# 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](/readme/headless/reference/price-groups.md) 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](/readme/headless/headless-api-authentication.md).

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 %}


---

# 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/custom-price-group-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.
