# Custom Promotion Criteria

Ucommerce has built-in criteria for handling most use cases. However, if the need arises, it's quite easy to create a custom criterion.

## Create a Custom Criterion

### Creating a Definition

To create a definition, follow these steps:

1. Create a definition and add it to your database. The definition must be of the *criterion* definition type.
2. Add appropriate definition fields to it. Built-in Ucommerce data types will work out of the box.
3. If you wish to be able to reuse the criterion for future projects we recommend you create a background service to automate this process.

**Example: buy-less-than Criterion**

Below is an example of setting up a criterion that triggers only when a customer is buying less than a given quantity.

```csharp
public class SetupCustomCriterion : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    /// <inheritdoc />
    public SetupCustomCriterion(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var asyncScope = _serviceProvider.CreateAsyncScope();
        var dbContext = asyncScope.ServiceProvider.GetRequiredService<UcommerceDbContext>();
        if (dbContext.Set<DefinitionEntity>().Any(x => x.Name == "Buy At Most"))
        {
            return;
        }

        // Set up data using dbContext
        // Find the TargetOrderLine data type
        var dataType = dbContext.Set<DataTypeEntity>()
            .FirstOrDefault(x => x.Guid.ToString() == "b5295545-5583-41e1-8c93-7bc921c09e27");
        var defFields = new List<DefinitionFieldEntity>();
        defFields.Add(new DefinitionFieldEntity
        {
            Name = "Max Amount",
            DataTypeId = 3,
            DisplayOnSite = true,
            DefaultValue = "0",
            RenderInEditor = true,
            Guid = Guid.Parse("42085d44-3e81-4d94-aad8-8699ae7d35b0")
        });

        defFields.Add(new DefinitionFieldEntity
        {
            Name = "Target Orderline",
            DefaultValue = "0",
            DisplayOnSite = true,
            RenderInEditor = true,
            DataType = dataType,
            Guid = Guid.Parse("aa793b3f-2baf-45fb-a162-cb675e8d9b64")
        });
        dbContext.Set<DefinitionEntity>()
            .Add(new DefinitionEntity
            {
                BuiltIn = false,
                Description = "buy at Most criterion definition",
                Name = "Buy At Most",
                DefinitionTypeId = 5434834, //Id of the Criterion Definition Type
                DefinitionFields = defFields,
                Guid = Guid.Parse("99ab73d5-22ee-425c-97f8-f9794ed01944")
            });

        await dbContext.SaveChangesAsync(stoppingToken);
    }
}
```

{% hint style="info" %}
Using explicit GUIDs makes subsequent steps easier.
{% endhint %}

{% hint style="info" %}
Background services are run on every startup, so it is important to have a check to prevent multiple additions of the same values.&#x20;
{% endhint %}

### Implementing a Pipeline Task for Satisfaction Check

To trigger a criterion we need to extend the `SatisfiedCriteria` pipeline with a pipeline task that checks whether the criterion is satisfied. There are four unique concepts to be aware of regarding this task:

* `CriterionDTO` is a DTO connecting a criterion with its properties.&#x20;
* `context.Output.CriterionDtos` is a dictionary of `CriterionDTO` grouped by their definition.
* `Context.Output.SatisfiedOrderLineCriteria` is a dictionary of lists containing criteria grouped by the order line that satisfied them. Use this for order line-level criteria.&#x20;
* `context.Output.SatisfiedOrderCriteria` is a list of criteria that are satisfied by the order. Use this for order-level criteria.

{% hint style="info" %}
Remember to register your pipeline task through the pipeline builder.
{% endhint %}

#### Example

Here's an example of a pipeline task for checking if the buy-at-most criterion is triggered by an order. Since it can target both line- and order-level this task can handle both cases. This will not be necessary in most cases.

