# Custom Price Group Criteria

Ucommerce has a few built-in criteria to control the context in which 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:

<pre class="language-csharp" data-full-width="false"><code class="lang-csharp"><strong>public class SetupPriceGroupMemberCriterion : BackgroundService
</strong>{
    private readonly IServiceProvider _serviceProvider;

    /// &#x3C;inheritdoc />
    public SetupPriceGroupMemberCriterion(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

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

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

        dbContext.Set&#x3C;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);
    }
}  
</code></pre>

{% hint style="info" %}
Using explicit GUIDs makes subsequent steps easier.&#x20;
{% 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 `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.

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

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

<pre class="language-csharp" data-full-width="false"><code class="lang-csharp">public class CheckPriceGroupMemberCriterionTask : AbstractPipelineTask&#x3C;CheckPriceGroupInput, CheckPriceGroupOutput>
{
    public override Task Execute(PipelineContext&#x3C;CheckPriceGroupInput, CheckPriceGroupOutput> context, CancellationToken cancellationToken)
    {
        //Checks if the correct query parameter is given
        if (!context.Input.FilterProperties.ContainsKey("Member"))
<strong>        {
</strong>            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;
    }
}
</code></pre>

### 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.&#x20;

#### Example

Here's an example of custom rules for the Member-based criterion that was created earlier.

```csharp
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();
    }
}
```

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

### 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](https://dev.ucommerce.net/readme/headless/reference/price-groups) 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.:

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

{% hint style="info" %}
Remember that calling headless endpoints will require [authentication](https://dev.ucommerce.net/readme/headless/headless-api-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.
{% endhint %}

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:

{% code fullWidth="false" %}

```csharp
public static IUcommerceBuilder AddPriceGroupMemberCriterion(this IUcommerceBuilder builder)
{
    builder.PipelineBuilder.InsertLast<
        IPipelineTask<CheckPriceGroupInput, CheckPriceGroupOutput>, 
        CheckPriceGroupMemberCriterionTask>();
    builder.Services.AddHostedService<SetupPriceGroupMemberCriterion>();
    return builder;
}
```

{% endcode %}

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
    .AddPriceGroupMemberCriterion(); //Here
```

{% endcode %}
