Extending Price Group Criteria

Price group criteria in Ucommerce are used to determine the accessibility of a price group.

Ucommerce has a built-in date-range criterion to control when a price group is valid. 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 price group 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: Member-Based Criterion

Below is an example of setting up a criterion that triggers for a specific member:

public class SetupPriceGroupMemberCriterion : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

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

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

        // Set up data using dbContext
        // Find the ShortText data type
        var dataType = dbContext.Set<DataTypeEntity>()
            .FirstOrDefault(x => x.Guid.ToString() == "2d65650b-810a-47d3-8431-a0608a853fed");
        var defFields = new List<DefinitionFieldEntity>
        {
            new()
            {
                Name = "Member",
                DataType = dataType,
                DisplayOnSite = true,
                RenderInEditor = true,
                Guid = Guid.Parse("b4f2b61e-7f71-4fba-a587-3c6ab8a701fe")
            }
        };

        dbContext.Set<DefinitionEntity>()
            .Add(new DefinitionEntity
            {
                BuiltIn = false,
                Description = "Member-based price group criterion",
                Name = "Member Criterion",
                DefinitionTypeId = 94868, //Id of the Price Group Criterion Definition Type
                DefinitionFields = defFields,
                Guid = Guid.Parse("b846d509-d1fb-4688-9db1-a23a4a6c66e1")
            });

        await dbContext.SaveChangesAsync(cancellationToken);
    }
}  

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 CheckPriceGroup pipeline with a pipeline task that checks whether the criterion is satisfied. There are three unique concepts to be aware of regarding this task:

  • PriceGroupCriterionDTO is a DTO connecting a criterion with its properties.

  • context.Output.PriceGroupCriteriaDtos iis a dictionary of PriceGroupCriterionDTO grouped by their definition.

  • context.Output.SatisfiedCriteria is a list of satisfied criteria. Add the criterion to this list if it's satisfied.

Remember to register your pipeline task through the pipeline builder.

Example

Here’s an example of a pipeline task that checks if the member criterion is satisfied. It compares the member ID provided in the input with the ID associated with the criterion.

public class CheckPriceGroupMemberCriterionTask : AbstractPipelineTask<CheckPriceGroupInput, CheckPriceGroupOutput>
{
    public override Task Execute(PipelineContext<CheckPriceGroupInput, CheckPriceGroupOutput> context, CancellationToken cancellationToken)
    {
        //Checks if the correct query parameter is given
        if (!context.Input.FilterProperties.ContainsKey("Member"))
        {
            return Task.CompletedTask;
        }

        //Checks if the member criteria is in the list of criteria, using the guid of the definition
        if (!context.Output.PriceGroupCriteriaDtos.TryGetValue(
                Guid.Parse("b846d509-d1fb-4688-9db1-a23a4a6c66e1"),
                out var allMemberCriteria))
        {
            return Task.CompletedTask;
        }

        foreach (var memberCriterion in allMemberCriteria)
        {
            var member = memberCriterion.Properties["b4f2b61e-7f71-4fba-a587-3c6ab8a701fe"]
                .Value; //Guid of the definition field "Member"

            if (context.Input.FilterProperties["Member"] == member)
            {
                context.Output.SatisfiedCriteria = context.Output.SatisfiedCriteria
                    .Add(memberCriterion.Criterion);
            }
        }

        return Task.CompletedTask;
    }
}

Validation (Optional)

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

UpdateCriterionInputValidatorBase 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 Member-based criterion that was created earlier.

public class UpdateCriteriaBuyAtMostCriteriaValidator : UpdateCriteriaInputValidatorBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="UpdateCriteriaQuantityCriteriaValidator"/> class.
    /// </summary>
    public UpdateCriterionMemberCriterionValidator()
        : base("b846d509-d1fb-4688-9db1-a23a4a6c66e1")
    {
        RuleForUpdatePropertyValue("Member")
            .Required()
            .IsEmail();
    }
}

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

Test that it works

After setting up the member-based criterion in the backoffice with a value (e.g., member@gmail.com), test its functionality by calling the headless endpoint for retrieving price groups. The endpoint accepts filter-* query parameters as described in the Price Groups reference and converts them into the property dictionary used in the pipeline.

Ensure the query parameter key matches the key in your pipeline task, and the value represents the user, e.g.:

GET {base_url}/api/v1/price-groups?filters-member=member@gmail.com&
    cultureCode="en-US"

Remember that calling headless endpoints will require authentication.

This endpoint returns price groups related to the store you are authenticating with, in order to see your price group, it should be in the allowed price groups list of a catalog on your store.

If the criterion is set up correctly, the price group will appear in the returned list only if the correct query parameter is provided. If the price group has a derived price group that is also accessible, only the deepest accessible price group will be shown.

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 AddPriceGroupMemberCriterion(this IUcommerceBuilder builder)
{
    builder.PipelineBuilder.InsertLast<
        IPipelineTask<CheckPriceGroupInput, CheckPriceGroupOutput>, 
        CheckPriceGroupMemberCriterionTask>();
    builder.Services.AddHostedService<SetupPriceGroupMemberCriterion>();
    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
    .AddPriceGroupMemberCriterion(); //Here

Last updated