```csharp
 /// <summary>
 /// Task for checking buyAtMost criteria.
 /// </summary>
public class BuyAtMostCriteriaPipelineTask : AbstractPipelineTask<SatisfiedCriteriaInput, SatisfiedCriteriaOutput>
{
    /// <inheritdoc />
    public override Task Execute(PipelineContext<SatisfiedCriteriaInput, SatisfiedCriteriaOutput> context, CancellationToken cancellationToken)
    {
        if (!context.Output.CriterionDtos.TryGetValue(
            Guid.Parse("99ab73d5-22ee-425c-97f8-f9794ed01944"), 
            out var buyAtMostCriterionDtos))
        {
            return Task.CompletedTask;
        }

        var orderLineBuyAtMostCriterionDtos = buyAtMostCriterionDtos.Where(x => 
                x.Properties["aa793b3f-2baf-45fb-a162-cb675e8d9b64"]
                    .Value == BuyAtMostCriteriaConstants.ORDER_LINE_ENUM)
            .ToImmutableList();
        var orderLevelBuyAtMostCriterionDtos = buyAtMostCriterionDtos.Where(x => 
                x.Properties["aa793b3f-2baf-45fb-a162-cb675e8d9b64"]
                    .Value == BuyAtMostCriteriaConstants.ORDER_ENUM)
            .ToImmutableList();

        var satisfiedCriteriaOrderLevel = GetBuyAtMostCriteriaOrderLevel(
                orderLevelBuyAtMostCriterionDtos,
                context.Output.Cart.OrderLines.ToImmutableList())
            .ToImmutableList();
        context.Output.SatisfiedOrderCriteria = context.Output.SatisfiedOrderCriteria
            .Concat(satisfiedCriteriaOrderLevel)
            .ToImmutableList();

        var satisfiedCriteriaOrderLineLevel = GetBuyAtMostCriteriaOrderLineLevel(
                orderLineBuyAtMostCriterionDtos,
                context.Output.Cart.OrderLines.ToImmutableList())
            .ToImmutableList();

        foreach (var satisfiedCriterion in satisfiedCriteriaOrderLineLevel)
        {
            if (context.Output.SatisfiedOrderLineCriteria.TryAdd(
                satisfiedCriterion.OrderLine, 
                ImmutableList.Create(satisfiedCriterion.Criterion)))
            {
                continue;
            }

            context.Output.SatisfiedOrderLineCriteria[satisfiedCriterion.OrderLine] = 
                context.Output.SatisfiedOrderLineCriteria[satisfiedCriterion.OrderLine]
                    .Add(satisfiedCriterion.Criterion);
        }

        return Task.CompletedTask;
    }

    /// <summary>
    /// Gets the buyAtMost criteria that are satisfied by the given order lines.
    /// </summary>
    /// <returns>IEnumerable of buyAtMost criteria where the buyAtMost of the order 
    /// lines given is above the threshold if the TargetOrderLine is true.
    /// If the TargetOrderline is false it instead checks if the order lines in total 
    /// have a higher buyAtMost than the threshold.</returns>
    protected virtual IEnumerable<CriterionEntity> GetBuyAtMostCriteriaOrderLevel(
        ImmutableList<CriterionDTO> criterionDtos,
        IReadOnlyCollection<OrderLineEntity> orderLines)
    {
        foreach (var criterionDto in criterionDtos)
        {
            var generated = orderLines.Where(o => o.Properties
                    .Any(p => p.Key == Constants.OrderProperties.GENERATED))
                .ToList();
            var any = orderLines.Where(o => !generated.Contains(o))
                .Sum(x => x.Quantity) <= int.Parse(
                    criterionDto.Properties["42085d44-3e81-4d94-aad8-8699ae7d35b0"]
                        .Value!);
            if (any)
            {
                yield return criterionDto.Criterion;
            }
        }
    }

    /// <summary>
    /// Gets the buyAtMost criteria that are satisfied by the given order lines.
    /// </summary>
    /// <returns>IEnumerable of buyAtMost criteria where the buyAtMost of the order 
    /// lines given is above the threshold if the TargetOrderLine is true.
    /// If the TargetOrderline is false it instead checks if the order lines in total 
    /// have a higher buyAtMost that the threshold.</returns>
    protected virtual IEnumerable<SatisfiedOrderlineDTO> GetBuyAtMostCriteriaOrderLineLevel(
        ImmutableList<CriterionDTO> criterionDtos,
        IReadOnlyCollection<OrderLineEntity> orderLines)
    {
        var generated = orderLines.Where(o => o.Properties
                .Any(p => p.Key == Constants.OrderProperties.GENERATED))
            .ToList();
        var viableOrderLines = orderLines.Where(o => !generated.Contains(o));

        foreach (var criterionDto in criterionDtos)
        {
            foreach (var orderLine in viableOrderLines)
            {
                if (orderLine.Quantity <= int.Parse(
                        criterionDto.Properties["42085d44-3e81-4d94-aad8-8699ae7d35b0"]
                            .Value!))
                {
                    yield return new SatisfiedOrderlineDTO(orderLine, criterionDto.Criterion);
                }
            }
        }
    }
}
```

