Ucommerce
  • Ucommerce Next Gen
    • Getting Started
      • Prerequisites
      • Licensing
      • Ucommerce Templates
      • Headless Template
      • MVC Template
    • Headless
      • Postman Collection
      • Headless API Authentication
        • Token endpoint - Authorization Header
        • Authorization Scopes
        • Refreshing the Access Token
      • Reference
        • Cart
        • Cart / Order Line Items
        • Shipment
        • Billing
        • Promotion Codes
        • Price Groups
        • Payment Methods
        • Countries
        • Shipping Methods
        • Catalogs
        • Cart Custom Properties
        • Line Item Custom Properties
        • Orders
        • Views for Cart modifying operations
      • Custom Headless APIs
      • Error Handling
      • Pagination
      • Deprecation
    • Backoffice Authentication
      • Microsoft Entra ID Example
      • Auth0 Authentication Example
    • Definitions
      • What is a Definition
    • Search and indexing
      • Configuration
      • Indexing
        • Index Definitions
        • Facets
        • Indexing Prices
        • Suggestions
        • Custom Data
      • Searching
    • Payment Providers
      • Stripe Provider Integration
      • Implementing a custom payment provider
    • Data Import
    • Miscellaneous
      • Media
      • Price Group Inheritance
      • Price Group Criteria
      • Soft Deletion Of Entities
      • Logging
      • OpenTelemetry
    • Extensions
      • Extending Pipelines
        • Order Processing Pipelines
        • Checkout Pipelines
      • Changing Service Behavior
        • Images
        • Content
      • Custom Headless APIs
      • Extend the Backoffice
        • Custom UI Components
      • Custom Editor UI
      • Custom Promotion Criteria
      • Custom Price Group Criteria
    • How-To
      • Migrate from Classic
        • Common database issues
      • Entities from code
        • Bootstrapping data on startup
        • Product Definitions & Fields
      • Discover pipelines and their tasks
      • Executing a pipeline
    • Integrations
      • Umbraco Media Delivery API
      • App Slices
        • Product Picker
  • Release Notes
  • Contact Us
Powered by GitBook
On this page
  • Create a Custom Criterion
  • Creating a Definition
  • Implementing a Pipeline Task for Satisfaction Check
  • Validation (Optional)
  • Tips for Reusability

Was this helpful?

  1. Ucommerce Next Gen
  2. Extensions

Custom Promotion Criteria

Promotion criteria in Ucommerce are used to determine when to trigger a discount on an order.

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.

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);
    }
}

Using explicit GUIDs makes subsequent steps easier.

Background services are run on every startup, so it is important to have a check to prevent multiple additions of the same values.

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.

  • 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.

  • context.Output.SatisfiedOrderCriteria is a list of criteria that are satisfied by the order. Use this for order-level criteria.

Remember to register your pipeline task through the pipeline builder.

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.

 /// <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.

Example

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

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'");
    }
}

RuleForUpdatePropertyValue is case sensitive so the string must exactly match the definition field name in the database.

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:

public static IUcommerceBuilder AddCustomCriterion(this IUcommerceBuilder builder)
{
    builder.PipelineBuilder.InsertLast<
        IPipelineTask<SatisfiedCriteriaInput, SatisfiedCriteriaOutput>, 
        BuyAtMostCriteriaPipelineTask>();
    builder.Services.AddHostedService<SetupCustomCriterion>();
    builder.Services.AddSingleton<UpdateCriteriaInputValidatorBase, UpdateCriteriaBuyAtMostCriteriaValidator>();
    return builder;
}

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

var ucommerceBuilder = builder.Services
    .AddUcommerce(builder.Configuration)
    .AddBackOffice()
    .AddWebSite()
    .AddInProcess<RouteParser>()
    .UcommerceBuilder
    .AddCustomCriterion(); //Here
PreviousCustom Editor UINextCustom Price Group Criteria

Last updated 8 months ago

Was this helpful?