### Validation (Optional)

It is possible to set validation rules for a criterion using FluentValidation. To make this easier, inherit from `UpdateCriteriaInputValidatorBase` in namespace `Ucommerce.Web.BackOffice.Validators.Promotions.Criteria.UpdateCriteria` to set rules for your custom criteria.

`UpdateCriteriaInputValidatorBase` contains the method `RuleForUpdatePropertyValue(string propertyName)` where `propertyName` is the definition field´s name.

After creating a validator it needs to be registered as a service in the program.cs file.&#x20;

#### Example

Here's an example of custom rules for the Buy-At-Most criterion that was created earlier.

```csharp
public class UpdateCriteriaBuyAtMostCriteriaValidator : UpdateCriteriaInputValidatorBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="UpdateCriteriaQuantityCriteriaValidator"/> class.
    /// </summary>
    public UpdateCriteriaBuyAtMostCriteriaValidator()
        : base("99ab73d5-22ee-425c-97f8-f9794ed01944")
    {
        RuleForUpdatePropertyValue("Max Amount")
            .Required()
            .GreaterThan(2)
            .WithMessage("Must be a positive number higher than 2");

        RuleForUpdatePropertyEnum("Target Orderline")
            .Required()
            .HasEnumValue(CriteriaTypeConstants.QuantityCriterion.ORDER_ENUM, CriteriaTypeConstants.QuantityCriterion.ORDER_LINE_ENUM)
            .WithMessage("Must be either 'Order' or 'Order Line'");
    }
}
```

{% hint style="info" %}
`RuleForUpdatePropertyValue` is case sensitive so the string must exactly match the definition field name in the database.
{% endhint %}

## Tips for Reusability

To facilitate reuse of a custom criterion across projects, it is recommended to create an extension method that registers the needed services.

This is an example of a method for registering the different parts of a custom criterion:

<pre class="language-csharp"><code class="lang-csharp"><strong>public static IUcommerceBuilder AddCustomCriterion(this IUcommerceBuilder builder)
</strong>{
    builder.PipelineBuilder.InsertLast&#x3C;
        IPipelineTask&#x3C;SatisfiedCriteriaInput, SatisfiedCriteriaOutput>, 
        BuyAtMostCriteriaPipelineTask>();
    builder.Services.AddHostedService&#x3C;SetupCustomCriterion>();
    builder.Services.AddSingleton&#x3C;UpdateCriteriaInputValidatorBase, UpdateCriteriaBuyAtMostCriteriaValidator>();
    return builder;
}
</code></pre>

That way, it is now possible to add the criterion to any Ucommerce solution like this:

{% code fullWidth="false" %}

```csharp
var ucommerceBuilder = builder.Services
    .AddUcommerce(builder.Configuration)
    .AddBackOffice()
    .AddWebSite()
    .AddInProcess<RouteParser>()
    .UcommerceBuilder
    .AddCustomCriterion(); //Here
```

{% endcode %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://dev.ucommerce.net/readme/extensions/extending-criteria.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
