# Ucommerce Next Gen

## Welcome

If you are brand new, we recommend starting with our [Getting Started](/readme/getting-started) guide.

Looking for a specific topic? Use the left-hand navigation or the search bar above.

To see new features, improvements, and bug fixes, you can find them in our [Release Notes](/release-notes).

## Feedback

We'd love to hear your positive and negative feedback, and we hope you will take the time to give us some. If you run into bugs and errors or have any feedback for improvements, please [send us an e-mail](mailto:feedback@ucommerce.net).

## Related Articles

{% content-ref url="/pages/AD8KRH3hFtnLMwKpHLvI" %}
[Release Notes](/release-notes)
{% endcontent-ref %}

{% content-ref url="/pages/0UQIcGzeVpB7NcgtOuQb" %}
[Getting Started](/readme/getting-started)
{% endcontent-ref %}


# Getting Started

Learn how to set up, configure and run Ucommerce.

The following articles are a step-by-step guide for getting started with Ucommerce. If you have any special requirements for your Ucommerce solution, feel free to contact us [here](https://ucommerce.net/contact)\
They cover the following topics:

* How to prepare your development environment.
* Choosing the suitable Ucommerce Template for your solution.
* Installing, configuring, and running a fully functional Ucommerce instance.


# Prerequisites

Before beginning, make sure you have the following installed:

* .NET SDK 10
* Latest Ucommerce 9 (for migrations only)
* \* [SQL Server Compatibility level 130](https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-database-transact-sql-compatibility-level?view=sql-server-ver16#compatibility_level--160--150--140--130--120--110--100--90--80-) or higher
* \* Elastic Server 8.11.0 or newer.
* \* Docker

\* When using the In-Process template, you can optionally use Docker. This eliminates the need for an SQL and Elastic Server on your machine.


# Licensing

## Trial

You can try out Ucommerce without a license. If you run your application without a license, it will shut down after 10 minutes. You can restart your application as many times as you want, giving you ample time to play around with the product.

## Developer license

When you have played around with Ucommerce and decided to use it for your project, you will want to get a free perpetual developer license.

To get your Ucommerce developer license, please contact us at <license@ucommerce.net>, with your company name and contact email address, and we will create a license for you or your company.

### Test environments

Ucommerce uses two ways to detect if the environment is test or production:

* The host environment
* The domain name of requests

#### Marking the host environment as a development environment

To make sure that the test environment is recognized as a development environment, it can be necessary to mark it as such. See [Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-8.0#set-the-environment-by-setting-an-environment-variable) for details.

#### Using test domain names

When using a developer license, make sure to access your test environment(s) through a domain name that includes one of the following:

* *localhost*
* *dev*
* *test*
* *uat*

Otherwise, Ucommerce will consider your instance as a **production environment**. Other (partial) domain names can be whitelisted. Please [contact us](mailto:license@ucommerce.net) with the values you would like to have whitelisted.

{% hint style="info" %}
An example of an allowed domain name is **test.mysite.com**
{% endhint %}

## Using your license

To add your license key to the application, simply add it to the configuration.

{% hint style="info" %}
It is recommended that you use [user secrets](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0#security-and-user-secrets) and/or a key vault, e.g. [Azure Key Vault](https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-8.0), to store your license key(s).
{% endhint %}

The `LicenseKey` property can be found under the `Ucommerce` object. It should look like this in your user secrets file:

```json
{
    "Ucommerce:LicenseKey": "XXXX-XXXX-XXXX-XXXX"
}
```

## Production license

When your project is ready for production, please [reach out to us](mailto:license@ucommerce.net) to get a new license key.


# Ucommerce Templates

To get started, we recommend using one of our .NET templates. A template will create a project with the correct dependencies and scaffold basic configuration for your application.

To install our templates package and get access to all templates:

* Open a terminal and run the following command:

```
dotnet new install Ucommerce.Templates
```

Once the templates package has been installed, a new project can be created using one of the available templates. Depending on the type of solution and use case, the following templates are available:

<table data-card-size="large" data-view="cards" data-full-width="false"><thead><tr><th align="center"></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center"><strong>Headless</strong></td><td><ul><li>Headless API</li><li>Administration Interface</li><li>SQL and Elasticsearch via Docker</li></ul></td><td>User touchpoints are decoupled from backend services. Recommended for most projects.</td><td><a href="/pages/cAqPPEalGBJAMNoxW55V">/pages/cAqPPEalGBJAMNoxW55V</a></td></tr><tr><td align="center"><strong>MVC</strong></td><td><ul><li>In-Process Libraries</li><li>Administration Interface</li><li>SQL and Elasticsearch via Docker</li><li>MVC implementation examples</li></ul></td><td>User touchpoints in the same process as backend services. A simpler but less flexible architecture. Recommended for quick client proof-of-concept projects.</td><td><a href="/pages/O6YaN7MDcZMsG5XK1S6l">/pages/O6YaN7MDcZMsG5XK1S6l</a></td></tr></tbody></table>

{% hint style="info" %}
Click the option above you want to learn more about. You can always return here if you change your mind.
{% endhint %}


# Headless Template

Getting started with the Standalone template.

## Setup

* Open a terminal where the new project should be set up.
* Execute the commands below:

```sh
# Generate the project
dotnet new uc-headless --name "<ProjectName>"

# Step into the new project folder
cd "ProjectName"

# Up the docker instances for SQL and Elasticsearch
docker compose -f docker/docker-compose.yml up -d

# Run the project
dotnet watch run
```

{% hint style="success" %}
Once your application runs, you can access the administration interface by requesting **/ucommerce**.
{% endhint %}

## Template options

<table><thead><tr><th width="208">Attribute</th><th width="203">Example Value</th><th width="249">Description</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td>--name</td><td>"TestProject"</td><td>A name to be used for the scaffolded project.</td><td>false</td></tr><tr><td>--elastic-password</td><td>"mypassword123"</td><td>Replace a default password for the Elasticsearch instance.</td><td>false</td></tr><tr><td>--sql-edge-password</td><td>"mypassword123"</td><td>Replace a default password for the SQL Edge instance.</td><td>false</td></tr><tr><td>--stripe</td><td>true</td><td>Add Stripe payment provider integration.</td><td>false</td></tr><tr><td>-h</td><td>-</td><td>Get a list of all available options. This won't execute the command.</td><td>false</td></tr></tbody></table>

## Related Articles

{% content-ref url="/pages/woVuLixbEGepQtqGIkwi" %}
[Headless](/readme/headless)
{% endcontent-ref %}


# MVC Template

## Setup

* Open a terminal where the new project should be set up.
* Execute the commands below:

```bash
# Generate the project
dotnet new uc-mvc --name "<ProjectName>"

# Step into the new project folder
cd "ProjectName"

# Up the docker instances for SQL and Elasticsearch
docker compose -f docker/docker-compose.yml up -d

# Run the project
dotnet watch run --project "<ProjectName>"
```

{% hint style="success" %}
Once your application runs, you can access the administration interface by requesting **/ucommerce**.
{% endhint %}

## Template options

<table><thead><tr><th width="208">Attribute</th><th width="203">Example Value</th><th width="249">Description</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td>--name</td><td>"TestProject"</td><td>A name to be used for the scaffolded project.</td><td>false</td></tr><tr><td>--elastic-password</td><td>"mypassword"</td><td>Replace a default password for the Elasticsearch instance.</td><td>false</td></tr><tr><td>--sql-edge-password</td><td>"mypassword"</td><td>Replace a default password for the SQL Edge instance.</td><td>false</td></tr><tr><td>--stripe</td><td>true</td><td>Add Stripe payment provider integration.</td><td>false</td></tr><tr><td>-h</td><td>-</td><td>Get a list of all available options. This won't execute the command.</td><td>false</td></tr></tbody></table>

{% hint style="info" %}
For a full list, you can execute:

`dotnet new uc-mvc -h`
{% endhint %}


# Headless

The Headless API is recommended to build quality projects with a long lifetime. It is designed to complete a transaction, from creating a Cart all the way to placing the order.

## Postman collection

The Postman collection is the perfect starting point. It contains a list of all available endpoints and can generate code examples for different languages/frameworks. It's our strong recommendation to supplement our documentation with the collection below.

[![Run In Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/32680912-d0572388-718a-4f1d-9439-753ab6454189?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D32680912-d0572388-718a-4f1d-9439-753ab6454189%26entityType%3Dcollection%26workspaceId%3Da5c90330-7879-45fc-b3c9-2dded67b746a)

## Sections

* Authentication

{% content-ref url="/pages/eVwdA0iu4XBfKo8ZncVO" %}
[Headless API Authentication](/readme/headless/headless-api-authentication)
{% endcontent-ref %}

* API reference and examples:

{% content-ref url="/pages/9qPvxuVB9ztoDyoHTt8A" %}
[Reference](/readme/headless/reference)
{% endcontent-ref %}

* Instructions on extending the Headless API:

{% content-ref url="/pages/Z2wj7Oo0F359u1n1qu8k" %}
[Custom Headless APIs](/readme/headless/custom-headless-apis)
{% endcontent-ref %}


# Headless API Authentication

Quick start guide - Authenticating a client

## Obtaining Credentials

1. Navigate to the Ucommerce Backoffice (`/ucommerce`) as an administrator.
2. In the Settings menu, click `API Access`.
3. When having multiple stores, one must be selected in the top-right corner dropdown.
4. Take note of the `Client ID` and the matching `Secret`.
5. Add the URL of your client to the `URL Whitelist`.

The headless API uses OAuth2 for authentication. This involves 2 steps: getting an authentication code and exchanging the code for a token.

## Connect

```bash
curl -D- -G \
    <base_url>/api/v1/oauth/connect \
    -d client_id=<CLIENT_ID> \
    -d redirect_uri=<REDIRECT_URI> \
    -d response_type=code
```

* `redirect_uri` is where you will be redirected to after the call has finished. This needs to match a redirect URL specified in the `URL Whitelist`.

The expected response is a `302 (Found or Moved Temporarily)`.

The `location` header contains the authentication code. This will be used in the next step.

{% hint style="warning" %}
The code expires after 1 minute. A new code must be requested if it has not been exchanged for a token.
{% endhint %}

{% hint style="info" %}
The `code` is URL encoded. The code will need to be [URL-decoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) before the next step. Some web frameworks might do that automatically.
{% endhint %}

## Exchange code for a token

```bash
curl -D- -X POST <base_url>/api/v1/oauth/token \
    -u <CLIENT_ID>:<CLIENT_SECRET> \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    -d '{
            "grant_type" : "authorization_code"
            "code": "<CODE>"
            "redirect_uri" : "<REDIRECT_URL>"
        }'
```

{% hint style="danger" %}
The authorization header above must be formatted. You can find an explanation and examples here: [Token endpoint - Authorization Header](/readme/headless/headless-api-authentication/token-endpoint-authorization-header)
{% endhint %}

An example of an expected response:

```json
{
  "access_token": "eyJhbGciOiJIUzI1Ni978juanR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJmYTE4OTI3MS1mOTY1LTRmNWMtOTlmOS1lNDViNzNiYzI4MzkiLCJjbGllbnRJZCI6InZpaWEtZnBwIiwicm9sZSI6IkNsaWVudFVzZXIiLCJzZ67623aW9uSWQiOiJlZmE1NWU0ZS0xZTUxLTQ1YWMtYWEyYy01OThhNjFjMTZlOTYiLCJuYmYiOjE1Njc0MTQxNzMsImV4cCI6MTU2NzQxNzc3MywiaWF0IjoxNTY3NDE0MTczfQ.7QD6zGcdonYy79384buXOqsykWrbWa3L6LW4d9uzb-zA",
  "expires_in": 300,
  "redirect_uri": "https://httpbin.org/anything",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI687234pXVCJ9.eyJ1c2VySWQiOiJmYTE4OTI3MS1mOTY1LTRmNWMtOTlmOS1lNDViNzNiYzI4MzkiLCJjbGllbnRJZCI6InZpaWEtZnBwIiwiY29uc2VudElkIjoiYTYyODExYWYtNzUxMS00ZWQ0LoyiauasiYTEtMjAwNzc2NGQ1MTIwIiwic2Vzc2lvbklkIjoiZWZhNTVlNGUtMWU1MS00NWFjLWFhMmMtNTk4YTYxYzE2ZTk2Iiwicm9sZSI6IlJlZnJlc2hUb2tlbiIsIm5iZiI6MTU2NzQxNDE3MywiZXhwIjoxNTY4NjIzNzczLCJpYXQiOjE1Njc0MTQxNzN9.5-x0NNg5lMxPnZRYtu983764q0sbPcSb7U9b23e3Zwx0Ss9I",
  "token_type": "bearer"
}
```

* `access_token` will be used in all future requests.
* `expires_in` can be used to identify when to refresh the token.
* `refresh_token` will be used to refresh an expired or expiring token.

## Using the token

All subsequent requests to the Headless API require the `access_token` in their respective `Authorization` headers.


# Token endpoint - Authorization Header

The `/api/v1/oauth/token` endpoint uses the Basic HTTP Authentication scheme (as defined in [rfc7617](https://datatracker.ietf.org/doc/html/rfc7617)).

The format for the header is: `Basic : <credentials encoded as base64>`. A step-by-step to formatting in .NET:

* Take the `clientId` and the `secret` and format them as delimited by a colon (:)

```csharp
string credentials = $"{clientId}:{clientSecret}";
```

* Next, encode this string as a base64 string

```csharp
byte[] credentialsByteData = Encoding.GetEncoding("iso-8859-1").GetBytes(credentials);
string base64Credentials = Convert.ToBase64String(credentialsByteData);
```

* Lastly, format it to include the "Basic" keyword, followed by the now encoded credentials:

```csharp
return $"Basic {base64Credentials}";
```

Put together in a method that can be reused for both token and refresh token requests:

```csharp
public string GenerateBasicAuthorizationHeaderValue(string clientId, string clientSecret)
{
    string credentials = $"{clientId}:{clientSecret}";
    byte[] credentialsByteData = Encoding.GetEncoding("iso-8859-1").GetBytes(credentials);
    string base64Credentials = Convert.ToBase64String(credentialsByteData);
    return $"Basic {base64Credentials}";
}
```


# Authorization Scopes

Scopes grants clients access to functionality reserved for special permissions. You can request one or more of the scopes listed below in the query parameter `scope` [when authenticating a client](/readme/headless/headless-api-authentication).

The following scopes are available:

| Scope                     | Functionality     | Description                                          |
| ------------------------- | ----------------- | ---------------------------------------------------- |
| transactions:custom:price | Custom Unit Price | Add or update a custom price or tax for a line item. |

Example:

```bash
curl -D- -G <base_url>/api/v1/oauth/connect \
    -d client_id=<CLIENT_ID> \
    -d redirect_uri=<REDIRECT_URL> \
    -d scope="transactions:custom:price" \
    -d response_type=code 
```


# Refreshing the Access Token

Access tokens expire in 5 minutes after generation. Once it expires, the `refresh_token` can be used to refresh it. Refresh tokens are valid for 90 days.

```bash
curl -D- -X POST <base_url>/api/v1/oauth/token \
    -u <CLIENT_ID>:<CLIENT_SECRET> \
    -H 'Content-Type: application/json'  \
    -d '{
            "grant_type" : "refresh_token",
            "refresh_token" : "<refresh_token>"
        }'
```

An example of a valid response:

```json
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI797234J9.eyJ1c2VySWQiOiJmYTE4OTI3MS1mOTY1LTRmNWMtOTlmOS1lNDViNzNiYzI4MzkiLCJjbGllbnRJZCI6InZpaWEtZnBwIiwicm9sZSI6IkNsaWVudFVzZXIiLCJzZXNzaW9uSWQiOiJhZGIy097298234gtYWM4Yy1kYWM5Zjk0NTk3ZWQiLCJuYmYiOjE1Njc1MDAwOTQsImV4cCI6MTU2NzUwMzY5NCwiaWF0IjoxNTY3NTAwMDk0fQ.39njmCN97823498UbPXUiXl_SmWgnxM2x9phxhAxYI",
    "expires_in": 300,
    "redirect_uri": null,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI797234J9.eyJ12Vy98762872344OTI3MS1mOTY1LTRmNWMtOTlmOS1lNDViNzNiYzI4MzkiLCJjbGllbnRJZCI6InZpaWEtZnBwIiwiY29uc2VudElkIjoiYTYyODExYWYtNzUxMS00ZWQ0LThiYTEtMjAwNzc2NGQ1MTIwIiwic2Vzc2lvbklkIjoiYWRiMjEyNmEtOTczZi00OWI4LWFjOGMtZGFjOWY5NDU5N2VkIiwicm9sZSI6IlJlZnJlc2hUb2tlbiI98o727934UwMDA5NCwiZXhwIjoxNTY4NzA5Njk0LCJpYXQiOjE1Njc1MDAwOTR9.6eBV4OH96782734HEQoMbB_9yedl_2JfzsiSNcwa0",
    "token_type": "bearer"
}
```

The new `access_token` and `refresh_token` can now be used for requests.


# Reference


# Cart

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)

## Create a Cart

### Parameters

* `cultureCode`, e.g. `en-US` (string)
* `currency`, e.g. `DKK` (string)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "currency": "<currency>"
    }'
```

### Response

```json
{
    "cartId": "fec4b401-6eb9-4af7-a907-73c817bcaf31"
}
```

## Get Cart

### Parameters

* `cartId` from [#create-a-cart](#create-a-cart "mention") (string)

### Request

<pre class="language-bash"><code class="lang-bash">curl -D- -X GET &#x3C;base_url>/api/v1/carts/&#x3C;cartId> \
<strong>    -H 'Authorization: Bearer &#x3C;ACCESS_TOKEN>'
</strong>    -H 'Content-Type: application/json' \
</code></pre>

### Response

```json
{
    "billingAddress":{
        "addressName": "{addressName}",
        "attention": "{attention}",
        "city": "{city}",
        "companyName": "{companyName}",
        "country":{
            "cultureCode": "{cultureCode}",
            "id": "{id}",
            "name": "{name}"
        },
        "emailAddress": "{email}",
        "firstName": "{firstName}",
        "id": "{id}",
        "lastName": "{lastName}",
        "line1": "{line1}",
        "line2": "{line2}",
        "mobilePhoneNumber": "{mobilePhoneNumber}",
        "phoneNumber": "phoneNumber",
        "postalCode": "{postalCode}",
        "state": "{state}"
    },
    "billingCurrency":{
        "id": "{id}",
        "isoCode": "{isoCode}"
    },
    "customer": "{customer}",
    "discounts": "{[discount1, discount2]}",
    "shipments": [
        {
            "deliveryNote": "{deliveryNote}",
            "id": "{id}",
            "orderLines": [
                {
                    "discount": "{discount}",
                    "discounts": "{[discount1, discount2]}",
                    "id": "{id}",
                    "price": "{price}",
                    "priceGroupId": "{priceGroupId}",
                    "productCatalogId": "{productCatalogId}",
                    "productName": "{productName}",
                    "quantity": "{quantity}",
                    "sku": "{sku}",
                    "total": "{total}",
                    "unitDiscount": "{unitDiscount}",
                    "variantSku": "{variantSku}",
                    "tax": "{tax}",
                    "taxRate": "{taxRate}",
                    "orderLineProperties": "[]"
                }
            ],
            "shipmentAddress":{
                "addressName": "{addressName}",
                "attention": "{attention}",
                "city": "{city}",
                "companyName": "{companyName}",
                "country":{
                    "cultureCode": "{cultureCode}",
                    "id": "{id}",
                    "name": "{countryName}"
                },
                "emailAddress": "{email}",
                "firstName": "{firstName}",
                "id": "{id}",
                "lastName": "{lastName}",
                "line1": "{line1}",
                "line2": "{line2}",
                "mobilePhoneNumber": "{mobilePhoneNumber}",
                "phoneNumber": "{phoneNumber}",
                "postalCode": "{postalCode}",
                "state": "{state}"
            },
            "shipmentDiscount": "{shipmentDiscount}",
            "shipmentName": "{shipmentName}",
            "shipmentPrice": "{shipmentPrice}",
            "shipmentPriceTotal": "{shipmentPriceTotal}",
            "shippingMethod":{
                "defaultPaymentMethod": "{defaultPaymentMethod}",
                "eligiblePaymentMethods":[
                    {
                        "displayName": "{displayName}",
                        "feePercent": "{feePercent}",
                        "fees":[
                            {
                                "amount": "{amount}",
                                "priceGroupId": "{priceGroupId}"
                            }
                        ],
                        "id": "{id}",
                        "imageUrl": "{imageUrl}",
                        "name": "{name}",
                        "paymentMethodProperties":[
                            {
                                "id": "{id}",
                                "cultureCode": "{cultureCode}",
                                "value": "{value}"
                            }
                        ]
                    }
                ],
                "id": "{id}",
                "name": "{name}"
            },
            "tax": "{tax}",
            "taxRate": "{taxRate}",
            "trackAndTrace": "{trackAndTrace}"
        }
    ],
    "customProperties":[
        {
            "id":"{id}",
            "key": "{key}",
            "value": "{value}"
        }
    ],
    "createdDate": "{createdDate}",
    "cultureCode": "{cultureCode}",
    "discount": "{discount}",
    "discountTotal": "{discountTotal}",
    "id": "{id}",
    "note": "{note}",
    "orderLines":[
        {
            "discount": "{discount}",
            "discounts": "{[]}",
            "id": "{id}",
            "price": "{priceAmount}",
            "priceGroupId": "{priceGroupId}",
            "productCatalogId": "{productCatalogId}",
            "productName": "{productName}",
            "quantity": "{quantity}",
            "sku": "{sku}",
            "total": "{total}",
            "unitDiscount": "{unitDiscount}",
            "variantSku": "{variantSku}",
            "tax": "{tax}",
            "taxRate": "{taxRate}",
            "customProperties":[
                {
                    "id":"{id}",
                    "key":"{propKey}",
                    "value":"{propValue}"
                }
            ]
        }
    ],
    "orderTotal": "{orderTotal}",
    "paymentTotal": "{paymentTotal}",
    "shippingTotal": "{shippingTotal}",
    "subTotal": "{subTotal}",
    "tax": "{tax}"
}
```

## Converting a Cart to an Order

### Parameters

* `cartId` from [#create-a-cart](#create-a-cart "mention")(string)
* `cultureCode`, e.g. `en-US` (string)
* `priceGroupId`, from [Price Groups](/readme/headless/reference/price-groups)
* `paymentMethodId`, from [Payment Methods](/readme/headless/reference/payment-methods)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/payments \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cartId": "<cartId>",
        "paymentMethodId": "<paymentMethodId>",
        "priceGroupId": "<priceGroupId>",
        "cultureCode": "<cultureCode>"
    }'
```

### Response

```json
{
    "paymentId": "fec4b401-6eb9-4af7-a907-73c817bcaf31",
    "paymentUrl": "<base_url>/api/v1/checkout?payment=fec4b401-6eb9-4af7-a907-73c817bcaf31&paymentMethod=5d7d4d0f-581c-408a-ae0d-c4a77d2d883c"
}
```

### Error Handling

| Error              | Description                                                                                                                 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| BadRequest (400)   | Execution of the pipeline fails; the Billing address is missing from the order; the Cart does not belong to the Store, etc. |
| Unauthorized (401) | The token is expired.                                                                                                       |
| Forbidden (403)    | The token does not have access to this endpoint.                                                                            |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Billing address is missing from the order.",
            "error": "BadRequest"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Cart / Order Line Items

You can enable a mini cart view in the following requests to show changes to the cart immediately. Find out more in the [Views for Cart modifying operations](/readme/headless/reference/views-for-cart-modifying-operations)

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cartId` from [Cart](/readme/headless/reference/cart#create-a-cart)

## Add Line Item to a Cart

### Parameters

* `catalogId` from [Catalogs](/readme/headless/reference/catalogs)
* `priceGroupId` from [Price Groups](/readme/headless/reference/price-groups) (string)
* `cultureCode`, e.g. `en-US` (string)
* `sku` (string)
* `quantity` (integer)

### Optional Parameters

* `addToExistingOrderline` (default: `true`).
  * If `false`, a new order line will be added even if the SKU is already in the Cart. (boolean)
* `variantSku` used alongside `sku` to identify a product variant. (string)
* `customProperties` a list of `key` (string) and `value` (string) pairs
* `price` custom unit price for the line item (decimal)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/lines \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
            "quantity": <quantity>,
            "sku": <sku>,
            "variantSku": <variantSku>,
            "priceGroupId": <priceGroupId>,
            "catalogId": <productCatalogId>,
            "cultureCode": <cultureCode>,
            "price" : <price>
            "customProperties":  [
                {
                    "key" : <propKey>,
                    "value": <propValue>
                }
            ]
        }'
```

### Custom Price

By default, Ucommerce calculates the price of the order line based on the product's price. To use a custom price instead, this can be specified in the `price` parameter.

{% hint style="warning" %}
**Note:** Due to risks associated with explicitly setting a product price, we recommend not to use custom price operations in publicly available applications and/or endpoints. Additionally, if the client can access price setters, you are potentially open to price interception. We recommend not to use the `transactions:custom:price` scope outside of internal applications.
{% endhint %}

## Delete Line Item from a Cart

A Line Item can be removed by either ID or SKU (and Variant SKU in case of a variant).

### Parameters

* `priceGroupId` from the Cart or from [Price Groups](/readme/headless/reference/price-groups)
* `cultureCode`, e.g. `en-US` (string)
* `sku` (string) or `lineId` (string)

### Optional Parameters

* `variantSku`, if the product being removed is a variant (string)

### Request

```bash
curl -D- -X DELETE <base_url>/api/v1/carts/<cartId>/lines/<lineId> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
            "PriceGroupId": <priceGroupId>,
            "CultureCode": <cultureCode>
        }'
```

## Update Line Item on a Cart

The following properties can be updated on a Line Item during checkout:

* Quantity
* Price
* TaxRate

{% hint style="info" %}
**Note:** When modifying the price or the tax rate, your access\_token needs the scope `transactions:custom:price`. If you modify the price and tax rate, Ucommerce will not edit those values in the future.
{% endhint %}

### Parameters

* `lineId` (string)
* `priceGroupId` [Price Groups](/readme/headless/reference/price-groups)
* `cultureCode`, e.g. `en-US` (string)

### Optional Parameters

* `quantity` (integer)
* `price` (decimal)
* `taxRate` (decimal)

```bash
curl -D- -X PATCH <base_url>/api/v1/carts/<cartId>/lines/<lineId>?PriceGroupId=<id>&CultureCode=<culture> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
            "Quantity": <quantity>,
            "Price": <price>,
            "TaxRate": <taxRate>           
        }'
```

### Error Handling

| Error              | Description                                                                                                                                                    |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BadRequest (400)   | The product does not exist; Price Group does not exist; Order Line does not exist; Mismatch between Cart and Price Group's currency; pipeline execution fails. |
| Unauthorized (401) | The token is expired.                                                                                                                                          |
| Forbidden (403)    | The token does not have access to this endpoint.                                                                                                               |
| NotFound (404)     | Cart does not exist.                                                                                                                                           |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Cart does not exist..",
            "error": "NotFound"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Shipment

You can enable a mini cart view in the following requests to show changes to the cart immediately. Find out more in the [Views for Cart modifying operations](/readme/headless/reference/views-for-cart-modifying-operations)

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cartId` from [Cart](/readme/headless/reference/cart#create-a-cart)
* `countryId` from from [Countries](/readme/headless/reference/countries)
* `shippingMethodId` from [Shipping Methods](/readme/headless/reference/shipping-methods)
* `priceGroupId` from the cart or from [Price Groups](/readme/headless/reference/price-groups)

## Add shipping information

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/shipping \
        -H 'Authorization: Bearer <ACCESS_TOKEN>'
        -H 'Content-Type: application/json' \
        -d '{
            "priceGroupId": "<priceGroupId>",
            "shippingMethodId": "<shippingMethodId>",
            "shippingAddress": {
                    "attention":"<attention>",
                    "city": "<city>",
                    "companyName":"<companyName>",
                    "countryId": "<countryId>",
                    "email":"<email>",
                    "firstName": "<firstName>",
                    "lastName": "<lastName>",
                    "line1": "<addressLine1>",
                    "line2": "<addressLine2>",
                    "mobileNumber":"<mobileNumber>",
                    "phoneNumber":"<phoneNumber>",
                    "postalCode": "<postalCode>",
                    "state":"<state>",
                },
            }'
```

### Response

```json
{
"success": "true"
}
```

### Error Handling

| Error              | Description                                                                                    |
| ------------------ | ---------------------------------------------------------------------------------------------- |
| BadRequest (400)   | Missing or incorrect access token; Bad or missing request data; pipeline execution fails; etc. |
| Unauthorized (401) | The token is expired.                                                                          |
| Forbidden (403)    | The token does not have access to this endpoint.                                               |
| NotFound (404)     | Cart not found; Country not found; Price group not found.                                      |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Country not found.",
            "error": "NotFound"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Billing

You can enable a mini cart view in the following requests to show changes to the cart immediately. Find out more in the [Views for Cart modifying operations](/readme/headless/reference/views-for-cart-modifying-operations)

### Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cartId` from [Cart](/readme/headless/reference/cart#create-a-cart)
* `countryId` from [Countries](/readme/headless/reference/countries)

## Add billing information

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/billing-address \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "city": "<City>",
        "firstName": "<firstName>",
        "lastName": "<lastName>",
        "postalCode": "<postalCode>",
        "line1": "<addressLine1>",
        "line2": "<addressLine2>",
        "countryId": "<countryId>",
        "email":"<email>",
        "state":"<state>",
        "mobileNumber":"<mobileNumber>",
        "attention":"<attention>",
        "companyName":"<companyName>"
    }'
```

### Response

```json
{
"success": "true"
}
```

### Error Handling

| Error              | Description                                                                                                       |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| BadRequest (400)   | Missing or incorrect access token; Country not found; Bad or missing request data; pipeline execution fails; etc. |
| Unauthorized (401) | The token is expired.                                                                                             |
| Forbidden (403)    | The token does not have access to this endpoint.                                                                  |
| NotFound (404)     | Cart not found; Country not found.                                                                                |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Country not found.",
            "error": "NotFound"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Promotion Codes

You can enable a mini cart view in the following requests to show changes to the cart immediately. Find out more in the [Views for Cart modifying operations](/readme/headless/reference/views-for-cart-modifying-operations)

### Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cartId` from [Cart](/readme/headless/reference/cart#create-a-cart)
* `priceGroupId` from [Price Groups](/readme/headless/reference/price-groups) (string)
* `cultureCode`, e.g. `en-US` (string)

## Add Promotion Codes

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/promotion-codes/<code> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
            "priceGroupId": "<priceGroupId>",
            "cultureCode": "<CultureCode>"
        }'
```

### Response

```json
{
    "promotionCodes": [
        {
            "code": "{promotionCode}",
        }
    ]
}
```

## Get Promotion Codes

### Request

```bash
curl -D- -X GET <base_url>/api/v1/carts/<cartId>/promotion-codes \
        -H 'Authorization: Bearer <ACCESS_TOKEN>'
        -H 'Content-Type: application/json' \
```

### Response

```json
{
    "promotionCodes": [
        {
            "code": "{promotionCode}",
        }
    ]
}
```

## Remove Promotion Codes

### Request

```bash
curl -D- -X DELETE <base_url>/api/v1/carts/<cartId>/promotion-codes/<code> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
            "priceGroupId": "<priceGroupId>",
            "cultureCode": "<cultureCode>"
        }'
```

### Response

```json
{
    "promotionCodes": [
    ]
}
```

## Error Handling

| Error            | Description                               |
| ---------------- | ----------------------------------------- |
| BadRequest (400) | PriceGroup does not exist.                |
| NotFound (404)   | Cart not found; Promotion Code not found. |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "AccessToken not found, access denied.",
            "error": "BadRequest"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Price Groups

## Prerequisites

* An `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* A `cultureCode`, e.g., `en-US` (string)

## Get Price Groups

### Optional Parameters

* `maxItems`, limits the number of results returned. in [Pagination](/readme/headless/pagination)
* `nextPagingToken`, required to fetch the next page. Read more in [Pagination](/readme/headless/pagination)
* `filters-*,` used for [filtering](#filtering-valid-price-groups) for valid price groups based on [price group criteria](/readme/miscellaneous/price-group-criteria).

### Request

```bash
curl -D- -X GET <base_url>/api/v1/price-groups?cultureCode=<cultureCode>&maxItems=<maxItems>&nextPagingToken=<nextPagingToken> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json'
```

### Response

```json
{
    "nextPagingToken": null,
    "priceGroups": [
        {  
            id: "{id}",
            name: "{priceGroupsName}"
        }
    ]
}
```

## Error Handling

| Error              | Description                                      |
| ------------------ | ------------------------------------------------ |
| BadRequest (400)   | Invalid access token; Invalid culture code etc.  |
| Unauthorized (401) | The token is expired.                            |
| Forbidden (403)    | The token does not have access to this endpoint. |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Invalid cultureCode.",
            "error": "BadRequest"
        }
    ]
}
```

## Filtering Valid Price Groups

To get the valid price groups for a context, this endpoint supports giving it a set of properties prefixed with `filters-`. Setting any filter property will make the API only return price groups that are valid according to the criteria set up for them.\
The API will map any parameters prefixed `filters-` to a dictionary of properties, sent to the `GetPriceGroups` pipeline, in the following way:\
The parameter name will be mapped to the key in the dictionary, with the `filters-` part of the name stripped off. The parameter value will be mapped to the value in the dictionary.

```sh
curl -D- -X GET <base_url>/api/v1/price-groups?filters-customer=29ae4d70-f0dd-4eca-83bf-7125920f3279&cultureCode=<cultureCode>&maxItems=<maxItems>&nextPagingToken=<nextPagingToken> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json'
```

In the above example, we're calling the API with a `filters-customer` query parameter.\
The key will be `customer` and the value will be `29ae4d70-f0dd-4eca-83bf-7125920f3279` - the GUID of the current customer.

This can be used to send filtering properties used by either the built-in criteria, or your custom criteria. The built-in criteria uses the following keys:

* Key `customer` - Validates the customer against all customer group or organization criteria.
  * Value: `Guid` of the customer
* Key `customerGroup` - Validates the customer group against all customer group criteria.
  * Value: `Guid` of the customer group
* Key `organization` - Validates the organization against all organization criteria.
  * Value: `Guid` of the organization

The time-based criterion doesn't use any properties as it is validated via server time. This means, that, if no other filter properties are needed, you will need to add an unused filter property, e.g. `filters-time=` to trigger the filtering.

See the [guide on extending price group criteria](/readme/extensions/custom-price-group-criteria) for more details on how to use the properties in a pipeline task.

{% hint style="info" %}
If any filtering properties are given, all criteria will be checked. This means that even if the only filtering property sent is a customer guid, all time-based criteria will also influence the filtering.
{% endhint %}

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Payment Methods

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cultureCode`, e.g. `en-US` (string)
* `countryId` from [Countries](/readme/headless/reference/countries)

## Get Payment Methods

### Optional Parameters

* `maxItems`, limits the number of results returned. in [Pagination](/readme/headless/pagination)
* `nextPagingToken`, required to fetch the next page. Read more in [Pagination](/readme/headless/pagination)
* `priceGroupId`, limits the results to Payment Methods available for a Price Group. From [Price Groups](/readme/headless/reference/price-groups)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/payment-methods?countryId=<countryId>&cultureCode=<cultureCode>&priceGroupId=<priceGroupId>&maxItems=<maxItems>&nextPagingToken=<nextPagingToken> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json'
```

### Response

```json
{
    "nextPagingToken": "{nextPagingToken | null}",
    "paymentMethods": [
        {
            "description": "{description}",
            "displayName": "{displayName}",
            "feePercent": "{feePercent}",
            "fees": [
                {
                    "amount": "{amount}",
                    "priceGroupId": "{priceGroupId}"
                }
            ],
            "id": "{id}",
            "imageUrl": "{imageUrl}",
            "name": "{paymentName}"
        },
        {
            "displayName": "{displayName}",
            "feePercent": "{feePercent}",
            "fees": [
                {
                    "amount": "{amount}",
                    "priceGroupId": "{priceGroupId}"
                }
            ],
            "id": "{id}",
            "imageId": "{imageId}",
            "imageUrl": "{imageUrl}",
            "name": "{name}"
        }
    ]
}
```

## Error Handling

| Error              | Description                                             |
| ------------------ | ------------------------------------------------------- |
| BadRequest (400)   | PriceGroup does not exist; Country does not exist; etc. |
| Unauthorized (401) | The token is expired.                                   |
| Forbidden (403)    | The token does not have access to this endpoint.        |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Invalid cultureCode.",
            "error": "BadRequest"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Countries

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)

## Get Countries

### Optional Parameters

* `filterOnStore`, limits the result to Countries with available Shipping or Payment Methods available for the authenticated store [Headless API Authentication](/readme/headless/headless-api-authentication)
* `maxItems`, limits the number of results returned. in [Pagination](/readme/headless/pagination)
* `nextPagingToken`, required to fetch the next page. Read more in [Pagination](/readme/headless/pagination)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/countries?filterOnStore=false&maxItems=<maxItems>&nextPagingToken=<nextPagingToken> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

### Response

```json
{
    "countries": [
        {
            "cultureCode": "{cultureCode}",
            "id": "{countryId}",
            "name": "{countryName}"
        }
    ],
    "nextPagingToken": "{nextPagingToken | null}"
}
```

## Error Handling

| Error              | Description                                      |
| ------------------ | ------------------------------------------------ |
| Unauthorized (401) | The token is expired.                            |
| Forbidden (403)    | The token does not have access to this endpoint. |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "The token is expired.",
            "error": "Unauthorized"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Shipping Methods

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cultureCode`, e.g. `en-US` (string)
* `countryId` from [Countries](/readme/headless/reference/countries)
* `priceGroupId` from the cart or from [Price Groups](/readme/headless/reference/price-groups)

## Get Shipping Methods

### Optional Parameters

* `maxItems`, limits the number of results returned. in [Pagination](/readme/headless/pagination)
* `nextPagingToken`, required to fetch the next page. Read more in [Pagination](/readme/headless/pagination)

### Request

```bash
curl -D- -X GET <base_url>/api/v1/shipping-methods?cultureCode=<cultureCode>&countryId=<countryId>&priceGroupId=<priceGroupId> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

### Response

```json
{
   "nextPagingToken": null,
   "shippingMethods": [
        {
           "description": "{description}",
           "displayName": "{displayName}",
           "id": "{id}",
           "imageUrl": "{imageUrl}",
           "name": "{name}",
           "price": {
           "amount": "{amount}",
           "currency": "{currency}"
           }
        },
        {
           "description": "{description}",
           "displayName": "{displayName}",
           "id": "{id}",
           "imageUrl": "{imageUrl}",
           "name": "{name}",
           "price": {
           "amount": "{amount}",
           "currency": "{currency}"
           }
        },
        {
           "description": "{description}",
           "displayName": "{displayName}",
           "id": "{id}",
           "imageUrl": "{imageUrl}",
           "name": "{name}",
           "price": {
           "amount": "{amount}",
           "currency": "{currency}"
           }
        }
       ]
}
```

## Error Handling

| Error              | Description                                                         |
| ------------------ | ------------------------------------------------------------------- |
| BadRequest (400)   | No shipping methods are found for the given price group or country. |
| Unauthorized (401) | The token is expired.                                               |
| Forbidden (403)    | The token does not have access to this endpoint.                    |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Shipping methods not found on the given context.",
            "error": "BadRequest"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Catalogs

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cultureCode`, e.g. `en-US` (string)
* `priceGroupId` from the cart or from [Price Groups](/readme/headless/reference/price-groups)

## Get Catalogs

### Optional Parameters

* `maxItems`, limits the number of results returned. in [Pagination](/readme/headless/pagination)
* `nextPagingToken`, required to fetch the next page. Read more in [Pagination](/readme/headless/pagination)

### Request

<pre class="language-bash"><code class="lang-bash">curl -D- -X GET &#x3C;base_url>/api/v1/catalogs?cultureCode=&#x3C;cultureCode>&#x26;priceGroupId=&#x3C;priceGroupId>&#x26;maxItems=&#x3C;maxItems>&#x26;nextPagingToken=&#x3C;nextPagingToken> \
<strong>    -H 'Authorization: Bearer &#x3C;ACCESS_TOKEN>'
</strong>    -H 'Content-Type: application/json' \
</code></pre>

### Response

```json
{
    "nextPagingToken": "{pagingToken | null}",
    "catalogs": [
        {
            "id": "{catalogId}",
            "name": "{catalogName}"
        }
    ]
}
```

## Error Handling

| Error              | Description                                      |
| ------------------ | ------------------------------------------------ |
| BadRequest (400)   | Invalid Price Group; Invalid cultureCode.        |
| Unauthorized (401) | The token is expired.                            |
| Forbidden (403)    | The token does not have access to this endpoint. |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Invalid Price Group.",
            "error": "BadRequest"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Cart Custom Properties

You can enable a mini cart view in the following requests to show changes to the cart immediately. Find out more in the [Views for Cart modifying operations](/readme/headless/reference/views-for-cart-modifying-operations)

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cartId` from [Cart](/readme/headless/reference/cart#create-a-cart)
* `cultureCode`, e.g. `en-US` (string)
* `priceGroupId` from the cart or from [Price Groups](/readme/headless/reference/price-groups)

## Add Custom Properties to Cart

### Parameters

* `key` the unique key of the property (string)
* `value` the value of the property (string)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/properties \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "priceGroupId": "<priceGroupId>",
        "key": "<key>",
        "value": "<value>"
    }'
```

### Response

<pre class="language-json"><code class="lang-json">{
<strong>    "miniCart": null,
</strong>    "success": true
}
</code></pre>

## Delete Custom Property from Cart

### Parameters

* `propertyId` from [#add-custom-properties-to-cart](#add-custom-properties-to-cart "mention") or [#get-custom-properties-for-cart](#get-custom-properties-for-cart "mention")

### Request

```bash
curl -D- -X DELETE <base_url>/api/v1/carts/<cartId>/properties/<propertyId> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "priceGroupId": "<priceGroupId>",
    }'
```

### Response

```json
{
    "miniCart": null,
    "success": true
}
```

## Get Custom Properties for Cart

### Request

```bash
curl -D- -X GET <base_url>/api/v1/carts/<cartId>/properties \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

### Response

```json
{
    "customProperties":
        [
            {
                "id" : "{id}",
                "key" : "{key}",
                "value" : "{value}",
            }
        ]
}
```

## Update Custom Properties for Cart

### Parameters

* `propertyId` from [#add-custom-properties-to-cart](#add-custom-properties-to-cart "mention") or [#get-custom-properties-for-cart](#get-custom-properties-for-cart "mention")
* `value` new value for the property (string)

### Request

```bash
curl -D- -X PATCH <base_url>/api/v1/carts/<cartId>/properties/<propertyId> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "priceGroupId": "<priceGroupId>",
        "value": "<value>"
    }'
```

### Response

```json
{
    "miniCart": null,
    "success": true
}
```

## Error Handling

| Error              | Description                                                                             |
| ------------------ | --------------------------------------------------------------------------------------- |
| BadRequest (400)   | Execution of the pipeline fails; Price group is not found; AccessToken is not attached. |
| Unauthorized (401) | The token is expired.                                                                   |
| Forbidden (403)    | The token does not have access to this endpoint.                                        |
| NotFound (404)     | Cart or Property not found.                                                             |
| Conflict (409)     | Property already exists on the order.                                                   |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Property not found.",
            "error": "NotFound"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Line Item Custom Properties

You can enable a mini cart view in the following requests to show changes to the cart immediately. Find out more in the [Views for Cart modifying operations](/readme/headless/reference/views-for-cart-modifying-operations)

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `cartId` from [Cart](/readme/headless/reference/cart#create-a-cart)

## Add Custom Properties to Line Item

### Parameters

* `lineId` (string)
* `key` the unique key of the property (string)
* `value` the value of the property (string)

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/<lineId>/properties \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "priceGroupId": "<priceGroupId>",
        "key": "<key>",
        "value": "<value>"
    }'
```

### Response

```json
{
    "miniCart": null,
    "success": true
}
```

## Delete Custom Property from Line Item

### Parameters

* `propertyId` from [#add-custom-properties-to-cart](#add-custom-properties-to-cart "mention") or [#get-custom-properties-for-cart](#get-custom-properties-for-cart "mention")
* `lineId` (string)

### Request

```bash
curl -D- -X DELETE <base_url>/api/v1/carts/<cartId>/lines/<lineId>/properties/<propertyId> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "priceGroupId": "<priceGroupId>",
    }'
```

### Response

<pre class="language-json"><code class="lang-json">{
    "miniCart": null,
<strong>    "success": true
</strong>}
</code></pre>

## Get Custom Properties for Line Item

### Parameters

* `lineId` (string)

### Request

```bash
curl -D- -X GET <base_url>/api/v1/carts/<cartId>/lines/<lineId>/properties \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

### Response

```json
{
    "customProperties":
        [
            {
                "id" : "{id}",
                "key" : "{key}",
                "value" : "{value}",
            }
        ]
}
```

## Update Custom Properties for Line Item

### Parameters

* `lineId` (string)
* `propertyId` from [#add-custom-properties-to-cart](#add-custom-properties-to-cart "mention") or [#get-custom-properties-for-cart](#get-custom-properties-for-cart "mention")
* `value` new value for the property (string)

### Request

```bash
curl -D- -X PATCH <base_url>/api/v1/carts/<cartId>/lines/<lineId>/properties/<propertyId> \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        "cultureCode": "<cultureCode>",
        "priceGroupId": "<priceGroupId>",
        "value": "<value>"
    }'
```

### Response

```json
{
    "miniCart": null,
    "success": true
}
```

## Error Handling

| Error              | Description                                                    |
| ------------------ | -------------------------------------------------------------- |
| BadRequest (400)   | Execution of the pipeline fails; AccessToken was not attached. |
| Unauthorized (401) | The token is expired.                                          |
| Forbidden (403)    | The token does not have access to this endpoint.               |
| NotFound (404)     | Cart or Property not found.                                    |
| Conflict (409)     | Property already exists on the order.                          |

Error Response Example

```json
{
"errors":
    [
        {
            "error-description": "Property not found.",
            "error": "NotFound"
        }
    ]
}
```

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Orders

## Prerequisites

* `access_token` from [Headless API Authentication](/readme/headless/headless-api-authentication)
* `orderId` or `paymentGuid`

## Get Order by Order ID

### Request

<pre class="language-bash"><code class="lang-bash"><strong>curl -D- -X GET &#x3C;base_url>/api/v1/orders/&#x3C;orderId> \
</strong>    -H 'Authorization: Bearer &#x3C;ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
</code></pre>

### Response

```json
{
  "id": "{id}",
  "orderNumber": "{orderNumber}",
  "orderStatus": {
    "id": "{id}",
    "name": "{orderStatusName}"
  },
  "createdDate": "{createdDate}",
  "completedDate": "{completedDate}",
  "cultureCode": "{cultureCode}",
  "discount": "{discountAmount | null}",
  "discountTotal": "{discountTotalAmount | null}",
  "note": "{note | null}",
  "orderTotal": "{orderTotalAmount}",
  "paymentTotal": "{paymentTotalAmount}",
  "shippingTotal": "{shippingTotalAmount}",
  "subTotal": "{subTotalAmount}",
  "tax": "{taxAmount}",
  "vat": "{vatAmount}",
  "store": {
    "id": "{storeId}",
    "name": "{storeName}",
    "description": "{storeDescription}"
  },
  "billingAddress": {
    "addressName": "{addressName}",
    "attention": "{attention}",
    "city": "{city}",
    "companyName": "{companyName}",
    "country": {
      "id": "{countryId}",
      "name": "{countryName}"
    },
    "emailAddress": "{emailAddress}",
    "firstName": "{firstName}",
    "id": "{id}",
    "lastName": "{lastName}",
    "line1": "{line1}",
    "line2": "{line2}",
    "mobilePhoneNumber": "{mobilePhoneNumber}",
    "phoneNumber": "{phoneNumber}",
    "postalCode": "{postalCode}",
    "state": "{state}"
  },
  "billingCurrency": {
    "id": "{currencyId}",
    "isoCode": "{isoCode}"
  },
  "customer": {
    "id": "{id}",
    "firstName": "{firstName}",
    "lastName": "{lastName}",
    "emailAddress": "{emailAddress}",
    "phoneNumber": "{phoneNumber}"
  },
  "customProperties": [],
  "orderProperties": [],
  "discounts": [],
  "orderLines": [
    {
      "id": "{id}",
      "sku": "{sku}",
      "productName": "{productName}",
      "price": "{priceAmount}",
      "quantity": "{quantityNumber}",
      "discount": "{discountAmount}",
      "tax": "{taxAmount}",
      "total": "{totalAmount}",
      "taxRate": "{taxRate}",
      "variantSku": "{variantSku | null}",
      "unitDiscount": "{unitDiscount | null}",
      "discounts": [],
      "orderLineProperties": []
    }
  ],
  "payments": [
    {
      "id": "{id}",
      "amount": "{amount}",
      "fee": "{fee}",
      "feePercentage": "{feePercentage}",
      "feeTotal": "{feeTotal}",
      "grossAmount": "{grossAmount | null}",
      "transactionId": "{transactionId}",
      "tax": "{tax | null}",
      "taxRate": "{taxRate | null}",
      "paymentMethod": {
        "id": "{id}",
        "name": "{paymentMethodName}",
        "description": "{description}",
        "displayName": "{displayName}",
        "feePercent": "{feePercent}",
        "fees": [
          {
            "amount": "{amount}",
            "priceGroupId": "{priceGroupId}"
          }
        ],
        "imageId": "{imageId | null}",
        "imageUrl": "{imageUrl | null}",
        "paymentMethodProperties": []
      }
    }
  ],
  "shipments": [
    {
      "id": "{id}",
      "shipmentDiscount": "{shipmentDiscount | null}",
      "shipmentName": "{shipmentName}",
      "shipmentPrice": "{shipmentPrice}",
      "shipmentPriceTotal": "{shipmentPriceTotal}",
      "tax": "{taxAmount}",
      "taxRate": "{taxRate}",
      "vat": "{vatAmount}",
      "vatRate": "{vatRate}",
      "trackAndTrace": "{trackAndTrace | null}",
      "deliveryNote": "{deliveryNote}",
      "shipmentAddress": {
        "addressName": "{addressName}",
        "attention": "{attention}",
        "city": "{city}",
        "companyName": "{companyName}",
        "country": {
          "id": "{countryId}",
          "name": "{countryName}"
        },
        "emailAddress": "{emailAddress}",
        "firstName": "{firstName}",
        "id": "{id}",
        "lastName": "{lastName}",
        "line1": "{line1}",
        "line2": "{line2}",
        "mobilePhoneNumber": "{mobilePhoneNumber}",
        "phoneNumber": "{phoneNumber}",
        "postalCode": "{postalCode}",
        "state": "{state}"
      },
      "shippingMethod": {
        "id": "{id}",
        "name": "{name}",
        "defaultPaymentMethod": "{defaultPaymentMethod | null}",
        "eligiblePaymentMethods": []
      },
      "orderLines": [
        {
          "id": "{id}",
          "sku": "{sku}",
          "productName": "{productName}",
          "price": "{priceAmount}",
          "quantity": "{quantityNumber}",
          "discount": "{discountAmount}",
          "tax": "{taxAmount}",
          "total": "{totalAmount}",
          "taxRate": "{taxRate}",
          "variantSku": "{variantSku | null}",
          "unitDiscount": "{unitDiscount | null}",
          "discounts": [],
          "orderLineProperties": []
        }
      ]
    }
  ],
  "additionalAddresses": "{additionalAddresses | null}"
}
```

## Error Handling

| Error              | Description                                                    |
| ------------------ | -------------------------------------------------------------- |
| BadRequest (400)   | Execution of the pipeline fails; AccessToken was not attached. |
| Unauthorized (401) | The token is expired.                                          |
| Forbidden (403)    | The token does not have access to this endpoint.               |
| NotFound (404)     | Order not found.                                               |

Error Response Example

```json
{
"errors": [
        {
            "error-description": "Order with the supplied id orderId was not found",
            "error": "NotFound"
        }
    ]
}
```

## Get Order by Payment Guid

### Request

```bash
// Some code
curl -D- -X GET <base_url>/api/v1/payments/<paymentGuid>/order \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

### Response

```json
{
    "id": "{id}",
    "orderNumber": "{orderNumber}",
    "orderStatus": {
        "id": "{id}",
        "allowOrderEdit": "{true | false}",
        "allowUpdate": "{true | false}",
        "alwaysAvailable": "{true | false}",
        "externalId": "{externalId | null}",
        "name": "{orderName}",
        "nextOrderStatus": {
            "id": "{id}",
            "allowOrderEdit": "{true | false}",
            "allowUpdate": "{true | false}",
            "alwaysAvailable": "{true | false}",
            "externalId": "{externalId | null}",
            "name": "{orderName}",
            "nextOrderStatus": {
                "id": "{id}",
                "allowOrderEdit": "{true | false}",
                "allowUpdate": "{true | false}",
                "alwaysAvailable": "{true | false}",
                "externalId": "{externalId | null}",
                "name": "{orderName}",
                "nextOrderStatus": {
                    "id": "{id}",
                    "allowOrderEdit": "{true | false}",
                    "allowUpdate": "{true | false}",
                    "alwaysAvailable": "{true | false}",
                    "externalId": "{externalId | null}",
                    "name": "{orderName}",
                    "nextOrderStatus": "{nextOrderStatus | null}",
                    "pipeline": "{pipeline | null}",
                    "renderInMenu": "{true | false}",
                    "renderChildren": "{true | false}",
                    "sort": "{sortNumber}"
                },
                "pipeline": "{pipeline | null}",
                "renderInMenu": "{true | false}",
                "renderChildren": "{true | false}",
                "sort": "{sortNumber}"
            },
            "pipeline": "{pipeline | null}",
            "renderInMenu": "{true | false}",
            "renderChildren": "{true | false}",
            "sort": "{sortNumber}"
        },
        "pipeline": "{pipeline | null}",
        "renderInMenu": "{true | false}",
        "renderChildren": "{true | false}",
        "sort": "{sortNumber}"
    },
    "createdDate": "{createdDate}",
    "completedDate": "{completedDate}",
    "cultureCode": "{cultureCode}",
    "discount": "{discountAmount}",
    "discountTotal": "{discountTotalAmount}",
    "note": "{note | null}",
    "orderTotal": "{orderTotalAmount}",
    "paymentTotal": "{paymentTotalAmount}",
    "shippingTotal": "{shippingTotalAmount}",
    "subTotal": "{subTotalAmount}",
    "tax": "{taxAmount}",
    "billingAddress": {
        "addressName": "{addressName}",
        "attention": "{attention}",
        "city": "{city}",
        "companyName": "{companyName}",
        "country": {
            "cultureCode": "{cultureCode}",
            "id": "{countryId}",
            "name": "{countryName}"
        },
        "emailAddress": "{email}",
        "firstName": "{firstName}",
        "id": "{id}",
        "lastName": "{lastName}",
        "line1": "{addressLine1}",
        "line2": "{addressLine2}",
        "mobilePhoneNumber": "{mobilePhoneNumber}",
        "phoneNumber": "{phoneNumber}",
        "postalCode": "{postalCode}",
        "state": "{state}"
    },
    "billingCurrency": {
        "id": "{id}",
        "isoCode": "{isoCode}",
        "exchangeRate": "{exchangeRate}"
    },
    "customer": {
        "id": "{id}",
        "firstName": "{firstName}",
        "lastName": "{lastName}",
        "emailAddress": "{emailAddress}",
        "phoneNumber": "{phoneNumber}"
    },
    "store": {
        "id": "{id}",
        "name": "{name}",
        "description": "{description}"
    },
    "discounts": [],
    "orderLines": [
        {
            "id": "{id}",
            "sku": "{sku}",
            "productName": "{productName}",
            "price": "{priceAmount}",
            "quantity": "{quantityNumber}",
            "discount": "{discountAmount}",
            "tax": "{taxAmount}",
            "total": "{totalAmount}",
            "taxRate": "{taxRate}",
            "variantSku": "{variantSku}",
            "unitDiscount": "{unitDiscount}",
            "discounts": "{[discount1, discount2]}"
        }
    ],
    "shipments": [
        {
            "id": "{id}",
            "shipmentDiscount": "{shipmentDiscount}",
            "shipmentName": "{shipmentName}",
            "shipmentPrice": "{shipmentPrice}",
            "tax": "{tax}",
            "shipmentPriceTotal": "{shipmentPriceTotal}",
            "taxRate": "{taxRate}",
            "trackAndTrace": "{trackAndTrace}",
            "deliveryNote": "deliveryNote",
            "shipmentAddress": {
                "addressName": "{addressName}",
                "attention": "{attention}",
                "city": "{city}",
                "companyName": "{companyName}",
                "country": {
                    "cultureCode": "{cultureCode}",
                    "id": "{id}",
                    "name": "{counteryName}"
                },
                "emailAddress": "{email}",
                "firstName": "{firstName}",
                "id": "{id}",
                "lastName": "{lastName}",
                "line1": "{line1}",
                "line2": "{line2}",
                "mobilePhoneNumber": "{mobilePhoneNumber}",
                "phoneNumber": "{phoneNumber}",
                "postalCode": "{postalCode}",
                "state": "{state}"
            },
            "shippingMethod": {
                "id": "{id}",
                "name": "{name}",
                "defaultPaymentMethod": "{defaultPaymentMethod}",
                "eligiblePaymentMethods": [
                    {
                        "displayName": "{displayName}",
                        "feePercent": "{feePercent}",
                        "fees": [
                            {
                                "amount": "{amount}",
                                "priceGroupId": "{priceGroupId}"
                            }
                        ],
                        "id": "{id}",
                        "imageUrl": "{imageUrl}",
                        "name": "{paymentName}"
                    }
                ]
            },
            "orderLines": [
                {
                    "id": "{id}",
                    "sku": "{sku}",
                    "productName": "{productName}",
                    "price": "{price}",
                    "quantity": "{quantity}",
                    "discount": "{discount}",
                    "tax": "{tax}",
                    "total": "{total}",
                    "taxRate": "{taxRate}",
                    "variantSku": "{variantSku}",
                    "unitDiscount": "{unitDiscount}",
                    "discounts": "{[discount1, discount2]}"
                }
            ]
        }
    ],
    "orderAddresses": [
        {
            "addressName": "{addressName}",
            "attention": "{attention}",
            "city": "{city}",
            "companyName": "{companyName}",
            "country": {
                "cultureCode": "{cultureCode}",
                "id": "{id}",
                "name": "{counteryName}"
            },
            "emailAddress": "{email}",
            "firstName": "{firstName}",
            "id": "{id}",
            "lastName": "{lastName}",
            "line1": "{line1}",
            "line2": "{line2}",
            "mobilePhoneNumber": "{mobilePhoneNumber}",
            "phoneNumber": "{phoneNumber}",
            "postalCode": "{postalCode}",
            "state": "{state}"
        },
        {
            "addressName": "{addressName}",
            "attention": "{attention}",
            "city": "{city}",
            "companyName": "{companyName}",
            "country": {
                "cultureCode": "{cultureCode}",
                "id": "{id}",
                "name": "{countryId}"
            },
            "emailAddress": "{email}",
            "firstName": "{firstName}",
            "id": "id",
            "lastName": "{lastName}",
            "line1": "{line1}",
            "line2": "{line2}",
            "mobilePhoneNumber": "{mobilePhoneNumber}",
            "phoneNumber": "{phoneNumber}",
            "postalCode": "{postalCode}",
            "state": "{state}"
        }
    ]
}
```

### Error Handling

| Error              | Description                                                      |
| ------------------ | ---------------------------------------------------------------- |
| BadRequest (400)   | Execution of the pipeline fails or AccessToken was not attached. |
| Unauthorized (401) | The token is expired.                                            |
| Forbidden (403)    | The token does not have access to this endpoint.                 |
| NotFound (404)     | Order or Payment not found.                                      |

## Related Articles

{% content-ref url="/pages/OKRcu1Dp3gn5Y38w55HD" %}
[Error Handling](/readme/headless/error-handling)
{% endcontent-ref %}


# Views for Cart modifying operations

Endpoints that modify a Cart ( e.g. [Cart / Order Line Items](/readme/headless/reference/cart-order-line-items#add-line-item-to-a-cart)) can return one of the available views representing the updated Cart. By using views, you will not need to fetch the updated Cart in a subsequent API call.

The views can be requested by adding a `views` query parameter to your request with at least one of the following views:

## Views

<table><thead><tr><th width="192">View</th><th>Description</th></tr></thead><tbody><tr><td>miniCart</td><td>Returns a subset of Cart data.</td></tr></tbody></table>

## Examples

### Request

```bash
curl -D- -X POST <base_url>/api/v1/carts/<cartId>/lines?views=miniCart \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
    -d '{
        ...<the parameters for the request>...
    }'
```

### MiniCart Response

```json
{
    "miniCart": {
        "customProperties": [
            {
                "id": "{id}",
                "key": "{propKey}",
                "value": "{propValue}"
            }
        ],
        "createdDate": "{createdDate}",
        "cultureCode": "{cultureCode}",
        "discount": "{discountAmount}",
        "discountTotal": "{discountTotalAmount}",
        "id": "{id}",
        "note": "{note}",
        "orderLines": [
            {
                "discount": "{discountAmount}",
                "discounts": "{[discount1Amount, discount2Amount]}",
                "id": "{id}",
                "price": "{priceAmount}",
                "priceGroupId": "{priceGroupId}",
                "productCatalogId": "{productCatalogId}",
                "productName": "{productName}",
                "quantity": "{quantityNumber}",
                "sku": "{sku}",
                "total": "{totalAmount}",
                "unitDiscount": "{unitDiscountAmount}",
                "variantSku": "{variantSku}",
                "tax": "{taxAmount}",
                "taxRate": "{taxRateAmount}",
                "customProperties": [
                    {
                        "id": "{id}",
                        "key": "{propKey}",
                        "value": "{propValue}"
                    }
                ]
            }
        ],
        "orderTotal": "{orderTotalAmount}",
        "paymentTotal": "{paymentTotalAmount}",
        "shippingTotal": "{shippingTotalAmount}",
        "subTotal": "{subTotalAmount}",
        "tax": "{taxAmount}"
    }
}
```


# Custom Headless APIs

Custom Headless APIs have two primary use cases:

* For a fully headless solution, you will need custom APIs to serve catalogs, categories, and products to your client application.
* For any special requirements that are not covered by the out-of-the-box APIs.

The goal is to create endpoints that give you freedom of implementation while respecting our authentication.

## Prerequisites

* Out-of-the-box Headless API available (for example, by using our [Standalone Template](/readme/getting-started/standalone) as a starting point).
* Secrets are set up for your stores, and the URI whitelist is correctly configured in the API Access part of the administration interface. You can [learn about it here](/readme/headless).

## Creating the API Controller

* In your project, create a new Class and Inherit from `Ucommerce.Web.WebSite.Controllers.HeadlessControllerBase`

This will inherit our authentication, restricting its usage to only authenticated clients.

* Add a route to your controller. It can match the out-of-the-box route as follows:

```csharp
[Route("api/v1.0/products")]
```

* In your controller's constructor, you can inject any components you may need in your implementation. For example,

```csharp
private readonly IIndex<ProductSearchModel> _productIndex;

public ControllerName(IIndex<ProductSearchModel> productIndex)
{
    _productIndex = productIndex;
}
```

## Creating the method

* Create and annotate a new method with the HTTP method type and route.

```csharp
[HttpGet]
[Route("")]
```

* For help with return types and choosing the right implementation details, [visit Microsoft's documentation.](https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types)
* Headless API authentication happens on a per-store basis. You can resolve the StoreId from the claim as follows:

```csharp
var storeGuid = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)
```

* Complete your implementation and return the results to your client.

## Complete example

For reference, here is a full controller example that will return products for a given Category:

```csharp
using System.Globalization;
using System.Security.Authentication;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Ucommerce.Extensions.Search.Abstractions;
using Ucommerce.Extensions.Search.Abstractions.Models.IndexModels;
using Ucommerce.Extensions.Search.Abstractions.Models.SearchModels;
using Ucommerce.Web.WebSite.Controllers;

namespace project.CustomHeadlessControllers;

[Route("api/v1.0/products")]
public class HeadlessProductController : HeadlessControllerBase
{
    private readonly IIndex<ProductSearchModel> _productIndex;

    public HeadlessProductController(IIndex<ProductSearchModel> productIndex)
    {
        _productIndex = productIndex;
    }

    [HttpGet("")]
    public async Task<ActionResult<YourResponseModel>> GetProducts(
        [FromQuery] Guid categoryId,
        [FromQuery] string cultureCode,
        CancellationToken token)
    {
        var storeGuid =
            Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new AuthenticationException());
        var culture = new CultureInfo(cultureCode);
        var products = _productIndex.AsSearchable(culture).Where(x => x.CategoryIds.Contains(categoryId));

        // 
        var productsResponse = new YourResponseModel()
        {
            Products = YourMapProducts(products)
        };

        return productsResponse;
    }

    // YourMapProducts implementation
}
```

## Related Articles

{% content-ref url="/pages/woVuLixbEGepQtqGIkwi" %}
[Headless](/readme/headless)
{% endcontent-ref %}


# Error Handling

All successful requests to the API return an HTTP 200 or 201. In case of errors, the API returns a JSON response containing the error code and details about the error:

```json
{
  "errors": [
    {
      "error-description": "Price Group cannot be null or empty",
      "error": "Price group must contain an id"
    }
  ]
}
```

The format of the error description varies between error codes.

The following combinations of error codes are returned from the API:

<table><thead><tr><th width="112">HTTP</th><th>Description</th><th>Handling</th></tr></thead><tbody><tr><td>400</td><td>Invalid input provided. See details.</td><td>Check that the input sent is correct.</td></tr><tr><td>401</td><td>Incorrect Client ID and/or Client Secret in HTTP header or Access Token invalid.</td><td></td></tr><tr><td>403</td><td>The authentication token does not have access to this endpoint.</td><td></td></tr><tr><td>404</td><td>A resource with the supplied ID was not found in the current store.</td><td>Try with an existing resource.</td></tr><tr><td>500</td><td>Unknown error.</td><td>Not retryable.</td></tr><tr><td>504</td><td>Gateway time out.</td><td>Call can be retried.</td></tr></tbody></table>


# Pagination

`HTTPGET` endpoints have:

* `maxItems` optional parameter to act as a limit for how many results should be returned.
  * The default value is 250
* `nextPagingToken` in the response that can be used in subsequent requests to get the next results page.
  * As long as `nextPagingToken` differs from null, there are more results available.
  * When null, the last page of results has been reached.
  * To request a previous page, the client should keep track of previously requested paging tokens.

## Example

### Request

```bash
curl -D- -X GET <baseUrl>/api/v1/countries?maxItems=3 \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

### Response

```json
{
    "countries": [
        {
            "cultureCode": "da-DK",
            "id": "0c53f836-9601-ec11-837b-64bc58542c92",
            "name": "Denmark"
        },
        {
            "cultureCode": "de-DE",
            "id": "1153f836-9601-ec11-837b-64bc58542c92",
            "name": "Germany"
        },
        {
            "cultureCode": "en-GB",
            "id": "0f53f836-9601-ec11-837b-64bc58542c92",
            "name": "Great Britain"
        }
    ],
    "nextPagingToken": "M3wzfGRjMDZlZGYxLTQ2MGQtNGVlZC1hMzMxLWJjNWYwMzI4NDlkNHxGYWxzZQ=="
}
```

## Next Page

```bash
curl -D- -X GET <baseUrl>/api/v1/countries?maxItems=3&nextPagingToken=M3wzfGRjMDZlZGYxLTQ2MGQtNGVlZC1hMzMxLWJjNWYwMzI4NDlkNHxGYWxzZQ== \
    -H 'Authorization: Bearer <ACCESS_TOKEN>'
    -H 'Content-Type: application/json' \
```

## Error Handling

| Error            | Description                                    |
| ---------------- | ---------------------------------------------- |
| BadRequest (400) | Paging token does not match the current query; |


# Deprecation

Deprecated endpoints will be marked with the HTTP header called `X-DEPRECATION-NOTICE`.

**Example:**

```json
"X-DEPRECATION-NOTICE": "PagingToken is deprecated. use NextPagingToken instead."
```


# Backoffice Authentication

Authentication gives you control over who has access to your backoffice.

By default, Ucommerce automatically authenticates as a test administrator account.\
This is very useful when developing locally, so you do not have to set up authentication from the get-go. When going to production this should be changed, however, so the back office is protected from unverified use, and handle individual user permissions.\
Ucommerce leverages the [built-in ASP.NET Core authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-7.0) to restrict access to the back office API. This means that you will have to implement an ASP.NET authorization system yourself.

It is recommended that you integrate a well-known identity provider. We have examples of how it can be done using [Microsoft Entra ID](/readme/backoffice-authentication/microsoft-entra-id-example) and [Auth0](/readme/backoffice-authentication/auth0-authentication-example).

{% hint style="info" %}
We suggest that you implement an authorization system using [OpenID Connect](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/secure-net-microservices-web-applications/#authenticate-with-an-openid-connect-or-oauth-20-identity-provider) since this will enable you to use single sign-on (SSO), allowing you to share logins between Ucommerce and other OAuth-enabled applications like a CMS.
{% endhint %}

## Add an external identity provider

Ucommerce takes care of the local login session, so you need to set up a scheme that will take care of logging in the user. Additionally, Ucommerce needs an implementation of the [`IExternalClaimsMapper`](#external-claims-mapper) interface.

Use the options hook when calling `AddBackOffice()` in `program.cs`, to configure your scheme:

```csharp
var ucommerceBuilder = builder.Services.AddUcommerce(builder.Configuration)
    .AddBackOffice(securitySettings =>
        {
            // This method can be called multiple times
            securitySettings.AddExternalIdentityProvider<MyExternalClaimsMapper>(
                "MyExternalScheme",
                authenticationBuilder =>
                {
                ... // Use the AuthenticationBuilder to add your scheme
                });
            // Configure Ucommerce to use your scheme from code
            securitySettings.UseExternalIdentityProvider("MyExternalScheme");
        }
    )
    ...
```

{% hint style="warning" %}
Since Ucommerce takes care of the session cookie, you should not set cookies in your external authentication scheme.
{% endhint %}

{% hint style="info" %}
You can use the [ASP.NET configuration](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0) to override the external identity provider that Ucommerce will forward to. You can define the scheme by setting the value for `Ucommerce:BackOffice:ExternalIdentityProviderScheme` to the name of one of the registered schemes. This allows you to change the provider depending on the environment. To use the built-in scheme set the value to `ucommerce-test-user`.
{% endhint %}

## External Claims Mapper

Ucommerce automatically creates and updates a local user when a user logs in via an external identity provider, so it needs a mapping of the external claims, to the local user in Ucommerce.\
To create a mapping you implement the `IExternalClaimsMapper` interface.

The interface consists of a single method `MapClaims` that must take care of the mapping from the incoming claim to an `AuthUser` object.

```csharp
using Ucommerce.Web.BackOffice.Authentication;
...

public class MyExternalClaimsMapper : IExternalClaimsMapper
{
    public Task<AuthUser> MapClaims(ClaimsPrincipal principal)
    {
        var externalId = // A claim value with a unique identifier for the user
        //e.g.
        //var externalId = principal.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var name = // A claim value containing the name of the user
        var isAdmin = // A claim value indicating if the user is to be an admin
        var user = new AuthUser(externalId, name)
        {
            IsAdmin = isAdmin
        };
        return Task.FromResult(user);
    }
}
```

### AuthUser Properties

| Property   | Description                                                                      |
| ---------- | -------------------------------------------------------------------------------- |
| ExternalId | The identifier from the external provider used to identify the user in Ucommerce |
| Name       | The name of the user                                                             |
| IsAdmin    | Indicate if the user should have administrator rights                            |

## The default `ExternalClaimsMapper`

Ucommerce comes with a default claims mapper (`Ucommerce.Web.BackOffice.Authentication.ExternalClaimsMapper`) that maps claims in the following way. The keys are also defined in the `UcommerceClaimTypes` constants class.

| Property   | UcommerceClaimTypes Key | Value                                                                                                                                   |
| ---------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| ExternalId | EXTERNAL\_IDENTIFIER    | [ClaimTypes.NameIdentifier](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes.nameidentifier?view=net-8.0) |
| Name       | NAME                    | [ClaimTypes.Name](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes.name?view=net-8.0)                     |
| IsAdmin    | IS\_ADMIN               | "IsAdmin"                                                                                                                               |

## Related Articles

{% embed url="<https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-7.0>" %}

{% embed url="<https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc>" %}

{% embed url="<https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-7.0>" %}

{% content-ref url="/pages/afGlzr37PZ2anwL1slrF" %}
[Microsoft Entra ID Example](/readme/backoffice-authentication/microsoft-entra-id-example)
{% endcontent-ref %}

{% content-ref url="/pages/jSskUNtNxL6cZJ7QXy5j" %}
[Auth0 Authentication Example](/readme/backoffice-authentication/auth0-authentication-example)
{% endcontent-ref %}


# Microsoft Entra ID Example

This is a quick guide on setting up Backoffice authentication with Microsoft Entra ID.

## Prerequisites

In this example, you will need the following NuGet packages

```
Microsoft.Identity.Web
Microsoft.Identity.Web.TokenCache
```

## Setting up Ucommerce Backoffice

Setting up Ucommerce backoffice with Microsoft Entra ID requires you to set up an external authentication scheme when calling `.AddBackOffice()`

To set up the authentication, use a helper method from the above packages to add all the required services to the [AuthenticationBuilder](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder?view=aspnetcore-8.0). We highly recommend looking into some of these methods, as they do much of the groundwork to set up OpenID.

```csharp
var ucommerceBuilder = builder.Services
    .AddUcommerce(builder.Configuration)
    .AddBackOffice(securitySettings =>
        {
            securitySettings.AddExternalIdentityProvider<MyExternalClaimsMapper>(
                OpenIdConnectDefaults.AuthenticationScheme,
                authenticationBuilder =>
                {
                    IEnumerable<string>? initialScopes = builder.Configuration["DownstreamApi:Scopes"]
                        ?.Split(' ');
                    // Use the AuthenticationBuilder from ASP.NET to set up authentication
                    authenticationBuilder.AddMicrosoftIdentityWebApp(builder.Configuration,
                            cookieScheme: null) // Ucommerce will handle the cookie session
                        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                        .AddInMemoryTokenCaches();
                });
            // Configure Ucommerce to use your scheme from code
            securitySettings.UseExternalIdentityProvider(OpenIdConnectDefaults.AuthenticationScheme);
        }
    )
    ...
```

{% hint style="info" %}
Remember to create your[ external claims mapper](/readme/backoffice-authentication#external-claims-mapper) to map the claims from Azure to Ucommerce.
{% endhint %}

## Create your Azure application

Follow [this guide](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-web-app-aspnet-core-sign-in) from Microsoft to set up your Azure App and `appsettings.json`. After following the guide, `appsettings.json` should look something like this

```json
 ...
 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "{Your-Tenant-Id}",
    "ClientId": "{Your-Client-Id}",
    "ClientCertificates": [
      {
        "SourceType": "StoreWithThumbprint",
        "CertificateStorePath": "CurrentUser/My",
        "CertificateThumbprint": "{Your-Certificate-Thumbprint}"
      }
    ],
    "CallbackPath": "/signin-oidc"
  },
  "DownstreamApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0/me",
    "Scopes": "user.read"
  },
...
```

{% hint style="warning" %}
Notice that the sources in `ClientCertificates` may change between environments.\
See the [Using Certificates](https://github.com/AzureAD/microsoft-identity-web/wiki/Using-certificates) documentation for details.
{% endhint %}

## Related Articles

{% embed url="<https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-web-app-aspnet-core-sign-in>" %}


# Auth0 Authentication Example

This is a quick guide on setting up Backoffice authentication with Auth0.

## Prerequisites

In this example, you will need the following NuGet package

```
Auth0.AspNetCore.Authentication
```

## Setting up Ucommerce Backoffice

Setting up Ucommerce backoffice with Auth0 requires you to set up an external authentication scheme and [external claims mapper](/readme/backoffice-authentication#external-claims-mapper) when calling `.AddBackOffice()`

To set up the authentication, use a helper method from the Auth0 package to add all the required services via the [AuthenticationBuilder](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder?view=aspnetcore-8.0). We highly recommend looking into some of these methods, as they do much of the groundwork to set up OpenID.

```csharp
var ucommerceBuilder = builder.Services
    .AddUcommerce(builder.Configuration)
    .AddBackOffice(securitySettings =>
        {
            securitySettings.AddExternalIdentityProvider<MyExternalClaimsMapper>(
                Auth0Constants.AuthenticationScheme,
                authenticationBuilder =>
                {
                    // Use the AuthenticationBuilder from ASP.NET to set up authentication
                    authenticationBuilder
                        .AddAuth0WebAppAuthentication(Auth0Constants.AuthenticationScheme,
                            auth0Options =>
                            {
                                auth0Options.Domain = builder.Configuration["Auth0:Domain"]!;
                                auth0Options.ClientId = builder.Configuration["Auth0:ClientId"]!;
                                // Ucommerce will handle the cookie session, so we disable it for Auth0
                                auth0Options.SkipCookieMiddleware = true;
                            });
                });
            // Configure Ucommerce to use your scheme from code
            securitySettings.UseExternalIdentityProvider(Auth0Constants.AuthenticationScheme);
        }
    )
    ...
```

{% hint style="info" %}
Remember to create your[ external claims mapper](/readme/backoffice-authentication#external-claims-mapper) to map the claims from Auth0 to Ucommerce.
{% endhint %}

## Create your Auth0 application

Create your application in the [Auth0 Dashboard](https://manage.auth0.com/dashboard).

When the application is created, update `appsettings.json` to keep track of `Domain` and `ClientId`.

```json
  "Auth0": {
    "Domain": "{YourDomain}",
    "ClientId": "{YourClientID}"
  },
```

For further guidance on setting up Auth0, we recommend the [Auth0 Quickstart Guide](https://auth0.com/docs/quickstart/webapp/aspnet-core/interactive).

## Related Articles

{% embed url="<https://auth0.com/docs/quickstart/webapp/aspnet-core/interactive>" %}


# Definitions

Definitions are the foundation for flexible data in Ucommerce

A lot of the business objects in Ucommerce are based on definitions, which are also referred to as dynamic entities. This article will cover how to work with definitions to make your store more flexible.

## List of definition-based entities

The definition-based entities in Ucommerce are:

* Store
* Catalog
* Category
* Product
* Promotion
* Payment method
* Shipping method
* Order/Cart, as they share a definition
* Criterion
* Discount


# What is a Definition

The idea behind definitions is to enable saving additional information, tailored to your specific needs, for each entity.

Consider the following scenario: You sell shirts and shoes in the store. To do that you need information about sizes and colors on your products.

The solution is simple - Ucommerce supports definitions, to which new fields can be added to store the information you need. You might already be familiar with the concept of *Document Types* in Umbraco or *Templates* in Sitecore. A definition in Ucommerce is basically the same thing but for e-commerce.

## Definition Fields <a href="#definition-fields" id="definition-fields"></a>

Definitions have a list of fields, which are available to the store manager to maintain in the back office. For the S*hirt* definition, you can have two fields available: *Color* and *Size*. For the S*hoe* definition, you can have one field available: *Size.*

Each field has a number of configuration options, which influence the look and functionality in the back office UI. They will be covered in detail in the next sections.

### Multilingual Fields <a href="#multilingual-fields" id="multilingual-fields"></a>

If you need to maintain the data of the field in multiple languages, you can mark the field as being multilingual. This will allow you to define a value per language configured.

### Display on Site <a href="#display-on-website" id="display-on-website"></a>

A `true`/`false` value which is set on the field to indicate whether to display the field on the website. If set to `false`, the field will not be part of the search index.

### Render in Editor <a href="#render-in-editor" id="render-in-editor"></a>

Sometimes you need data you don’t want the back office user to edit. Setting this property to `false` will exclude the field from the back office UI, effectively making the field accessible via code only.

### Data Type <a href="#data-types" id="data-types"></a>

Each field has an associated data type, which determines the editor for the particular field, e.g. a checkbox, text box, date picker, etc. Ucommerce comes with a number of built-in data types such as `ShortText`, `LongText`, `Number`, `Boolean`, `Enum`, and `Image`.\
`Enum` is a special type that enables you to predefine a list of values for the back office user to select from, e.g. colors or sizes. Basically data with a finite number of options to choose from.

#### Validation Expression <a href="#validation-expressions" id="validation-expressions"></a>

A data type can have a validation expression associated with it to validate user input when editing in the back office. A validation expression is a standard regular expression evaluated whenever a field of the given type is saved.

### Converting (string) field values to numbers

Given the flexible nature of the definition system, field values are always stored as strings. When a definition field is of type `Number` you can use the extension methods `ToDecimal()`, `ToFloat()`, and `ToDouble()` built into Ucommerce, to make sure the value is converted using the same culture Ucommerce uses when storing the value as a string. The extension methods are found in the namespace `Ucommerce.Web.Common.Extensions` .

### Variant property (products only) <a href="#variant-property" id="variant-property"></a>

When creating a definition field on a product, you can decide whether it is a variant field, or not. This will change the definition field from appearing on the parent product, or on the product variant.

## Composable Definitions <a href="#inherited-definitions" id="inherited-definitions"></a>

A definition can inherit from other definitions, which enables you to build your definitions in a composable way. Add fields that are common across multiple definitions, in a single definition and reuse it as a parent for other definitions, e.g. SEO information is something you want to store on all products and categories regardless of their various definitions so the *shirts*, *shoes*, and *accessories* definitions all inherit from the *SEO* definition.

The result is less work when defining new fields as shared fields can be added once and "mixed" in with the relevant definitions.

Ucommerce supports inheritance from multiple parents, which means a single definition may inherit from multiple other definitions, e.g. the *Shoe* definition might inherit fields from both the *SEO* and *Footwear* definition.


# Search and indexing

Increase performance on your website by utilizing indexes to serve content.

Using SQL queries with joins to retrieve the data wanted for a search result can be expensive and cumbersome to customize. Instead, Ucommerce utilizes de-normalized data in a data store optimized for indexing, e.g. ElasticSearch, to deliver the search results fast. This increases performance on the website. It also makes it easy to customise what data you want to serve to your customers using custom [index definitions](/readme/search-and-indexing/indexing/index-definitions) and [adorners](/readme/search-and-indexing/indexing/custom-data) instead of SQL queries.

The following articles contain detailed information on how to access and use various features of Ucommerce's search API. They cover the following topics:

* [Indexing](/readme/search-and-indexing/indexing) - Get data into the index
* [Searching](/readme/search-and-indexing/searching) - Get data from the index


# Configuration

How to configure your Elasticsearch

## Main configuration

The main setup for your Elasticsearch will be done in the [configuration](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0) of your application, for example, in an appsettings.json file. Here, you will have to add a section with options for Elasticsearch that should look something like this:

```json
"Ucommerce": {
  "Search": {
    "ElasticClient": {
      "NodePoolType": "Single",
      "AuthenticationType": "None"
    }
  }
}
```

The `NodePoolType` property indicates the type of Elasticsearch setup your application should use and we currently support Single-node and Cloud setups.

With a cloud setup, you will need additional properties to connect to your Elasticsearch cloud instance, for example:

```json
"Ucommerce": {
  "Search": {
    "ElasticClient": {
      "NodePoolType": "Cloud",
      "AuthenticationType": "ApiKey",
      "CloudId": "yourIdHere",
      "ApiKey": "yourApiKey"
    }
  }
}
```

\
The `AuthenticationType` property indicates the type of authentication used for your setup, the currently supported options are `None`, `ApiKey`, and `BasicAuthentication`.

## Adding Elasticsearch to your application

To add Elasticsearch to your application, add the following code to your ucommerceBuilder:

```csharp
.AddSearch()
.UcommerceBuilder
.AddElasticsearch();
```

If you need to add extra configuration or just prefer to configure via code, the options overload will allow you to set the same parameters, as shown below:

```csharp
.AddSearch()
.UcommerceBuilder
.AddElasticsearch(options => options.UseInMemoryTransportClient = true);
```

The options are validated on application startup, so your application will let you know if you add invalid settings.

## Properties

Here's an overview of what properties are available and what they mean:

<table><thead><tr><th width="271">Property</th><th width="253">Value(s)</th><th>Usage</th></tr></thead><tbody><tr><td>NodePoolType</td><td>Single, Cloud</td><td>Determines which type of Elasticsearch setup you're using</td></tr><tr><td>AuthenticationType</td><td>None, BasicAuthentication, ApiKey</td><td>Determines what type of authentication you're using in your setup.</td></tr><tr><td>CloudId</td><td>CloudId from your Elasticsearch Cloud instance.</td><td>Connects the setup to your Elasticsearch Cloud instance.</td></tr><tr><td>ApiKey</td><td>ApiKey from your Elasticsearch instance.</td><td>Used for authentication against your Elasticsearch instance.</td></tr><tr><td>Uri</td><td>Uri for your Elasticsearch node, default is http://localhost:9200</td><td>Determines the location of your Elasticsearch cluster.</td></tr><tr><td>Username</td><td>Username for your Elasticsearch user.</td><td>Used for basic authentication.</td></tr><tr><td>Password</td><td>Password for your Elasticsearch user.</td><td>Used for basic authentication.</td></tr><tr><td>UseInMemoryTransportClient</td><td>true or false</td><td>Determines whether your application should use the in-memory transport client. Can be useful for testing, but is not recommended for production.</td></tr><tr><td>SerializerFactory</td><td>An object of type UcommerceSerializerFactory</td><td>Can only be set via the options overload in code, used for custom serializer logic.</td></tr><tr><td>WriteChunkSize</td><td>Any number greater than zero</td><td>This is the number of documents that will be written to Elasticsearch in one go.<br>Default is 1000.</td></tr><tr><td>WriteMaxParallelism</td><td>Any number greater than zero</td><td>The maximum number of parallel writes to Elasticsearch.<br>Default is 5.</td></tr></tbody></table>


# Indexing

Get data into your index

You will have to run a scratch index to get data into your index. There are two ways to do that - using the back office or calling the API directly, using, e.g., Postman.

{% hint style="info" %}
It is only necessary to index from scratch when importing data from external sources into Ucommerce or making a new [custom index definition](/readme/search-and-indexing/indexing/index-definitions#customizing-an-index-definition). When using the back office to change properties for products, etc., the back office makes sure to index the changes automatically.
{% endhint %}

{% hint style="info" %}
Only a single scratch index operation can run at any time. The API will return a *409 - Conflict* HTTP status code if a scratch index operation is already running. The back office will display the current status.
{% endhint %}

## Scratch index using the back office

Go to *Settings* -> *Search - rebuild index* -> Click *Rebuild index*

## Scratch index using the API

Send an empty `POST` request to the URL responsible for indexing:

```
https://YourStore:YourPort/ucommerce/api/v1/search
```


# Index Definitions

Index definitions specify the data type, content, and searchability of indexed data in Ucommerce.

## Default index definitions

Ucommerce ships with built-in index definitions for the following entities:

* Store (`DefaultStoresIndexDefinition`)
* Catalog (`DefaultCatalogsIndexDefinition`)
* Category (`DefaultCategoriesIndexDefinition`)
* Product (`DefaultProductsIndexDefinition`)
* PriceGroup (`DefaultPriceGroupsIndexDefinition`)
* ProductPrice (`DefaultPricesIndexDefinition`)

## Customizing an index definition

To have custom data, e.g., user-defined fields, in the index, you must create a custom index definition by creating a class that either inherits from `IIndexDefintion<SearchModelType>` or the default definition as in the example below.

```csharp
public class CustomProductIndexDefinition : DefaultProductsIndexDefinition
    {
        public CustomProductIndexDefinition()
        {
            this.Field(p => p["Color"], typeof(UserDefinedEnum));
            this.Field(p => p["Coupons"], typeof(UserDefinedEnum));
            this.Field(p => p["Downloadable"], typeof(bool));

            this.Field(p => p.ShortDescription)
                .Exclude();
            this.Field(p => p.PricesInclTax["EUR 15 pct"]);
        }
    }
```

{% hint style="info" %}
If you inherit from the default definition, you get all the standard fields in addition to the fields added in the custom index definition class. It's possible to exclude standard fields, as shown in the example above.\
\
If you want full control, inherit from the interface and explicitly add the fields you need.
{% endhint %}

{% hint style="info" %}
By default, the property `VariantProperties` is not populated when the search model is retrieved from the index. To access variant properties on the parent entity add the following to your custom index definition class:

```csharp
this.Field(p => p.VariantsProperties)
```

{% endhint %}

After defining a custom index, it must override the default in the service collection of your app.

```csharp
builder.Services.AddUnique<IIndexDefinition<ProductSearchModel>, CustomProductIndexDefinition>();
```

{% hint style="warning" %}
A rebuild of the indices is needed after changing the index definition.
{% endhint %}

### Enum fields

When adding a user-defined enum, e.g. `Color`, use the type `UserDefinedEnum` in the index definition as shown in the example above. This type has special handling in the index to make sure that the display name and value of the enum is handled correctly, e.g.:

```json
"Coupons": [
  {
    "DisplayName": "5 coupons",
    "Value": "5"
  },
  {
    "DisplayName": "10 coupons",
    "Value": "10"
  },
  {
    "DisplayName": "15 coupons",
    "Value": "15"
  }
],
"Downloadable": [
  "on"
]
```

Notice that the `Coupons` property has `DisplayName` and `Value` properties while `Downloadable` does not. This is because `Coupons` is defined as `UserDefinedEnum` while `Downloadable` is defined as `bool`.

See [translation of values](#translation-of-values) below for more details on how the multilingual display name is translated.

### Multi-value fields

When adding a multi-value field, e.g. `available colors`, the API can automatically split this correctly into a list of possible values as shown below.

```csharp
this.Field(p => p["availableColors"], typeof(IEnumerable<UserDefinedEnum>));
```

Example: *Red* and *Green* are the available colors for a product, then said product can be found by filtering on either *Red* or *Green* or both.

{% hint style="info" %}
Splitting multi-value fields is not limited to `UserDefinedEnum` . Other types, like strings, can also be used.
{% endhint %}

{% hint style="info" %}
To make sure that the system can automatically split the values, use `Ucommerce.Web.Infrastructure.Constants.DATA_SPLITTING_CHARACTER` to join the values.
{% endhint %}

## Translation of values

Since every index corresponds to a specific language, the system will automatically handle translation of multilingual user-defined fields. Therefore, if you have configured a user-defined field to be multilingual, the appropriate translation will be automatically available in each index. This makes the system highly flexible without requiring additional index configuration.


# Facets

Faceted search is the ability to narrow a search based on one or more dimensions.

## Overview

When using Ucommerce, you can create a custom index definition that specifies which built-in or user-defined properties on your products should be generated as facets. Once you have completed this step, any queries against the index will return facets and their respective values, as shown in the image below.

<figure><img src="/files/5GSXp40pjQTJyX1tp2ca" alt=""><figcaption><p>Faceted search example</p></figcaption></figure>

## Creating a facet

When you want to enable faceted search for a field, your index definition needs to define the field to be part of the index as well as tell the system that this field should be faceted.

```csharp
this.Field(p => p["Coupons"], typeof(UserDefinedEnum))
    .Facet();
```

{% hint style="info" %}
The search API will automatically retrieve all possible values for facets.
{% endhint %}

### Price facets

Depending on the price indexing mode, prices can be different than most other fields. See [Indexing Prices](/readme/search-and-indexing/indexing/indexing-prices) for more details.


# Indexing Prices

Since Ucommerce 10.6.0, prices can be indexed in one of two ways: As part of the product index, or in its own index. What mode to use depends entirely on the use case.

## Overview

Before Ucommerce 10.6.0, prices were always included as part of the product index - one price for each price group in Ucommerce:

```json
{
  "Name": "Support",
  "PrimaryImageUrl": "ImageNotFoundImageURL",
  "DisplayName": "Support",
  "Sku": "200-000-001",
  "UnitPrices": [
    "EUR 15 pct": 100.00,
    "USD 5 pct": 110.00,
    ...
  ],
  "PricesInclTax": [
    "EUR 15 pct": 115.00,
    "USD 5 pct": 115.50,
    ...
  ],
  "Tax": [
    "EUR 15 pct": 15.00,
    "USD 5 pct": 5.50,
    ...
  ],
  ...
}
```

This doesn't scale well for use cases with many price groups. Therefore, the option to index prices in their own index has been added in Ucommerce 10.6.0.

By default, indexing works as it used to - having prices in the product index. To change the indexing mode, simply add the following setting in Program.cs:

```csharp
...
.AddSearch(options => options.IncludePricesInProductIndex = false)
...
```

Or put the following in appsettings.json:

```json
{
    "Ucommerce":
    {
        ...,
        "Search":
        {
            ...,
            "IncludePricesInProductIndex": false,
            ...
        },
        ...
    }
}
```

{% hint style="warning" %}
A rebuild of the indices is needed after changing the indexing mode.
{% endhint %}

## Using the Price Index

When indexing prices in their own index, use `IIndex<PriceSearchModel>` to work with price data. The `PriceSearchModel` contains the following information:

```json
{
  "ProductGuid": "a11b433e-742e-4f0f-9755-371265cf04ca",
  "MinimumQuantity": 1,
  "PriceGroupGuids": ["8769e717-08d2-4313-82a9-30d4f4886663"],
  "SourcePriceGroupGuid": "8769e717-08d2-4313-82a9-30d4f4886663",
  "UnitPrice": 3495,
  "Id": "e57d3c48-a502-ed11-a2f4-e45f665ce0d6"
}
```

The list of `PriceGroupGuids` contains all price groups that the price is valid for. This means that there's no longer a need for the price group name, as in the product index. Instead, use the GUIDs to find the right price for a given product and price group.

{% hint style="info" %}
When using the price index you get access to the source price group of the price. This is useful when using [derived price groups](/readme/miscellaneous/price-group-inheritance) because it allows you to easily determine where the price originates from. Use this information to e.g. combine B2B custom pricing with promotions when a price is not custom.
{% endhint %}

{% hint style="info" %}
The price index does not contain tax information. This is because a derived price group can have a different tax rate than its base. See [Price Group Inheritance](/readme/miscellaneous/price-group-inheritance#shared-pricing-with-different-tax-rates) for why this is useful.
{% endhint %}

### Calculating Tax

As noted above, the index does not contain information about tax because a derived price group can have a different tax rate than its base. In Ucommerce, the `ITaxService` is used to ensure that tax calculation is consistent across the system. Simply ask for `ITaxService` in your class and call the `Calculate` method to get the right tax, e.g.:

```csharp
// productGuids is a List<Guid> or similar concrete in-memory collection
// priceGroup is a PriceGroupSearchModel object
var prices = await _priceIndex.AsSearchable(cultureInfo)
    .Where(x => 
        productGuids.Contains(x.ProductGuid) 
        && x.PriceGroupGuids.Contains(priceGroup.Id))
    .ToResultSet(token);
foreach(var price in prices)
{
    var tax = await _taxService.Calculate(price.UnitPrice, priceGroup.TaxRate, token);
    // Use the tax for whatever you need,
    // e.g. mapping the price and tax to a DTO returned by an endpoint
}
```

### Populating Products with Price(s)

If the code base heavily depends on having prices as part of the product model, but the use case favors using the price index, mimicking the product model's price structure is easy. Just populate the `PricesInclTax`, `UnitPrices` and `Taxes` properties using the name of the price group as the key, e.g.:

```csharp
// productGuids is a List<Guid> or similar concrete in-memory collection
// priceGroup is a PriceGroupSearchModel object
var prices = await _priceIndex.AsSearchable(cultureInfo)
    .Where(x => productGuids.Contains(x.ProductGuid))
    .ToResultSet(token);
foreach (var product in viewModel.Products)
{
    var price = prices.Results.FirstOrDefault(p => p.ProductGuid == product.Id);
    if (price is not null)
    {
        var tax = await _taxService.Calculate(price.UnitPrice, priceGroup.TaxRate, token);
        product.PricesInclTax[priceGroup.Name] = price.UnitPrice + tax;
        product.UnitPrices[priceGroup.Name] = price.UnitPrice;
        product.Taxes[priceGroup.Name] = tax;
    }
}
```

## Facets

### Prices in the Product Index

When having prices as part of the product index, the properties containing price information are dictionaries. This means that they must be configured in the index definition individually for each price group, as shown below:

```csharp
this.Field(p => p.UnitPrices["EUR 15 pct"])
    .Facet()
    .AutoRanges(count: 5, precision: 10);
 
this.Field(p => p.UnitPrices["USD 5 pct"])
    .Facet()
    .AutoRanges(count: 5, precision: 100);
```

### Using the Price Index

When using the price index, faceted search works exactly like faceted search on other fields:

```csharp
this.Field(p => p.UnitPrice)
    .Facet()
    .AutoRanges(count: 5, precision: 10);
```

See [Facets](/readme/search-and-indexing/indexing/facets) for more details on faceted search.

## Filtering Products Using the Price Index

When using filters (e.g. facets), a little more work is required to make sure the filtering is done correctly because some of the filtered properties might exist on the product while others exist on the price.

First, make sure to supply each index with the correct `FacetDictionary` if facets are used:

```csharp
var productFacetDictionary = new FacetDictionary();
var priceFacetDictionary = new FacetDictionary();

if (request.SelectedCouponFacets is not null)
{
    productFacetDictionary.Add("Coupons", request.SelectedCouponFacets);
    viewModel.SelectedFacets["Coupons"] = request.SelectedCouponFacets.ToImmutableList();
}

if (request.SelectedPriceFacets is not null)
{
    priceFacetDictionary.Add("UnitPrice", request.SelectedPriceFacets);
    viewModel.SelectedFacets["UnitPrice"] = request.SelectedPriceFacets.ToImmutableList();
}

var productsSearchable = _productIndex.AsSearchable(cultureInfo)
    .Where(productFacetDictionary);
var pricesSearchable = _priceIndex.AsSearchable(cultureInfo)
    .Where(priceFacetDictionary);
```

Second, remember to combine the filters, e.g. only look up prices for products found, and remove products without a price:

```csharp
var products = productsSearchable.ToResultSet(token);
var productGuids = products.Results.Select(p => p.Id)
    .ToList();
var prices = await pricesSearchable.Where(x => productGuids.Contains(x.ProductGuid))
    .ToResultSet(token);

// Make a copy using ToList() to be able to remove products from the original list
foreach (var product in products.ToList())
{
    var price = prices.Results.FirstOrDefault(p => p.ProductGuid == product.Id);
    if (price is null)
    {
        // isPriceRangeSet can be based on the value of a price-slider or similar
        if (priceFacetDictionary.Any() || isPriceRangeSet)
        {
            products.Remove(product);
        }
    }
}
```

## What Indexing Mode to Choose?

### Performance

A huge benefit of using the price index is, that you no longer have to transfer a big product model with a lot of superfluous prices over the network. Instead, filter prices using the price group(s) and product(s) GUIDs to retrieve exactly the price(s) needed. This increases performance when the product model size increases due to a high number of price groups.

On the other hand, using the price index means there are two round trips to the index: one to get the product(s) and one to get the price(s). If the number of price groups is low, the extra round trip might be more costly than transferring the superfluous prices from the product index.

### Code Maintainability

As shown above, code complexity can increase if you need to combine the product and price index results. However, the price index does not use volatile (user-editable) price group names as the key to accessing price data in a dictionary.

If performance does not clearly favor one of the modes, use the added/removed code complexity as a tie-breaker.


# Suggestions

Suggestions help your customers find what they are looking for.

## Overview

When using Ucommerce, you can create a custom index definition that specifies which fixed or user-defined properties on your products should trigger suggestions. This will help to make suggestions that will show up as the customer is typing in a search field.

## Enabling suggestions

When you want to enable suggestions for a field, your index definition needs to define the field to be part of the index as well as tell the system that this field should be suggestable.

```csharp
this.Field(p => p["DisplayName"], typeof(string))
    .Suggestable();
```

{% hint style="info" %}
To use suggestions, you must call `ToSuggestions()` in the search API. See [Suggestions](/readme/search-and-indexing/searching#suggestions) for details.
{% endhint %}


# Custom Data

Add custom or external data to your index.

{% hint style="info" %}
This article is not relevant if your custom data is already in a definition field. Instead, you can simply add the property to your [custom index definition](/readme/search-and-indexing/indexing/index-definitions).
{% endhint %}

If you wish to add custom or external data to your index, Ucommerce provides the interface `IAdorn<T>` where `T` is the search model type, e.g. `ProductSearchModel`. The interface provides 2 methods, one for general properties and one for language-specific (multilingual) properties:

{% code title="General properties" %}

```csharp
Task<IImmutableList<T>> Adorn(
            IImmutableList<T> items,
            DeserializedDataBase<T> rawData,
            CancellationToken token);
```

{% endcode %}

{% code title="Language-specific properties" %}

```csharp
Task<ImmutableDictionary<CultureInfo, IImmutableList<T>>> Adorn(
            ImmutableDictionary<CultureInfo, IImmutableList<T>> items,
            DeserializedDataBase<T> rawData,
            CancellationToken token);
```

{% endcode %}

{% hint style="danger" %}
You cannot store nested data structures; for instance, you cannot store a list of product relationships, each with its list of products. If you require such flexibility, you will need to create your own search model.
{% endhint %}

{% hint style="info" %}
In many cases it's only necessary to implement one of the two methods of the interface to add the desired data. When this happens, simply return the `items` parameter using `return await items.InTask();`in the other method.
{% endhint %}

Below is an example of an adorner that adds the product id to the product index.

{% hint style="warning" %}
It is not recommended to use the id externally. This is also the reason the id is not part of the search model.
{% endhint %}

{% hint style="warning" %}
The below adorner is not meant for production as it is not very performant to make a database call for each product to get the id.
{% endhint %}

```csharp
public class ProductIdAdorner : IAdorn<ProductSearchModel>
    {
        private readonly UcommerceDbContext _dbContext;

        /// <summary>
        /// Constructor
        /// </summary>
        public ProductIdAdorner(UcommerceDbContext dbContext)
        {
            _dbContext = dbContext;
        }
        
        /// <inheritdoc />
        public virtual async Task<IImmutableList<ProductSearchModel>> Adorn(
            IImmutableList<ProductSearchModel> items,
            DeserializedDataBase<ProductSearchModel> rawData,
            CancellationToken token)
        {
            foreach (var productSearchModel in items)
            {
                var productEntity = await _dbContext.Products.FirstAsync(p => p.Guid == productSearchModel.Id, token);
                productSearchModel["LegacyId"] = productEntity.Id;
            }

            return items;
        }

        /// <inheritdoc />
        public virtual Task<ImmutableDictionary<CultureInfo, IImmutableList<ProductSearchModel>>> Adorn(
            ImmutableDictionary<CultureInfo, IImmutableList<ProductSearchModel>> items,
            DeserializedDataBase<ProductSearchModel> rawData,
            CancellationToken token) =>
            items.InTask();
    }
```

To register your adorner, add it to the service collection like this:

```csharp
builder.Services.AddScoped<IAdorn<ProductSearchModel>, ProductIdAdorner>();
```

{% hint style="info" %}
Use `AddScoped` instead of `AddSingleton` if the adorner depends on any scoped instances, e.g. `UcommerceDbContext` like above. Microsoft Dependency Injection framework will let you know if you forget by throwing an exception on startup.
{% endhint %}

Remember to check that the property is part of the index definition, otherwise add it:

```csharp
this.Field(p => p["LegacyId"], typeof(int));
```

{% hint style="warning" %}
A rebuild of the indices is needed after changing the index definition.
{% endhint %}


# Searching

Get data from the index

## Indexes

Ucommerce has five built-in indexes: Store, Product, Catalog, Category, and PriceGroup.

To query an index, you need to take a dependency on `IIndex<T>` where `T` is one of the [Ucommerce search models](#search-models).\\

```csharp
public class ProductController : Controller
{
    private readonly IIndex<ProductSearchModel> _productIndex;

    public ProductController(IIndex<ProductSearchModel> productIndex)
    {
        _productIndex = productIndex;
    }
}
```

The `IIndex` interface allows you to query the index with a LINQ-like fluid syntax.\
To get started, you get the `ISearch<Model>` object that you will do your queries on from the index.

```csharp
ISearch<ProductSearchModel> query = _productIndex.AsSearchable(new CultureInfo("da-DK"));
```

{% hint style="warning" %}
Be aware that the interface is LINQ-like but not LINQ. This means the interface does not support certain things. See [Query caveats](#query-caveats) for more details.
{% endhint %}

## Filtering

You can use the `Where()` method to filter the results in the index down to only the objects you are interested in. See [facets](#facets) below for filtering on preconfigured dimensions.

The simplest filter is to match on equality.

```csharp
var SKUQuery = query.Where(x => x.Sku == "Some SKU");
```

### Match

The Match class is for more advanced matching like Full Text, Literal, Fuzzy, and Wildcard.

```csharp
// Full Text
var fullTextFilter = query.Where(x => x.DisplayName == Match.FullText("SearchTerm"));
// Fuzzy
var fuzzyFilter = query.Where(x => x.DisplayName == Match.Fuzzy("SearchTerm", 2));
// Literal
var literalFilter = query.Where(x => x.DisplayName == Match.Literal("SearchTerm"));
// Wildcard with case-insensitive searching, default is case-sensitive search
var wildcardFilter = query.Where(x => x.DisplayName == Match.Wildcard("*searchTerm*", true));
```

### Range

To query in a range, you can use the `Range(int from, int to)` method on the Match class.

```csharp
var rangeQuery = query
                .Where(x => x.PricesInclTax["pricegroup"] == Match.Range(100, 200));
```

## Sorting

You can sort the results with the `Order` and `OrderByDescending` methods.

```csharp
// Order By
var orderedSKUQuery = query.Where(x => x.Sku == "Some SKU")
                .OrderBy(x => x.DisplayName);
// Order By Descending                
var descendingSKUQuery = query.Where(x => x.Sku == "Some SKU")
                .OrderByDescending(x => x.DisplayName);
```

## Paging

Paging can be done using the Skip and Take methods.

```csharp
// The 4th to 6th result
var pagedQuery = query.Skip(3).Take(3);
```

## Results

Like with LINQ, the execution of the query is deferred until you finish the query definition and get the results by calling one of the following methods.

```csharp
ResultSet<ProductSearchModel> result = await query.ToResultSet(token);
         
FacetResultSet<ProductSearchModel> resultWithFacets = await query.ToFacets(token: token);

long count = await query.Count(token);

ProductSearchModel firstProduct = await query.First(token);
ProductSearchModel? firstOrDefaultProduct = await query.FirstOrDefault(token);

ProductSearchModel singleProduct = await query.Single(token);
ProductSearchModel? singleOrDefaultProduct = await query.SingleOrDefault(token);

IImmutableList<NewClass> selectResult = await query.Select<NewClass>(
                            x => new NewClass
                            {
                                // Map properties
                            },
                            token);
```

{% hint style="warning" %}
You should call these methods last to avoid unexpected behavior and keep the query on a search engine level instead of in memory. This also avoids the use of excessive resources on your web server.
{% endhint %}

{% hint style="info" %}
The method `ToSuggestions` mentioned in [suggestions](#suggestions) also triggers the query execution.
{% endhint %}

### Total Count

If you need a total count for a query, it is available on the query result as `TotalCount`. The `TotalCount` property gives you the total number of objects in the index.\
To get the number of objects the query returns, use the `Count` property on the `Results` property.

```csharp
var totalCount = result.TotalCount;
var querycount = result.Results.Count;
```

## Suggestions

To give suggestions to the user when they are typing in a search field, you can use the `Suggestions()` search method.

```csharp
// SuggestionResultSet<ProductSearchModel>
var suggestionsResult = await _productIndex.AsSearchable(new CultureInfo("da-DK")
                                .ToSuggestions(
                                    "DisplayName", //Property to base suggestions on
                                    "se",          //Search Term
                                    false,         //Fuzzy?
                                    token);
```

The Fuzzy parameter controls whether or not the suggestions should be fuzzy, which takes spelling errors into account. This will impact performance, so keep that in mind.

{% hint style="info" %}
To get suggestions on a field you need to have added `Suggestable()` to the [Index Definition](/readme/search-and-indexing/indexing/index-definitions).
{% endhint %}

## Facets

Facets can be used to filter based on preconfigured dimensions. First, follow the documentation on how to [create a facet](/readme/search-and-indexing/indexing/facets#creating-a-facet). After facets are configured and the index created, the facets can be accessed using the `ToFacets()` search method. This method can also be told to return the results of the query to avoid multiple calls to Elasticsearch.

```csharp
var resultWithFacets = await query.ToFacets(false, token);
var facets = resultWithFacets.Facets;
foreach(var facet in facets)
{
    facet.DisplayName //Pretty name of the field name in the index, presented to the customer.
    facet.Name //Name of the field in the index that contains the possible values based on the current search.
    facet.TotalCount //Total Count of documents that has a field with any value based on the search.
    foreach(var val in facet.FacetValues)
    {
        val.DisplayName //The DisplayName of the facet value, Will differ from Value if the facet is multilingual or enum, e.g. "5 coupons".
        val.Value //The individual term as found in the Index, e.g. "5".
        val.Count //Count of documents matching this value based on an existing search.
    }
}
```

Note: The overall facet should be used for creating a header in the filter page while the individual facet values should become actual filters, e.g. checkboxes or sliders. See [facets](/readme/search-and-indexing/indexing/facets) for a visual example.

## Search Models

| Entity     | Search Model          |
| ---------- | --------------------- |
| Product    | ProductSearchModel    |
| Store      | StoreSearchModel      |
| Catalog    | CatalogSearchModel    |
| Category   | CategorySearchModel   |
| PriceGroup | PriceGroupSearchModel |

## Query Caveats

Although the fluid query syntax is LINQ-like, it isn't fully LINQ-compatible.

### Known Limitations

#### Contains

❌ Using `Contains()` on a search model collection property of complex objects does not work:

```csharp
ISearch<ProductSearchModel> query = _productIndex
    .AsSearchable(new CultureInfo("da-DK"))
    .Where(product => product.CategoryProductRelations.Contains(someRelation));
```

✅ Using `Contains()` on a search model collection property of simple types works fine:

```csharp
ISearch<ProductSearchModel> query = _productIndex
    .AsSearchable(new CultureInfo("da-DK"))
    .Where(product => product.CategoryIds.Contains(categoryGuid));
```

✅ Using `Contains()` on an in-memory collection checking for an index model property works fine:

```csharp
ISearch<ProductSearchModel> query = _productIndex
    .AsSearchable(new CultureInfo("da-DK"))
    .Where(product => productGuids.Contains(product.Guid))
```


# Payment Providers


# Stripe Provider Integration

How to get started with payment provider integrations.

## Stripe

### Installing the integration

* Install the `Ucommerce.Payments.Stripe` NuGet package.
* Add the Stripe integration to the program.cs file to use the default Stripe implementation:

{% code title="Program.cs" %}

```csharp
        services.AddUcommerce()    
            .AddBackOffice()
            .AddPayments()
            .AddStripe()
        
        app.UseUcommerce()
            .UseBackOfficeUi()
            .UsePayments()
            .UseStripe("OptionalCallbackUri");
```

{% endcode %}

{% hint style="warning" %}
`OptionalCallbackUri` is the URI where the Stripe webhook should be pointed to later. If left without a value, the default endpoint will be:`{yourDomain}/Stripe/process/callback`
{% endhint %}

Stripe is now available as a payment method service. It can now be selected for new and existing payment methods.

### Creating a Payment Method

* In the back office UI, navigate to **Settings** -> **Payment Methods.**
* Click *New* to create a new payment method.
* Select *Stripe* as the service.

<figure><img src="/files/Agr1L5SyL0819dzePwd2" alt="" width="446"><figcaption><p>All added services will show up in the <em>Service</em> tab</p></figcaption></figure>

### Configuring Stripe

Connect the payment method to a Stripe account by filling out the *Service properties* section in UI.

<figure><img src="/files/maFgqPMZCb5P5OxnKTSr" alt="" width="563"><figcaption><p>This is what the default payment service property editor looks like - it contains <em>PublicKey</em>, <em>CancelUrl</em>, <em>SuccessUrl</em>, <em>SecretKey</em> and <em>WebhookSecret</em> properties.</p></figcaption></figure>

{% hint style="info" %}
The service properties available will vary depending on the payment service selected.
{% endhint %}

<table><thead><tr><th>Property</th><th>Description</th><th data-type="checkbox">Required</th></tr></thead><tbody><tr><td>Public Key</td><td>The account-specific public key for the Stripe account. It can be found in the Stripe Developer Dashboard and starts with `pk_`.</td><td>true</td></tr><tr><td>Secret Key</td><td>The account-specific secret key for the Stripe account. It can be found in the Stripe Developer Dashboard and starts with `sk_`.</td><td>true</td></tr><tr><td>Webhook Secret</td><td>The webhook-specific key. It is set up in the Stripe Developer Dashboard on the webhooks page and starts with `whsec_`. If using a Local listener it is output from the `stripe listen --forward-to` command response</td><td>true</td></tr><tr><td>Success URL</td><td>The partial route that the user is navigated to on a successful purchase.</td><td>true</td></tr><tr><td>Cancel URL</td><td>The partial route that the user is navigated to on an unsuccessful purchase.</td><td>true</td></tr></tbody></table>

{% hint style="warning" %}
The keys can be found on the Stripe Developer Dashboard located [here](https://dashboard.stripe.com/). When setting up the webhook, remember to point it to the [webhook set on startup](#installing-the-integration).
{% endhint %}

Once configured, the payment method can be enabled by flipping the *Enabled* toggle in the UI.

{% hint style="info" %}
When testing the Stripe integration, the [Stripe CLI](https://stripe.com/docs/stripe-cli) can greatly help. It does not require a tunnel to use and can give better results when implementing a stripe payment provider as it allows you to test the payment flow E2E.
{% endhint %}

### Understanding the integration

When you install the integration, and configure it in the back office, you then need to know what its GUID is to use in requesting a payment. This can be found in either the database (`[dbo].[uCommerce_PaymentMethod]`) or in the URL on the payment method page in the back office UI.

<figure><img src="/files/PsFXJ1vkCiu49ZdCw1Rj" alt="" width="446"><figcaption><p>Payment method id in the URL</p></figcaption></figure>

* Once you have created a cart, added an order line, added a shipping address and a billing address, you can then create a payment (see: [Converting a cart to an order](https://github.com/Ucommercenet/ucommerce-docs/blob/main/ucommerce-next-gen/headless/reference/cart/README.md#converting-a-cart-to-an-order)).
* The response will include a `paymentUrl`. Redirect the customer to this URL to complete payment with Stripe. On completion, the customer will be redirected to the `SuccessUrl` or `CancelUrl` depending on the outcome.
* Once payment is completed, Stripe will send a `checkout.session.completed` webhook event to the URL specified in `.UseStripe("OptionalCallbackUri")`, or to `{yourDomain}/Stripe/process/callback` if none has been configured.
* The Stripe webhook handler will reconcile the payment and move it into `Authorized` status. The Stripe implementation uses manual capture, so funds are not taken at this point. The `checkout` pipeline will also execute, converting the cart into an order with `New Order` status.
* When you are ready to capture the funds — for example, after the goods have been dispatched — move the order to `Completed` status via the back office. This triggers the capture of the funds and completes the checkout process.


# Implementing a custom payment provider

## What you will need

To implement a payment provider, you will need to build the following:

* A background service to set up the definition.
* A request to model callbacks from the payment provider.
* A payment provider class.
* A middleware to process callbacks from the payment provider.

## The Background Service

The background service is used to set up the definition for the provider on startup. You should be able to follow the guide for [Bootstrapping data on startup](/readme/how-to/entities-from-code/bootstrapping-data-on-startup) using the *PaymentMethods Definitions* as the definition type:

```csharp
// Gets the definition type from the database
var definitionType = await dbContext.Set<DefinitionTypeEntity>()
                    .FirstOrDefaultAsync(x => x.Name == "PaymentMethod Definitions", token);
```

First, build up the definition field entities:

```csharp
// Creates a list of definition field names and data types matching the 
// payment provider's needs
var fields = new List<KeyValuePair<string, string>>
{
    new("PublicKey", "ShortText"),
    new("SecretKey", "ShortText"),
    new("SuccessUrl", "ShortText"),
    new("CancelUrl", "ShortText"),
    new("WebhookSecret", "ShortText")
}.ToImmutableDictionary();
var definitionFields = new List<DefinitionFieldEntity>();
fields.ForEach(field => definitionFields.Add(new DefinitionFieldEntity
{
    BuiltIn = false,
    DataType = field.Value,
    Definition = definition,
    Multilingual = false,
    Name = field.Key,
    DisplayOnSite = true,
    RenderInEditor = true
}));


```

Then build the definition with the list of definition fields and the correct definition type, and add it to change tracking:

```csharp
// Creates and tracks the definition
var definition = new DefinitionEntity
    {
        BuiltIn = false,
        Description = "Payment provider for X",
        Name = "X",
        DefinitionType = definitionType,
        DefinitionFields = definitionFields
    };
await dbContext.AddAsync(definition, token);
```

Finally, the background service should save using the `dbContext`:

```csharp
// Saves via DbContext
await dbContext.SaveChangesAsync(token);
```

## The Request Model

The request must inherit from `CallbackRequestBase` and pass the payment entity to the base class. It's also recommended that the request takes any objects the payment provider may send to the application, as parameters.

```csharp
// Record that takes in payment, a Stripe Event, Stripe Signature and JSON
public record StripeCheckoutCallbackRequest(PaymentEntity Payment, Event StripeEvent, string StripeSignature, string Json) : CallbackRequestBase(Payment);
```

## The Payment Provider

The provider must inherit from one of the payment provider base classes supported.\
Currently, there are 2 options:

* `RenderpagePaymentProvider`\
  This is for payment providers that render inside the storefront. This provider contains a `GetForm` method to render the payment form in the storefront.
* `RedirectionPaymentProvider`\
  This is for payment providers that redirect to a new page to complete the payment. This provider contains a `GetRedirectUrl` method, which can be used to redirect the customer to their payment.

Both types contain the following methods:

* `Cancel`, to cancel an authorized payment.
* `Capture`, to capture the payment when goods have been shipped.
* `ProcessCallback`, to process the callback from the provider when the payment is created.
* `Refund`, to refund a payment.
* `Alias`, a string value that ties the definition and provider together. It must match the name of the definition created with the background service.

The payment provider wraps the callback request, so in this example, it will look like this:

```csharp
// Payment provider class for stripe
public class StripeCheckoutPaymentProvider : RenderPagePaymentProvider<StripeCheckoutCallbackRequest>
```

Once the provider class is created, add the logic to handle the corresponding operation of each method as needed.

{% hint style="info" %}
Refer to the payment provider's documentation to best determine how to handle various operations.
{% endhint %}

## The Middleware

The middleware must inherit from `ProcessCallbackMiddlewareBase<T>`, where `T` is the request model, e.g.:

```csharp
public class StripeCheckoutCallbackMiddleware : ProcessCallbackMiddlewareBase<StripeCheckoutCallbackRequest>
```

The base class contains 2 abstract methods you must implement:

* `ParseCallback`
* `ValidateCallback`

These methods will be run as the first step of the middleware and are used to ensure the validity of the callback and protect the system.

## Registration

In order to encapsulate all logic related to the provider, and making it easy to reuse, it's recommended to create extension methods for the payment provider using the `PaymentBuilder` and `IPaymentApplicationBuilder`, here is an example of how such methods could look:

```csharp
/// <summary>
/// Adds Stripe and all needed services to the application builder.
/// </summary>
public static PaymentBuilder AddStripe(this PaymentBuilder builder)
{
    builder.UcommerceBuilder.Services.AddHostedService<SetupStripeDefinitions>();
    builder.UcommerceBuilder.Services.AddScoped<IPaymentProvider, StripeCheckoutPaymentProvider>();
    builder.UcommerceBuilder.Services.AddUnique<IPaymentProvider<StripeCheckoutCallbackRequest>, StripeCheckoutPaymentProvider>(ServiceLifetime.Scoped);

    return builder;
}
```

<pre class="language-csharp"><code class="lang-csharp"><strong>/// &#x3C;summary>
</strong>/// Tells the application builder to use Stripe with the given callbackUri.
/// &#x3C;/summary>
public static IPaymentApplicationBuilder UseStripe(this IPaymentApplicationBuilder builder, string processCallbackUri = "/Stripe/process/callback")
{
    builder.UseProvider&#x3C;StripeCheckoutCallbackRequest, StripeCheckoutCallbackMiddleware, StripeCheckoutPaymentProvider>(processCallbackUri);

    return builder;
}
</code></pre>

These methods can now be called on the `PaymentBuilder` and `IPaymentApplicationBuilder` on startup to register and use the provider.


# Data Import

The data importer is a great tool for importing data from external sources. It can be used for the initial setup of a project or scheduled synchronization with a third-party application.

{% hint style="info" %}
Currently, the data importer supports products, prices, price groups, and currencies.
{% endhint %}

## What the Data Importer Does

The data importer inserts data that have not been previously imported into the Ucommerce database. If an entity already exists, it updates the existing entity. The only exception is the currency entity; if a currency with the same ISO code already exists in the database, that entity will not be updated.

## Getting Started

To use the data importer, install the `Ucommerce.DataImport.Core` NuGet package:

```
dotnet add package Ucommerce.DataImport.Core
```

After installing the package, implement the following interfaces to fetch data from the external source(s):

* **ICurrencyFetcher**
* **IPriceGroupFetcher**
* **IPriceFetcher**
* **IProductFetcher**

Each interface has a method to fetch the data. The method takes `batchNumber` and `batchSize` as parameters. Use these to fetch a specific amount of data on each call. For example, if `batchNumber` is 2 and `batchSize` is 100, the fetcher should skip the first 200 elements and take the next 100:

```csharp
var elements = data.Skip(batchNumber * batchSize).Take(batchSize);
```

Below is an example of how `CurrencyFetcher` could be implemented by reading from a CSV file:

```csharp
public class MyCurrencyFetcher : ICurrencyFetcher
{
    public virtual Task<IReadOnlyList<CurrencyData>> GetCurrencyData(int batchNumber, int batchSize)
    {
            var filePath = "currencies.csv";
            using var reader = new StreamReader(filePath);
            using var csv = new CsvReader(reader, config);
            csv.Context.RegisterClassMap(new CurrencyMap());
            var records = csv.GetRecords<CurrencyData>();
            records = records.Skip(batchNumber * batchSize)
                .Take(batchSize);
            return Task.FromResult<IReadOnlyList<CurrencyData>>(records.ToImmutableList());
    }
}
```

## Registering the Fetcher(s)

The fetcher(s) need to be registered in the service collection. It's recommended to create a helper method to register all the fetchers for reusability:

```csharp
public static class MyPIMExtensions
{
    public static IServiceCollection AddPIMFetchers(this IServiceCollection services)
    {
        services.AddSingleton<ICurrencyFetcher, MyCurrencyFetcher>();
        services.AddSingleton<IProductFetcher, MyProductFetcher>();
        ...
        return services;
    }
}
```

## Setting up the Data Importer

Add the necessary settings to the appsettings.json file (or similar) under the Ucommerce section to set up the data importer. The two key settings are `BatchSize` (optional - default is 1000), which determines the size of data chunks to be fetched, and `ConnectionString` (required), which should point to the Ucommerce database the data will be imported into. Here is an example of the app settings for the data importer:

```json
{
  "Ucommerce": {
    "DataImport": {
      "BatchSize": 100,
      "ConnectionString": "YourConnectionString"
    }
  }
}
```

## Running the Data Importer

To run the data importer, inject it where you want to execute it and call the `Run` method.\
Here is an example of an API Controller with an endpoint for executing the data importer on demand:

```csharp
public class DataImportController : Controller
{
    private readonly DataImporter _dataImporter;

    public DataImportController(DataImporter dataImporter)
    {
        _dataImporter = dataImporter;
    }

    public virtual async Task<IActionResult> ImportData()
    {
        await _dataImporter.Run();
        return Ok();
    }
}
```

{% hint style="info" %}
The importer should run on a regular schedule, such as a background service within the website, or even better, as a separate console application outside of the website. Especially if the importer need access to the external source of truth.
{% endhint %}

## Community contributed implementations

If you have implemented a data importer for Ucommerce that you would like to share with the community [please let us know](https://ucommerce.net/contact).

* [UcommerceCsvImporter](https://github.com/CasperWSchmidt/UcommerceCsvImporter) - A flexible importer for CSV files, just tell it where to look for the files, which column is which, and optionally a few other things, and it will take care of the rest.


# Miscellaneous


# Media

All media used in Ucommerce is based on the `Content` data model. The model describes either a file, an image, or a folder.

## Content

The `Content` class is a specialized model designed for Ucommerce, encompassing essential properties to represent images, files, or folders within the system. Key properties of the `Content` class include:

* Id
  * *Description:* The `Id` property serves as a reference identifier utilized by Ucommerce. It should be configured to point to the corresponding data within your external image database.
* NodeType
  * *Description:* The `NodeType` property defines whether the `Content` instance should be interpreted as an image, a file, or a folder. To facilitate this, you can refer to the predefined constant values within `Constants.ImagePicker` that align with these property distinctions.
* ParentId
  * *Description:* The `ParentId` property plays a crucial role in establishing the folder hierarchy within the media picker. For example, an image should possess the `ParentId` value that corresponds to the folder it is contained within.
* URL
  * *Description:* The URL used by the backoffice to render the given image.
* ChildrenCount *(Only applicable for folders)*
  * *Description:* The number of children in the folder. If the content provider does not support a children count, leaving this property out will allow users to navigate to the folder.\\

    <figure><img src="/files/HvzVOTadAb7BuDDD6uCp" alt=""><figcaption><p>Example of folders with and without <code>ChildrenCount</code> set</p></figcaption></figure>

{% hint style="info" %}
The ChildrenCount property is important when working with folders. The *Open Folder* button will not appear in the media picker when the folder does not have children.
{% endhint %}

By implementing the `Content` class with attention to these properties, you can seamlessly manage and represent images, files, and folders within the Ucommerce system.

## Related articles

{% content-ref url="/pages/fvKlyqvZoNOyu0vXE08d" %}
[Images](/readme/extensions/change-service-behavior/images)
{% endcontent-ref %}


# Price Group Inheritance

Starting with version 10.5, a price group can inherit from another price group. This feature enables a derived price group to automatically adopt all product prices set in its base price group. If a specific product price is defined in the derived price group, this price will take precedence when working with that price group. Because product prices are adopted, a derived price group is locked to the same currency as its base price group.

{% hint style="info" %}
If this capability is not needed, it's recommended to avoid creating derived price groups.
{% endhint %}

## **Common Use Cases**

### **Segment-based Pricing**

Derived price groups are handy for assigning different prices for a subset of products to certain customers, organizations, or customer groups while keeping the default price for all remaining products. Simply define all base prices in a base price group. Then let the segment-specific price groups derive from the base and set only the segment-specific prices on the corresponding price groups. This way all segment-specific price groups will inherit all base prices for products not defined directly in it.

### Shared Pricing with Different Tax Rates

Derived price groups are also handy in e.g. the US/EU where the currency and unit price might be the same, but the sales tax/VAT varies by state/country. Simply define all prices in a base price group and create a derived price group for every state/country with the right tax rate for that state/country. This way customers in e.g. CA or Germany will see prices from the base price group, but with the correct tax applied.

## **Limitations**

The default limit for the depth of a price group inheritance tree is **10**. To change this, set the `Ucommerce:Core:MaxPriceGroupHierarchyDepth` setting in your app configuration.

{% hint style="warning" %}
Be careful with deeply nested price group trees. It can significantly impact performance. The feature is not designed for extensive hierarchical depth.
{% endhint %}


# Price Group Criteria

Starting with Ucommerce 10.6.0, you can assign different criteria to price groups, enabling you to filter accessible price groups based on specified properties. This document outlines how to manage these criteria, utilize inheritance, set priorities, and handle the activation of price groups.

## Managing Price Group Criteria

The criteria section can be found in the backoffice on the price group page. Here, you can add, remove, or edit criteria as needed. The current built-in criteria are:

* Time-based criteria
  * Restricts the validity of a price group to a specific time period.
* Customer group criteria
  * Restricts the price group to be usable only by customers within the given customer group(s).
* Organization criteria
  * Restricts the price group to be usable only by customers within the given organization(s).

To add your own custom criteria, refer to the [guide on extending price group criteria](/readme/extensions/custom-price-group-criteria).

## Fetching Valid Price Groups

The process for fetching valid price groups is handled through the `GetPriceGroups` pipeline from the `Ucommerce.Web.Website` headless package. To retrieve accessible price groups, you can use the headless endpoint [`GET price groups`](/readme/headless/reference/price-groups). This endpoint supports query parameters prefixed with `filters-`. These parameters are used in the pipeline to evaluate and determine which price groups are accessible based on their criteria. See the [headless price groups reference](/readme/headless/reference/price-groups) and [extending price group criteria](/readme/extensions/custom-price-group-criteria) for more details on how to use these parameters.

## Price Group Inheritance

Getting price groups traverses the inheritance tree to identify the deepest accessible price groups. If a price group’s criteria are not met, none of its derived price groups will be accessible. This feature is particularly useful for applying a criterion to a group of price groups by making them derive from a common base with the criterion.

## Setting Price Group Priority

Ucommerce 10.6.0 also introduces the concept of price group priority, which determines the order in which price groups are returned. The priority value ranges from 0 upwards, with higher numbers taking precedence in the returned list. This feature is useful for specifying which price group should be prioritized when multiple price groups are accessible.

## Enabling and Indexing Price Groups

From Ucommerce 10.6.0 onwards, when creating a new price group, it is disabled by default. This means that the price group will not be indexed until it is explicitly enabled. This default behavior allows you to fully configure a price group before making it accessible in the index. When inheritance is used, the enabled/disabled status of a price group also affects the accessibility of its derived price groups.


# Soft Deletion Of Entities

When deleting an entity in Ucommerce using

```csharp
_dbContext.Remove(entity)
_dbContext.SavechangesAsync(cancellationToken)
```

or by using one of our APIs to remove an entity, EF Core deletion will be intercepted, and the entity will be checked for soft deletion. If the entity is soft deletable, the deleted flag will be set to true; otherwise, the deletion will go through to the database.

When an entity is soft deleted, its `Deleted` property will be set to true.

## Permanently deleting an entity

It is possible to permanently delete soft deletable entities from the database. This can be done by calling `ExecuteDelete` or `ExecuteDeleteAsync` on the `DbSet` you want to delete from.

```csharp
await _dbContext.Set<PriceGroup>()
    .Where(x => objectsToDelete.Select(p => p.Guid)
        .Contains(x.Guid))
    .ExecuteDeleteAsync(cancellationToken);
```

Remember to carefully filter your query if you wish to do this, as it circumvents all safety measures besides the ones directly built into the database.

## Querying entities

When an entity is soft deleted, it will still be returned from direct queries to a DbContext. They can be filtered out based on the `Deleted` property.

```csharp
await _dbContext.Set<PriceGroup>()
    .Where(x => !x.Deleted)
    .FirstOrDefaultAsync(x => x.Guid == guid, token)
```

## List of soft deletable entities

The following entities are soft deleted by the system:

* Campaign
* Promotion
* Category
* Country
* Currency
* DataType
* DataTypeEnum
* Definition
* DefinitionField
* DefinitionType
* EmailProfile
* EmailType
* OrderNumberSerie
* PaymentMethod
* PriceGroup
* Catalog
* Store
* ProductDefinition
* ProductDefinitionField
* ShippingMethod


# Logging

Good logs make it easier to identify and debug issues with your application at runtime.\
Ucommerce uses the built-in logging system from ASP.NET to make it simple for you to get and store log messages.

### Writing logs using ILogger\<TCategoryName>

Getting started with logging in Ucommerce is easy since it uses the built-in logging APIs.\
You can take a dependency on the `ILogger<TCategoryName>` interface and dependency injection will inject a logger into your class.

```csharp
public class MyClass {
    private readonly ILogger<MyClass> _logger;

    public MyClass(ILogger<MyClass> logger) {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
...
}
```

### Providers

The default Ucommerce templates don't make any changes from the default [logging provider setup](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-8.0#logging-providers).\
We recommend that you gather logs into a central log store to make discovery and debugging production issues easier.

Examples:

* [Seq](https://datalust.co/seq)
* [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview)
* [Datadog](https://www.datadoghq.com/)

### EF Core queries and sensitive data

When debugging issues it can be very useful to see all the values given to, and the structure of, a query created by Entity Framework. Ucommerce makes it easy to set this up for the `UcommerceDBContext` by setting the `Ucommerce.Persistance.SensitiveDataLogging` option to true in your `appSettings.json` file.

```json
{
...
  "Ucommerce": {
    "Persistence":{
      "SensitiveDataLogging": true
    },
...
}
```

The option activates the following in the database context:

* Log queries to the console and debug output.
* [Enables sensitive data logging](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.enablesensitivedatalogging)
* [Enables detailed errors](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.enabledetailederrors)

If you are only interested in seeing the queries created you can set the logging level for the `Microsoft.EntityFrameworkCore.Database.Command` category to `Information` or lower.\
This should also work in production as opposed to the above option.

```json
{
...
  "Logging": {
    "LogLevel": {
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
...
}
```


# OpenTelemetry

Use OpenTelemetry to trace performance of your Ucommerce project

[OpenTelemetry](https://opentelemetry.io/) has become the industry standard for tracing and metrics. Ucommerce comes with support for OpenTelemetry out of the box, and includes tracing for all pipelines.

To enable OpenTelemetry, see the [OpenTelemetry documentation](https://opentelemetry.io/docs/languages/dotnet/). To add the Ucommerce pipeline tracing, add `ucommerce.web.infrastructure.pipelines` as a source. See example below:

{% code overflow="wrap" %}

```csharp
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
{
    builder.Logging.AddOpenTelemetry(logging =>
    {
        logging.IncludeFormattedMessage = true;
        logging.IncludeScopes = true;
    });

    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics =>
        {
            metrics.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation();
        })
        .WithTracing(tracing =>
        {
            tracing.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddSqlClientInstrumentation()
                .AddSource("ucommerce.web.infrastructure.pipelines");
        });

    builder.AddOpenTelemetryExporters();

    return builder;
}
```

{% endcode %}

Leveraging OpenTelemetry when developing locally is easy using [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) as it gives you a nice UI to monitor all ressources. For production, use Azure Application Insights or similar.


# Extensions

How to extend Ucommerce with custom functionality and override default behavior.


# Extending Pipelines

Modify system behavior by modifying Pipelines.

Ucommerce Pipelines are the primary extension points where you can execute any custom logic to fit your requirements. These extensions could be changing the existing behavior of the pipelines, adding new steps to be executed, and integrating with other systems.

## Implementing a Task

Before implementing a new Pipeline Task, we must [identify which pipeline we want to extend](/readme/how-to/discover-pipelines-and-their-tasks), specifically the input and output parameter types. In the examples below, we'll use XPipelineInput and XPipelineOutput to signify the types, as they follow the naming convention of the Pipeline name followed by Input or Output.

* Create a new Class.
* Inherit from `IPipelineTask<XPipelineInput, XPipelineOutput>`
* Implement the Execute method.

{% hint style="info" %}
**Context.Input** contains information passed to the Pipeline when it is being executed.

**Context.Output** contains information that will be returned once the Pipeline is finished executing.
{% endhint %}

{% hint style="info" %}
A Pipeline will save changes in the current database context as the last task. Any tracked entity changes will be saved.
{% endhint %}

```csharp
public class MyCustomTask : IPipelineTask<XPipelineInput, XPipelineOutput>
{
    public CascadeMode CascadeMode { get; } = CascadeMode.Continue;

    public Task Execute(PipelineContext<XPipelineInput, XPipelineOutput> context, CancellationToken cancellationToken)
    {
        // Your custom logic.
    }
}
```

## Adding a Task

The next step is registering the PipelineTask in the IoC container for the proper Pipeline and position. This can be done via the Ucommerce builder, and it is recommended to use a helper method for reusability and clarity.

### Extension method example

This example displays different ways of modifying a pipeline.

```csharp
/// <summary>
/// Extension for adding my custom pipeline task.
/// </summary>
public static class CustomPipelineExtensions
{
    /// <summary>
    /// Adds Custom Cart Extension.
    /// </summary>
    public static IUcommerceBuilder AddMyCartExtension(this IUcommerceBuilder builder)
    {
        //These examples all modify the default 'CalculateCart' pipeline
        
        //This will insert the task 'MyCustomTask1' just before 'SaveUpdatedCartPipelineTask'
        builder.InsertPipelineTaskBefore<IPipelineTask<CalculateCartInput, CalculateCartOutput>, MyCustomTask1>(
            typeof(SaveUpdatedCartPipelineTask));
            
        //This will insert it just after 'SaveUpdatedCartPipelineTask'
        builder.InsertPipelineTaskAfter<IPipelineTask<CalculateCartInput, CalculateCartOutput>, MyCustomTask2>(
            typeof(SaveUpdatedCartPipelineTask));
            
        //This will insert it as the first task of the pipeline
        builder.InsertPipelineTaskFirst<IPipelineTask<CalculateCartInput, CalculateCartOutput>, MyCustomTask3>();
        
        //This will insert it as the last task of the pipeline
        builder.InsertPipelineTaskLast<IPipelineTask<CalculateCartInput, CalculateCartOutput>, MyCustomTask4>();
        
        //This will remove the default task 'SaveUpdatedCartPipelineTask' from the pipeline
        builder.RemovePipelineTask<IPipelineTask<CalculateCartInput, CalculateCartOutput>, SaveUpdatedCartPipelineTask>();
        
        return builder;
    }
}
```

{% hint style="info" %}
Replace tasks by placing yours before or after the default task, then remove the default task.
{% endhint %}

### Register your custom pipeline tasks, using an extension method

Now you can register all of your customized pipeline tasks using your extension method like this.

```csharp
builder.Services.AddUcommerce(builder.Configuration)
    // ... register other Ucommerce components first
    .AddMyCartExtension()
    .Build();
```

{% hint style="info" %}
When adding custom tasks, remember to do it after adding all of the Ucommerce components. This way, you can be sure that your custom logic is added in the desired order and registered correctly.
{% endhint %}


# Order Processing Pipelines

Order processing pipelines are workflows executed automatically when an order moves from one status to another. When an order status changes, the pipeline associated with the new status is executed and can be customized for business logic and integrations.

The way to customize the order processing pipelines differs slightly from regular pipelines. It happens on a dedicated pipeline builder and requires an alias identifier for each pipeline.

Ucommerce comes with the following order processing pipelines out of the box:

| Pipeline Alias   | Order Status Trigger   |
| ---------------- | ---------------------- |
| ToCompletedOrder | Order set as completed |
| ToCancelled      | Order set as cancelled |

## Implementing a Task

Tasks are implemented in the same way as other pipelines in Ucommerce by creating a new class that inherits from the interface\
`IPipelineTask<OrderProcessingInput, OrderProcessingOutput>`.\
See [Implementing a Task section on the Extending Pipelines page](/readme/extensions/extending-pipelines#implementing-a-task) for details.

```csharp
public class MyCustomOrderProcessingTask : IPipelineTask<OrderProcessingInput, OrderProcessingOutput>
{
    public CascadeMode CascadeMode => CascadeMode.Continue;

    public Task Execute(PipelineContext<OrderProcessingInput, OrderProcessingOutput> context, CancellationToken cancellationToken)
    {
        // Your custom logic.
    }
}
```

## Adding a Task

Adding a task is done through the `PaymentBuilder` returned by the `AddPayments` extension method on `IUcommerceBuilder`. It is recommended that you create an extension method to add your configuration.

```csharp
public static class CheckoutPipelineExtensions
{
    public static PaymentBuilder AddMyOrderProcessingExtensions(this PaymentBuilder builder)
    {
        //This will insert the task 'MyCustomOrderProcessingTask' just before 'DefaultToCompletedPipelineTask'
        builder
            .OrderProcessingPipelines
            .GetByAlias("ToCompletedOrder")
            .InsertBefore<MyCustomOrderProcessingTask>(typeof(DefaultToCompletedPipelineTask));

        //This will insert the task 'MyCustomOrderProcessingTask' just after 'DefaultToCompletedPipelineTask'
        builder
            .OrderProcessingPipelines
            .GetByAlias("ToCompletedOrder")
            .InsertAfter<MyCustomOrderProcessingTask>(typeof(DefaultToCompletedPipelineTask));

        //This will insert the task 'MyCustomOrderProcessingTask' as the first task of the pipeline
        builder
            .OrderProcessingPipelines
            .GetByAlias("ToCompletedOrder")
            .InsertFirst<MyCustomOrderProcessingTask>();

        //This will insert the task 'MyCustomOrderProcessingTask' as the last task of the pipeline
        builder
            .OrderProcessingPipelines
            .GetByAlias("ToCompletedOrder")
            .InsertLast<MyCustomOrderProcessingTask>();

        //This will remove the default task 'DefaultToCompletedPipelineTask' from the pipeline
        builder
            .OrderProcessingPipelines
            .GetByAlias("ToCompletedOrder")
            .Remove<DefaultToCompletedPipelineTask>();

        return builder;
    }
}
```

{% hint style="info" %}
Replace a task by placing your custom task before or after the default task, then remove the default task.
{% endhint %}

### Use the extension method to register tasks

```csharp
builder.Services.AddUcommerce(builder.Configuration)
    // ... register other Ucommerce components first
    .AddPayments() // We are extending the PaymentBuilder
    .AddMyOrderProcessingExtensions()
    // ...
    .Build();
```


# Checkout Pipelines

The Checkout pipeline is the workflow executed when an order is placed and can be extended with custom business logic and integrations. Ucommerce also supports multiple workflows attached to different Payment Methods by creating multiple checkout pipelines as described below.

The way to customize the checkout pipelines differs slightly from regular pipelines. It happens on a dedicated pipeline builder and requires an alias identifier for each pipeline.

## Implementing a Task

Tasks are implemented in the same way as other pipelines in Ucommerce by creating a new class that inherits from the interface\
`IPipelineTask<CheckoutInput, CheckoutOutput>`.\
See [Implementing a Task section on the Extending Pipelines page](/readme/extensions/extending-pipelines#implementing-a-task) for details.

```csharp
public class MyCustomCheckoutTask : IPipelineTask<CheckoutInput, CheckoutOutput>
{
    public CascadeMode CascadeMode { get; } = CascadeMode.Continue;

    public Task Execute(PipelineContext<CheckoutInput, CheckoutOutput> context, CancellationToken cancellationToken)
    {
        // Your custom logic.
    }
}
```

## Adding a Task

Adding a task is done through the `PaymentBuilder` returned by the `AddPayments` extension method on `IUcommerceBuilder`. It is recommended that you create an extension method to add your configuration.

```csharp
public static class CheckoutPipelineExtensions
{
    public static PaymentBuilder AddMyCheckoutExtensions(this PaymentBuilder builder)
    {
        //These examples all modify the default 'Checkout' pipeline

        //This will insert the task 'MyCustomCheckoutTask' just before 'DefaultAssignOrderNumberPipelineTask'
        builder
            .CheckoutPipelines
            .GetByAlias("Checkout")
            .InsertBefore<MyCustomCheckoutTask>(typeof(DefaultAssignOrderNumberPipelineTask));

        //This will insert the task 'MyCustomCheckoutTask' just after 'DefaultAssignOrderNumberPipelineTask'
        builder
            .CheckoutPipelines
            .GetByAlias("Checkout")
            .InsertAfter<MyCustomCheckoutTask>(typeof(DefaultAssignOrderNumberPipelineTask));

        //This will insert the task 'MyCustomCheckoutTask' as the first task of the pipeline
        builder
            .CheckoutPipelines
            .GetByAlias("Checkout")
            .InsertFirst<MyCustomCheckoutTask>();

        //This will insert the task 'MyCustomCheckoutTask' as the last task of the pipeline
        builder
            .CheckoutPipelines
            .GetByAlias("Checkout")
            .InsertLast<MyCustomCheckoutTask>();

        //This will remove the default task 'DefaultAssignOrderNumberPipelineTask' from the pipeline
        builder
            .CheckoutPipelines
            .GetByAlias("Checkout")
            .Remove<DefaultAssignOrderNumberPipelineTask>();

        return builder;
    }
}
```

{% hint style="info" %}
Replace a task by placing your custom task before or after the default task, then remove the default task.
{% endhint %}

### Use the extension method to register tasks

```csharp
builder.Services.AddUcommerce(builder.Configuration)
    // ... register other Ucommerce components first
    .AddPayments() // We are extending the PaymentBuilder
    .AddMyCheckoutExtensions()
    // ...
    .Build();
```

## Creating a new checkout pipeline

Creating a new pipeline is very similar to modifying an existing one.

```csharp
public static class CheckoutPipelineExtensions
{
    public static PaymentBuilder AddMyCheckoutExtensions(this PaymentBuilder builder)
    {
        // Create a new checkout pipeline with the default tasks and your custom task
        builder
            .CheckoutPipelines
            .Create("MyCheckout")
            .AddDefaultTasks() // Add the default checkout pipeline tasks
            .InsertLast<MyCustomCheckoutTask>();

        // Create a new pipeline with only your task
        builder
            .CheckoutPipelines
            .Create("MyCheckout")
            .InsertFirst<MyCustomCheckoutTask>();

        return builder;
    }
}
```


# Changing Service Behavior

A service is a class with reusable logic that will be leveraged across the system, for example, in multiple pipelines. Changing the behavior of a service means applying this behavior platform-wide.

For example, customizing the `ITaxService` will change how taxes are calculated for shipments, payments, products, and order lines.

All services are registered in the IoC container, so it is easy to overwrite them.

## Implementing a Service

Once the service to change is identified, create a new class and either implement the interface or inherit from the existing implementation.

* Implementing the interface requires writing a full implementation.
* Inheriting an existing implementation allows for overriding only parts of an existing implementation, where applicable.

### Implementing the Interface

```csharp
 public class CustomXService: IXService
    {
        public Task<XReturn> XMethod(.., CancellationToken token)
        {
            // Custom implementation
            return xReturn;
        }
    }
```

### Inheriting an Existing Implementation

If `XService` has multiple methods, and only one needs to be overridden, it can be done by inheriting the default implementation and selecting which methods to override.

{% hint style="info" %}
It is possible to call the base method from the custom code using

`"base.Xmethod(..);"`
{% endhint %}

```csharp
public class CustomXService: XService
    {
        public override Task<XReturn> XMethod(.., CancellationToken token)
        {
            // Custom implementation
            // Possible to call base.Xmethod(.., token) from custom code.
            return xReturn;
        }
    }
```

### Registering the Custom Service Implementation

```csharp
builder.Services.AddUcommerce(builder.Configuration)
    // ... register other Ucommerce components first
    .Build();
builder.Services.AddUnique<IXService, CustomXService>();
```


# Images

To make use of images with Ucommerce, you will need to implement your image service based on the `IImageService` interface.

## Prerequisites

Before implementing your custom Image Service with Ucommerce, ensure the following:

* **External Data Source:** You need access and necessary permissions to fetch and format data from an external source and into Ucommerce. If you are using a CMS, you could consider using any asset management system implemented in that.

{% hint style="info" %}
A Digital Asset Management (DAM) system is highly recommended for efficient asset management and reliable functionality.
{% endhint %}

## IImageService

The `IImageService` interface provides three distinct methods for retrieving content. These methods cater to various use cases within the system.

1. **`Get` Method**

   This method is commonly used by the backoffice media picker and is designed to retrieve a list of content.

   * *Parameters:*
     * `parentId`: Filter the content based on the parent ID. Default is `null`.
     * `startAt`: Define the starting point for the retrieval. Default is 0.
     * `limit`: Set a limit on the number of content items to retrieve. Default is 30.
     * `token`: Cancellation token for task management. Default is `null`.
   * *Usage:*
     * The media picker relies on this method to:
       * Retrieve all content.
       * Initially (when no ID is set) obtain the first root folder in the list and then invoke the method again with this folder's ID as the root.
   * *Note:*
     * As a result, the first elements in the list should typically represent some form of root object.
2. **`GetById` Method**

   This method is designed to fetch image content by a specific ID.
3. **`GetByIds` Method**

   This method is intended to retrieve all content items associated with a list of specified IDs.

All of these methods should fetch and filter your data, based on your external data source.

## Related articles

{% content-ref url="/pages/njonZbrUttQXGAMloUEW" %}
[Media](/readme/miscellaneous/media)
{% endcontent-ref %}


# Content

*Content* is a term used to describe different types of data within a CMS. By default, content is not used anywhere in Ucommerce. By using the definition system, it can be added to any definition-driven entity, by giving it a field with the **Content** or **ContentPickerMultiSelect** data types.

To make use of content within Ucommerce, you will need to implement your content service based on the `IContentService` interface.

## Prerequisites

Before implementing your custom content service, ensure the following:

* **External Data Source:** You need access and necessary permissions to fetch and format data from an external source and into Ucommerce. If you are using a CMS, it will most likely have some form of content management API that requires authentication.

### **IContentService**

When you implement `IContentService`, you will be faced with four distinct methods for retrieving and managing content.

#### **`GetContent` Method**

This method has two variants: one that accepts a single content ID and another that accepts a list of IDs. Both should retrieve the content associated with the specified ID(s), whether individual or multiple.

#### **`GetChildren` Method**

This method should retrieve all content items that have the specified content ID as their parent.

Additionally, if the ID given is null, this method should return the root level content (the content with no parent).

#### **`DownloadContent` Method**

This method should return a complete HTML file for the specified content input. By default, it is used in a single location: the `DefaultSendConfirmationEmailPipelineTask`, where it populates an email with HTML content. So if there is no need for this functionality, ignore this method and consider removing/altering that task.

All methods should fetch and filter data based on the configured external data source.

{% hint style="info" %}
The `DefaultContentService` implementation built into Ucommerce already implements the `DownloadContent` method in a meaningful way. By inheriting from `DefaultContentService` instead of `IContentService` you can skip implementing `DownloadContent` and focus on overriding the other methods that the `DefaultContentService` does not implement in a meaningful way (it always returns a content object representing *Not found*).
{% endhint %}


# Extend the Backoffice

We recognize that our backoffice UI might not fully satisfy your users' requirements. Therefore, we have introduced several extension points, enabling you to integrate your own components into the UI.


# Custom UI Components

Sometimes you want to display more information to your users, than Ucommerce does by default.\
You may want to display some contextual information from an external source or have a button that calls a custom API.\
For this purpose, Ucommerce has support for inserting custom [web components](https://www.webcomponents.org/) into specific sections of the UI.

## Custom UI Mode

We have made it easy for you as a developer to identify the sections where you can insert your custom components. For this purpose, you can enable Custom UI Mode from Settings, which will display a box with information on how to insert a component in all the places where available.\
In some places, like the products editor, we also pass in a `props` object containing relevant context data.

### Getting Started

To begin using the Custom UI feature, ensure that using admin account. Navigate to the ***Settings***, and under ***Developer*** you'll find the ***Custom UI mode*** toggle. Enable this toggle to activate the Custom UI mode.

#### Activating Custom UI Mode

Once the Custom UI mode is activated, a notification will be displayed in the main navigation area, indicating that you are currently in Custom UI mode. This notification serves as a visual cue to remind you that you're seeing extra sections as potential placeholders for your custom component .

#### Integrating Custom Components

In Custom UI mode, each page will display placeholders where you can render your custom web components. These placeholders are marked with unique keys, indicating the specific location where your custom component will be displayed.

#### Viewing Available Properties

Each placeholder includes an anchor labeled 'Show Available Properties.' Clicking on this anchor will reveal an object containing the properties (props) that are passed to your custom component.

### Creating Custom Web Components

1. Develop your web component using standard JavaScript.
2. Ensure that your component adheres to the Web Components standard.

#### Example of web component

```javascript
class StoresNameLoader extends HTMLElement {
  static get observedAttributes() {
    return ['props'] // Add the props attribute to the list of observed attributes
  }

  constructor() {
    super()
    this.attachShadow({ mode: 'open' })

    this._props = this.getAttribute('props') // Initialize the property from the attribute

    this.container = document.createElement('div')

    this.shadowRoot.appendChild(this.container)

    this.renderElements()
  }

  renderElements() {
    this.container.innerHTML = `
      <h5>Sample of custom web component from Ucommerce</h5>
          <h2>${this._props?.header || ''}</h2>
          <button id="stores-loader-button">Load stores</button>
          <ul id="stores-list"></ul>
  `

    this.shadowRoot
      .getElementById('stores-loader-button')
      .addEventListener('click', () =>
        this.handleClick(this._props.orderGuid, this._props.cultureCode)
      )
  }

  set props(value) {
    // Define a setter for the property
    this._props = value
    this.renderElements()
  }

  get props() {
    // Define a getter for the property
    return this._props
  }

  async handleClick(orderGuid, cultureCode) {
    const path = `stores/shallow?startAt=0&limit=50&cultureCode=${cultureCode}`
    const response = await this._props.ucommerceHttpService(path)

    if (response.status === 200) {
      const ul = this.shadowRoot.querySelector('ul')


      response.data.forEach((store) => {
        const li = document.createElement('li')
        li.textContent = store.name
        ul.appendChild(li)
      })
    }
  }
}

customElements.define('stores-name-loader', StoresNameLoader)
```

### **Inserting Components**

**For setting up your components we recommend creating an extension method on the IUcommerceBuilder encapsulating your configuration and calling it in your program.cs.**

```csharp
public static class MyCustomExtensions
{
    public static IUcommerceBuilder AddMyCustomComponents(this IUcommerceBuilder builder)
    {
        // Add a com    ponent not hosted by the CustomComponentServer
        builder.AddCustomComponentWithUrl(
            "stores-home_main-top", // The key from the Ucommerce backoffice
            "Stores name loader", // The section header
            "stores-name-loader", // The constructor to call in your component
            "https://url.to/your/storesNameLoader.js");

        // Add a component hosted by the CustomComponentServer
        // See Hosting Components in Ucommerce below
        builder.AddCustomComponentWithPath(
            "orders-home_main-bottom", // The key from the Ucommerce backoffice
            "The order top first viewer", // The section header
            "result-component", // The constructor to call in your component
            "webComponentSample.js"); // The path to the file inside the /CustomComponents directory
        return builder;
    }
}
```

Call your extension method in program.cs when setting up Ucommerce

```csharp
var ucommerceBuilder = builder.Services.AddUcommerce(builder.Configuration)
    ...
    .AddMyCustomComponents();
```

### Hosting Components in Ucommerce

In order for the backoffice UI to download your custom web component, you will have to host it on a server.

Ucommerce can host your components with the CustomComponentServer.

By default, we look for a directory called `/CustomComponents` in your web project, so you need to ensure that it exists and is published on your website.

Create the `/CustomComponents` directory in your project.

Add the following to your Ucommerce project `.csproj` file to publish the contents of the folder to your Ucommerce website.

```xml
</Project>
...
    <ItemGroup>
        <Content Include="CustomComponents\**\*" CopyToPublishDirectory="Always" />
    </ItemGroup>
...
</Project>
```

Once the directory exists and is published you can enable the CustomComponentServer by calling the UseComponentServer method on the `IUcommerceApplicationBuilder` application builder.

```csharp
app.UseUcommerce()
    ...
    .UseCustomComponentServer();
```


# Custom Editor UI

Ucommerce has built-in editors for most use cases. If they do not cover a specific use case, it is possible to create a custom editor by utilizing our [Custom UI component](/readme/extensions/extend-the-backoffice/custom-ui-components) system.

Using a custom component as an editor draws on the power of our definition system and our custom UI components system.

To get started, create a new data type in the database. The `DefinitionName` column on the data type is the property that maintains a connection to a custom component. Here is a code example of this:

```csharp
var dataType = new DataTypeEntity
{
    Name = "MyCustomField",
    Nullable = true,
    ValidationExpression = "",
    BuiltIn = false,
    DefinitionName = "CustomCriterionField01",
};
```

Use this new data type, to either create a new definition for the entity you want to extend or add a definition field to an existing definition.

An example of creating a definition with definition fields can be found in our [Extending Criteria](/readme/extensions/extending-criteria) or [Product Definitions & Fields](/readme/how-to/entities-from-code/product-definitions-and-fields) documentation. They use criterion and product entities as examples, but the definition system can be used for any entity supported by the definition system.

The key of the custom UI component has to match the DefinitionName of the data type entity. Custom UI components in general are discussed further in [Custom UI Components](/readme/extensions/extend-the-backoffice/custom-ui-components).

When a definition field, using a data type with a `DefinitionName` property matching the name of a registered component exists, the custom UI editor is functional and will show up in the backoffice UI. Below is an example of a class containing a background service that creates and saves a new definition, similar to the example in the [Product Definitions & Fields](/readme/how-to/entities-from-code/product-definitions-and-fields) documentation. The class also has a method for registering the new custom components and the background service in the DI container. The `AddCustomEditor` method can then be used to register everything needed for a new definition to show up.

```csharp
/// <summary>
/// Class containing setup of a new Custom Criterion Definition
/// with custom UI components.
/// </summary>
public static class CustomCriterionCustomUiComponents
{
    private const string CUSTOM_EDITOR01 = "CustomCriterionField01";
    private const string CUSTOM_EDITOR02 = "CustomCriterionField02";
    // The id of the definition type to map to in your database
    private const int DEFINITION_TYPE_ID = 5434834;

    /// <summary>
    /// Method for registering a new criterion definition with custom UI components.
    /// </summary>
    public static IUcommerceBuilder AddCustomEditor(this IUcommerceBuilder builder)
    {
        builder.Services
            .AddHostedService<SetupCustomCriterionUsingCustomUiComponents>();
        builder.AddCustomComponentWithPath(
            CUSTOM_EDITOR01, 
            $"{CUSTOM_EDITOR01} Header", 
            "result-component", 
            "webComponentSample.js");
        builder.AddCustomComponentWithPath(
            CUSTOM_EDITOR02, 
            $"{CUSTOM_EDITOR02} Header", 
            "result-component", 
            "webComponentSample.js");
        builder.AddCustomComponentWithPath(
            CUSTOM_EDITOR02, 
            $"{CUSTOM_EDITOR02} second Header", 
            "result-component", 
            "webComponentSample.js");
        return builder;
    }

    public class SetupCustomCriterionUsingCustomUiComponents : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;

        public SetupCustomCriterionUsingCustomUiComponents(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await using var asyncScope = _serviceProvider.CreateAsyncScope();
            var dbContext = asyncScope.ServiceProvider
                .GetRequiredService<UcommerceDbContext>();

            // Guard to ensure code is only run once
            if (dbContext.Set<DefinitionEntity>()
                    .FirstOrDefault(x => x.Name == "Custom Criterion Definition") is not null)
            {
                return;
            }

            // Set up data using dbContext
            var firstDataType = new DataTypeEntity
            {
                Name = "First Custom Editor Type",
                Nullable = true,
                ValidationExpression = "",
                BuiltIn = false,
                DefinitionName = CUSTOM_EDITOR01,
            };

            // Set up data using dbContext
            var secondDataType = new DataTypeEntity
            {
                Name = "Second Custom Editor Type",
                Nullable = true,
                ValidationExpression = "",
                BuiltIn = false,
                DefinitionName = CUSTOM_EDITOR02
            };
            dbContext.Add(firstDataType);
            dbContext.Add(secondDataType);
            var defFields = new List<DefinitionFieldEntity>();

            defFields.Add(new DefinitionFieldEntity
            {
                Name = "First Custom Field",
                DefaultValue = "",
                DataType = firstDataType,
                DisplayOnSite = true,
                RenderInEditor = true
            });
            defFields.Add(new DefinitionFieldEntity
            {
                Name = "Second Custom Field",
                DefaultValue = "",
                DataType = secondDataType,
                DisplayOnSite = true,
                RenderInEditor = true
            });

            dbContext.Set<DefinitionEntity>()
                .Add(new DefinitionEntity
                {
                    BuiltIn = false,
                    Description = "Custom Criterion Definition Description",
                    Name = "Custom Criterion Definition",
                    DefinitionTypeId = DEFINITION_TYPE_ID,
                    DefinitionFields = defFields
                });

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

{% hint style="info" %}
Definitions and custom UI components are cached so a hard reload of the browser might be necessary to see the new editors.
{% endhint %}


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

```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.
{% 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.
* `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.

{% 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.

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


# Custom Price Group Criteria

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

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.
{% 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.
{% 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.

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

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


# How-To


# Migrate from Classic

## Before Migrating

If you have any complex custom code in your Ucommerce Classic solution, we recommend contacting us [here](https://ucommerce.net/contact) before you start your migration.\
It's also important that your solution meets our prerequisites, which are listed [here](/readme/getting-started/prerequisites).

## Migrating

### Migrating the database

First, you must upgrade your database to the latest version of Ucommerce 9. If you're already on a version of Ucommerce 9, this should be easily accomplished by upgrading your Ucommerce package and starting your application.\
If you're using a regular SQL Server for your database, make sure that your database has a SQL Server compatibility level of at least 130.

### Installing and connecting Next-Gen

We recommend using our templates for easy installation of Ucommerce Next-Gen. Information on them can be found [here](/readme/getting-started/ucommerce-templates).\
Once you've created a new project from one of the templates, make sure you add the connection string for your database. When this is done, you're ready to start the project and it will migrate the database for you.\
After this, go to the Ucommerce backoffice at `/ucommerce` and verify that your data has been migrated.

### Migrating custom logic

To migrate custom logic, we recommend getting an overview of which parts of the solution are custom-built and then using our documentation to guide you on the changes needed.

The main differences between Ucommerce Classic and Next-Gen are how dependency injection works, application setup, database access, and pipeline registration.\
Some articles that might be of use to you are listed below:

* [Discover pipelines and their tasks](/readme/how-to/discover-pipelines-and-their-tasks)
* [Extending Pipelines](/readme/extensions/extending-pipelines)
* [Change Service Behaviour](/readme/extensions/change-service-behavior)
* [Entities from code](/readme/how-to/entities-from-code)
* [Custom UI Components](/readme/extensions/extend-the-backoffice/custom-ui-components)

*N.B.: All custom logic must be ported from the .Net framework to .Net 8.*


# Common database issues

## 10.9.0: Duplicate definition field names on definitions

When migrating to Ucommerce Next Gen >10.9.0, you might encounter the following error:

{% code overflow="wrap" %}

```
Ucommerce database migration failed with message The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.uCommerce_DefinitionField' and the index name 'UX_uCommerce_DefinitionField_DefinitionId_Name'. The duplicate key value is (1, Organization). The statement has been terminated., in script 117 - Create missing unique indexes on definition fields
```

{% endcode %}

This means that you have multiple definition fields with the same name for the same definition. This makes the data ambiguous and should be avoided. Unfortunately, we cannot know what data is correct and apply automatic corrections. Instead, what you have to do is clean up the database manually before proceeding with the migration.

Fortunately we have made this task easy for you. To find the duplicates that needs to be cleaned up, run the following scripts:

{% code title="Product definitions" overflow="wrap" %}

```sql
SELECT pdf.* FROM uCommerce_ProductDefinitionField as pdf 
INNER JOIN ( 
    SELECT [ProductDefinitionId], [Name] 
    FROM [uCommerce_ProductDefinitionField] 
    GROUP BY ProductDefinitionId, Name 
    HAVING COUNT(*) > 1 
) AS Duplicates ON pdf.ProductDefinitionId = Duplicates.ProductDefinitionId AND pdf.Name = Duplicates.Name
ORDER BY Name, ProductDefinitionId
```

{% endcode %}

{% code title="Other definitions" overflow="wrap" %}

```sql
SELECT df.* FROM uCommerce_DefinitionField as df 
INNER JOIN ( 
    SELECT [DefinitionId], [Name] 
    FROM [uCommerce_DefinitionField] 
    GROUP BY DefinitionId, Name 
    HAVING COUNT(*) > 1 
) AS Duplicates ON df.DefinitionId = Duplicates.DefinitionId AND df.Name = Duplicates.Name
ORDER BY Name, DefinitionId
```

{% endcode %}


# Entities from code

This section contains how-to guides on how to create Entities from code. These guides are recommendations and inspiration for when you need to work directly with inserting Ucommerce data into the database from code.


# Bootstrapping data on startup

This article is a general recommendation for setting up data on startup. This can be useful when bootstrapping a project and setting up Data Types, Definitions, and other entities.

## When to create the data?

We recommend placing the logic for creating data into a HostedService. By inheriting BackgroundService, you create a potentially long-running job activated once at app startup.

{% embed url="<https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services>" %}
Read more about Background tasks on MicrosoftLearn.
{% endembed %}

## Create the Service

{% code overflow="wrap" %}

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

    public SetupData(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var asyncScope = _serviceProvider.CreateAsyncScope();
        var dbContext = asyncScope.ServiceProvider.GetRequiredService<UcommerceDbContext>();

        // Set up data using dbContext

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

{% endcode %}

## Register the Service

```csharp
builder.Services.AddHostedService<SetupData>();
```

## Related Articles

{% content-ref url="/pages/1yn0Iz3oAJLrBaNc79Dy" %}
[Product Definitions & Fields](/readme/how-to/entities-from-code/product-definitions-and-fields)
{% endcontent-ref %}


# Product Definitions & Fields

Creating Product Definitions and their related definition fields is the most frequent use for definitions. The taxonomy of products differs from implementation to implementation, and below are a few examples of how they can be created via code.

{% hint style="info" %}
The code in these examples can and should be customized for individual use cases.
{% endhint %}

## Product Definition Fields

Start by creating as many Product Definition Fields as needed for your Product Definition. We start with the fields because when creating the definition, we can then immediately add the fields before we save anything to the database.

{% code overflow="wrap" %}

```csharp
private ProductDefinitionFieldEntity CreateProductDefinitionField(
    DataTypeEntity dataType,
    string name,
    bool isMultilingual,
    bool isVariantProperty)
{
    return new ProductDefinitionFieldEntity
    {
        Name = name,
        Deleted = false,
        Multilingual = isMultilingual,
        DisplayOnSite = true,
        RenderInEditor = true,
        IsVariantProperty = isVariantProperty,
        DataType = dataType
    };
}
```

{% endcode %}

The method above can be re-used to create multiple Product Definition Fields by supplying:

* dataType - denoting the field should be displayed as, for example, a short text or a number field.
* name - the name of the field that will appear on the product editor.
* isMultilingual - if true, the field will be able to have differentiated values for each configured language.
* isVariantProperty - if true, the field will be available only on variants; otherwise, it will be available only on products.

## Product Definition

Continue with creating a Product Definition.

{% code overflow="wrap" %}

```csharp
private ProductDefinitionEntity CreateProductDefinition(
string name, 
string description)
{
    return new ProductDefinitionEntity
    {
        Name = name,
        Description = description,
        Deleted = false
    };
}
```

{% endcode %}

* name - Name of the definition, denoting the type of product.
* description - A description of what the definition is for.

## Create & Save

The below example ties it all together. This is done within the context of a HostedService, which is covered in [Bootstrapping data on startup](/readme/how-to/entities-from-code/bootstrapping-data-on-startup).

{% code overflow="wrap" %}

```csharp
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var asyncScope = _serviceProvider.CreateAsyncScope();
        var dbContext = asyncScope.ServiceProvider.GetRequiredService<UcommerceDbContext>();
        
        var exists = dbContext.Set<ProductDefinitionEntity>()
            .Any(x => x.Name == "Shirt");
        
        if (exists)
        {
            return;
        }
        
        var shortTextDataType = dbContext
            .Set<DataTypeEntity>()
            .First(x => x.DefinitionName == "ShortText");
        
        var shirtDefinition = CreateProductDefinition("Shirt", "Definition for Shirt type products.");
        
        shirtDefinition.ProductDefinitionFields = new List<ProductDefinitionFieldEntity>()
        {
            CreateProductDefinitionField(shortTextDataType, "Color", true, true),
            CreateProductDefinitionField(shortTextDataType, "Code", false, false)
        };
        
        dbContext.Add(shirtDefinition);
        
        await dbContext.SaveChangesAsync(stoppingToken);
    }
```

{% endcode %}

{% hint style="info" %}
Note the early return if the Definition already exists. Since this code runs on each startup, there must be a check so it only gets executed when necessary. In this case, if the definition already exists.
{% endhint %}

* Create a scope within which to resolve a UcommerceDbContext.
* Implement a check to decide if the bootstrapping should run on the database.
* Find the right data type (in this example, we use ShortText for both fields).
* Create the Product Definition using our helper method.
* Create and add Product Definition Fields to the definition (in this example, we create two, one multilingual variant property for color and one non-multilingual product property for barcode)
* Add the definition to the context and save changes to the database.

Once the application starts, the new definition will be selectable for products in the backoffice.

## Related Articles

{% content-ref url="/pages/4F8D6TkbAjEn0G20TAYf" %}
[Bootstrapping data on startup](/readme/how-to/entities-from-code/bootstrapping-data-on-startup)
{% endcontent-ref %}


# Discover pipelines and their tasks

Before extending a pipeline, you need to know which one to extend. To make it easier to discover our pipelines, an endpoint is available from the backoffice with the URL `http(s)://{yourdomain}/ucommerce/api/v1.0/pipelines`

{% hint style="info" %}
The endpoint uses [authentication](/readme/backoffice-authentication) like the rest of the backoffice.
{% endhint %}

Calling the endpoint will return JSON containing all pipelines that are registered in the service collection of your app with input and output types, namespace, tasks (in order of execution), and the interface each task implements. The result will be grouped by entity and look like this:

```json
"order": [
    {
        "tasksImplement": "IPipelineTask<GetOrderInput, GetOrderOutput>",
        "inputType": "GetOrderInput",
        "name": "GetOrderPipeline",
        "namespace": "Ucommerce.Web.Core.Pipelines.Order.GetOrder",
        "outputType": "GetOrderOutput",
        "tasks": [
            "GetOrderPipelineTask"
        ]
    },
    {
        "tasksImplement": "IPipelineTask<GetOrderNumberInput, GetOrderNumberOutput>",
        "inputType": "GetOrderNumberInput",
        "name": "GetOrderNumberPipeline",
        "namespace": "Ucommerce.Web.Core.Pipelines.Order.GetOrderNumber",
        "outputType": "GetOrderNumberOutput",
        "tasks": [
            "GetOrderNumberPipelineTask"
        ]
    },
    ...more order-related pipelines
],
...more entities
```

{% hint style="info" %}
Using [Postman](https://www.postman.com/) to call the endpoint makes it easy to collapse irrelevant parts of the response or search for the things you need.
{% endhint %}

The endpoint will also reflect your own changes to the app, and will help you know if your custom tasks have been registered correctly, as your pipeline tasks will be shown in the appropriate list after registration.


# Executing a pipeline

### Injecting and Executing a Pipeline

#### Inject the Pipeline

If you want to execute a pipeline from somewhere else, first, inject it into the constructor. This is demonstrated in the `MyController` constructor, which takes an `IPipeline<PipelineInputType, PipelineOutputType>` as a parameter:

```csharp
public MyController(IPipeline<PipelineInputType, PipelineOutputType> pipeline)
{
    _myPipeline = pipeline;
}
```

#### Execute the Pipeline

When you're ready to execute the pipeline, create an instance of the input type and populate it with your desired parameters. Then, use the `_myPipeline.Execute` method to process the input and get the output from the result.\
Here's how you can do it:

```csharp
var input = new PipelineInputType()
{
    Property1 = true,
    Property2 = 42,
    Property3 = "Hello",
};
var response = await _myPipeline.Execute(input, token);
var output = response.EnsureSuccess();
```

`response.EnsureSuccess()` throws a `PipelineException` if any exceptions happen during the execution of the pipeline, otherwise, it will return the output (result) of the pipeline execution. The exceptions and output are also directly accessible using `response.Errors` and `response.Output` respectively.


# Integrations

Ucommerce is built to work in a [composable](https://ucommerce.net/product#composable) world, and ships with a lot of default functionality that can be replaced by integrations to other systems, like PIMs, CMSs, DAMs, etc., as needed. In this section we'll show examples of how to integrate systems with Ucommerce.


# Umbraco Media Delivery API

Leverage Umbraco as your DAM

In this guide, we'll use Umbraco as a *Digital Asset Management* system and integrate it with Ucommerce. We'll use the *Umbraco Media Delivery API* to retrieve the information we need.

Here's a look at the final result:

<figure><img src="/files/3pEFOj2wV2ddW5OIRVhY" alt=""><figcaption><p>A look at how Umbraco can be leveraged as a DAM system for Ucommerce</p></figcaption></figure>

## Prerequisites

Before you get started, please make sure both [Umbraco templates](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/install) and [Ucommerce templates](/readme/getting-started/ucommerce-templates) are installed.

## Setup Projects

### Umbraco

Create a new project using the [Umbraco template](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/install/install-umbraco-with-templates). Make sure both delivery API and media is enabled in *appsettings.json*:

```
"DeliveryApi": {
  "Enabled": true,
  "PublicAccess": true,
  "Media": {
    "Enabled": true,
    "PublicAccess": true
  }
}
```

{% hint style="warning" %}
`PublicAccess` should not be `true` in a publicly available environment
{% endhint %}

When Umbraco is installed, take it for a spin and add some images to the Media library:

<figure><img src="/files/Ryyd7rD1fJewA0Rer6YU" alt=""><figcaption><p>Media added to Umbraco</p></figcaption></figure>

### Ucommerce

Create a new project using the [Ucommerce Headless template](/readme/getting-started/standalone).

## Generate the Umbraco Client

To make Ucommerce communicate with Umbraco, we need a client to handle the REST calls to the Umbraco Delivery API. You can either code this yourself or use a tool to generate it. For this guide, we're going to use [Microsoft Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview) to generate it for us.

First, install Microsoft Kiota as a [.Net tool](https://learn.microsoft.com/en-us/openapi/kiota/install?tabs=bash#install-as-net-tool) or as a [VS Code extension](https://learn.microsoft.com/en-us/openapi/kiota/install?tabs=bash#install-the-visual-studio-code-extension).

Second, use Kiota to generate a client from the Umbraco Delivery API Specification using either the command line (see below) or the VS Code plugin UI.

```bash
kiota generate 
    --openapi "https://localhost:{yourUmbracoPortNumber}/umbraco/swagger/delivery/swagger.json" \
    --language CSharp \
    --additional-data false \
    --class-name UmbracoClient \
    --namespace-name UcommerceTest.Integrations.Umbraco \
    --output ./Umbraco
```

{% hint style="info" %}
The Umbraco Delivery API Specification can be found on [https://localhost:{yourUmbracoPortNumber}/umbraco/swagger/delivery/swagger.json](https://dev.ucommerce.net/readme/integrations/https:/localhost:{yourUmbracoPortNumber}/umbraco/swagger/delivery/swagger.json)
{% endhint %}

{% hint style="info" %}
See [Optional parameters](https://learn.microsoft.com/en-us/openapi/kiota/using#optional-parameters-3) for more details on the command
{% endhint %}

Third, add the needed Kiota dependencies to the Ucommerce project.

{% hint style="info" %}
Run `kiota info --language CSharp` to get a list of dependencies needed.
{% endhint %}

The csproj file should look similar to this:

```html
<ItemGroup>
    <PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.14.0" />
    <PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.14.0" />
    <PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.14.0" />
    <PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.14.0" />
    <PackageReference Include="Microsoft.Kiota.Serialization.Multipart" Version="1.14.0" />
    <PackageReference Include="Microsoft.Kiota.Serialization.Text" Version="1.14.0" />
    <PackageReference Include="Ucommerce.Web.BackOffice" Version="10.8.0" />
    <PackageReference Include="Ucommerce.Web.WebSite" Version="10.8.0" />
    <PackageReference Include="Ucommerce.Search.Elastic" Version="10.8.0" />
</ItemGroup>
```

## Implementing UmbracoImageService.cs

In the Ucommerce project, create a new class `UmbracoImageService` that inherits from `IImageService`. This class is responsible for mapping between the `UmbracoClient` created previously and Ucommerce. See [Images](/readme/extensions/change-service-behavior/images) for more details.

Below is a complete example:

{% code overflow="wrap" lineNumbers="true" fullWidth="false" %}

```csharp
using System.Collections.Immutable;
using Ucommerce.Web.Infrastructure;
using Ucommerce.Web.Infrastructure.Core;
using Ucommerce.Web.Infrastructure.Core.Models;
using UcommerceTest.Integrations.Umbraco;
using UcommerceTest.Integrations.Umbraco.Models;

namespace UcommerceTest;

public class UmbracoImageService(UmbracoClient kiotaClient, UmbracoSettings umbracoSettings) : IImageService
{
    public async Task<IImmutableList<Content>> Get(
        string? parentId = null, 
        int startAt = 0, 
        int limit = 30,
        CancellationToken? token = null)
    {
        parentId ??= "/";
        var items =
            await kiotaClient.Umbraco.Delivery.Api.V2.Media.GetAsync(
                request =>
                {
                    request.QueryParameters.Fetch = $"children:{parentId}";
                    request.QueryParameters.Skip = startAt;
                    request.QueryParameters.Take = limit;
                }, token.GetValueOrDefault());

        return items?.Items?
            .Select(item => MapMedia(parentId, item))
            .ToImmutableList() ?? ImmutableList<Content>.Empty;
    }

    public async Task<Content> GetById(string? id, CancellationToken token)
    {
        if (string.IsNullOrEmpty(id))
        {
            return ImageNotFound();
        }

        var item = await kiotaClient.Umbraco.Delivery.Api.V2.Media.Item[Guid.Parse(id)]
            .GetAsync(cancellationToken: token);

        return item is null ? ImageNotFound() : MapMedia(null, item);
    }
    
    public async Task<IImmutableList<Content>> GetByIds(
        IImmutableList<string> ids, 
        CancellationToken? token = null)
    {
        var guids = ids.Select(Guid.Parse).Cast<Guid?>().ToArray();
        var items = await kiotaClient.Umbraco.Delivery.Api.V2.Media.Items.GetAsync(
            request => request.QueryParameters.Id = guids, token.GetValueOrDefault()) ?? [];

        return items
            .Select(item => MapMedia(null, item))
            .ToImmutableList();
    }

    /// <summary>
    /// Maps the response model to a Content object
    /// </summary>
    private Content MapMedia(
        string? parentId, 
        ApiMediaWithCropsResponseModel responseModel) => new()
    {
        Name = responseModel.Name ?? "No name",
        NodeType = responseModel.MediaType!.Equals("Folder") ? Constants.ImagePicker.Folder : Constants.ImagePicker.Image,
        Url = string.IsNullOrWhiteSpace(responseModel.Url) ? "No URL" : CombineUris(umbracoSettings.BaseUrl, responseModel.Url),
        Id = responseModel.Id?.ToString() ?? "No ID",
        ParentId = parentId,
        Icon = responseModel.MediaType!.Equals("Folder") ? "icon-folder" : "icon-picture",
        ChildrenCount = null
    };

    /// <summary>
    /// Gives a default image when the request image could not be found
    /// </summary>
    private static Content ImageNotFound()
    {
        return new Content
        {
            Id = "image-not-found",
            Name = "image-not-found.png",
            Url = "ImageNotFoundImageURL",
            NodeType = Constants.ImagePicker.Image
        };
    }
    
    /// <summary>
    /// Combines two URIs into one making sure there is only one '/' between them
    /// </summary>
    private static string CombineUris(string uri1, string uri2)
    {
        uri1 = uri1.TrimEnd('/');
        uri2 = uri2.TrimStart('/');
        return $"{uri1}/{uri2}";
    }
}
```

{% endcode %}

Each method is straightforward: Get the necessary data from Umbraco using the `UmbracoClient`, map the data to fit Ucommerce, and return the result.

{% hint style="info" %}
Note that the `ChildrenCount` is set to `null`. This is because the Umbraco Delivery API does not contain any information on the number of children for a given node. See [Media](/readme/miscellaneous/media) for more details on what that means.
{% endhint %}

## Tying everything together

The last step needed is to set up `UmbracoClient` and `UmbracoImageService` in the Ucommerce project. Because the Umbraco Delivery API does not contain any information on the base URL, we'll have to set that up manually. As you might have noticed, we also need the base URL when mapping the URL as Umbraco returns only relative URLs.

### UmbracoSettings.cs

The settings object is a single line of code:

```csharp
public record UmbracoSettings(string BaseUrl);
```

### UmbracoModuleExtensions.cs

To make the integration reusable, we suggest using an extension method for every module:

{% code overflow="wrap" lineNumbers="true" %}

```csharp
public static class UmbracoModuleExtensions
{
    public static IUcommerceBuilder AddUmbracoIntegration(
        this IUcommerceBuilder builder, 
        IConfiguration configuration)
    {
        
        var umbracoSettings = configuration.GetRequiredSection("Umbraco").Get<UmbracoSettings>();
        if(umbracoSettings is null)
        {
            throw new InvalidOperationException("Umbraco settings are missing");
        }
        builder.Services.AddSingleton(umbracoSettings);
        builder.Services.AddSingleton(new UmbracoClient(
            new HttpClientRequestAdapter(new AnonymousAuthenticationProvider())
            {
                BaseUrl = umbracoSettings.BaseUrl
            }));
        builder.Services.AddUnique<IImageService, UmbracoImageService>();
        return builder;
    }
}
```

{% endcode %}

{% hint style="warning" %}
Remember to use a different authentication provider when your Umbraco instance is not publicly accessible.
{% endhint %}

For the configuration to work, add the base URL to the following path in *appsettings.json*: `Umbraco:BaseUrl`:

```json
"Umbraco":
{
  "BaseUrl": "https://localhost:{yourUmbracoPortNumber}"
}
```

The final step is to call `AddUmbracoIntegration` in *Program.cs*:

{% code overflow="wrap" lineNumbers="true" %}

```csharp
// Set up services
builder.Services.AddUcommerce(builder.Configuration)
    ...
    .AddUmbracoIntegration(builder.Configuration)
    .Build();
```

{% endcode %}

**Congratulations! Ucommerce is now able to get images from Umbraco!**

## See it in action

Now, try taking the Ucommerce project for a spin and select an image for a product.


# App Slices

{% embed url="<https://www.youtube.com/watch?v=U_ERzcWZQJE&t=569s>" %}

App Slices enable content editors to easily leverage data from Ucommerce in their composable stack. App Slices are native user experiences that can help you easily integrate your composable platform backends with Ucommerce. App Slices help you build integrated e-commerce solutions where your users do not need to switch between different systems to get the job done.

Ucommerce is built for composable solutions and therefore needs to easily integrate with other systems in your composable stack. The app slice takes care of authentication and communication with the Ucommerce backend. You just need to integrate it with your CMS or website backend. To make this easy, we use [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) which are reusable HTML components that can be integrated into any website.

### Solution overview

No two e-commerce solutions are the same, and Ucommerce does not limit you to a specific CMS or framework for your store. Therefore, we have chosen to create app slices as web components that you, as a developer, can integrate into whatever framework or CMS you choose for your store content management.

### Available App Slices

We are committed to continually adding more App Slices and currently have the following available:

* [Product Picker](/readme/integrations/app-slices/product-picker)


# Product Picker

The product picker app slice helps you easily integrate product data into your content.

<figure><img src="/files/txUKfdCCyRfDIWGUX3OX" alt=""><figcaption></figcaption></figure>

The app slice consists of a [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) that communicates with a private [GraphQL API](https://graphql.org/) on top of the Ucommerce product [search index](/readme/search-and-indexing/searching).

### Getting started

The following four tasks are needed to integrate the product picker into your solution.

* [Set up Ucommerce](#set-p-ucommerce)
* [Integrate with Content Management System](#integrate-with-content-management-system)
* [Create product data endpoint in Ucommerce](#create-product-data-api-in-ucommerce)
* [Fetch and add data to your pages](#fetch-and-add-data-to-pages)

#### Set up Ucommerce

We have a few prerequisites for the app slice to work on the Ucommerce side.

* Activate hosting of custom component files
* Add GraphQL API

The hosting of app slices uses the same infrastructure as custom UI components, which means you need to activate the [custom component server](/readme/extensions/extend-the-backoffice/custom-ui-components#hosting-components-in-ucommerce).

The Ucommerce GraphQL API used by the app slice is released in the [Ucommerce.Web.WebSite.GraphQL](https://www.nuget.org/packages/Ucommerce.Web.WebSite.GraphQL) package. Install this package and set it up in your `program.cs` file in the following way:

````csharp
```
using Ucommerce.Web.WebSite.GraphQL.DependencyInjection;
...
builder.Services.AddUcommerce(builder.Configuration)
...
    .AddGraphQLApi()
...

var app = builder.Build();
app.UseUcommerce()
...
    .UseGraphQLApi();
```
````

#### Integrate with Content Management System

Load the web component into your content management system by importing the app slice javascript file into your page. Ucommerce hosts the web component on the following URL, which you can reference on runtime:

```html
<script type="module" src="http(s)://[your Ucommerce host]/CustomComponents/widgets/ucommerce-product-picker.js"></script>
```

Then you can use the new HTML tag on your page.

```html
<ucommerce-product-picker
    base-api-url="http(s)://[your Ucommerce host]"
    client-id="Your headless clienId"
    client-secret="Your headless client secret"
    culture-code="The culture code for the language to query"
></ucommerce-product-picker>
```

{% hint style="danger" %}
Notice that the credentials are available in the HTML so this should not be used in publicly available parts of your website.
{% endhint %}

The `ucommerce-product-picker` element will emit an event `product-selection`, with the IDs of the selected products, when the user clicks the *add* button. These IDs can then be saved and used for fetching all the product data from the API built in the next step.

{% hint style="info" %}
We have a short [Umbraco custom property editor example](https://github.com/Ucommercenet/ucommerce.examples/tree/main/App%20Slices/Product%20Picker/Umbraco) that you can use for inspiration.
{% endhint %}

#### Create product data endpoint in Ucommerce

You will need an endpoint in Ucommerce to fetch the product data when your store generates the page with the picked products.

A solution could be to create a controller extending the `HeadlessControllerBase` class using the [Search APIs](/readme/search-and-indexing/searching) to get your product data. See [custom headless APIs](/readme/headless/custom-headless-apis) for more details.

#### Fetch and add data to your pages

The last piece in the puzzle is to generate your page using the product data from your endpoint using the product IDs returned by the picker.

{% hint style="info" %}
We recommend fetching the product data at this time, to ensure that the product information is always up to date. If prices, descriptions, etc. change in Ucommerce, you would want the information to flow to the website without delay.
{% endhint %}


# Release Notes

## Ucommerce 10.13.0

**Release Date:** 2026-06-18

**Important Changes**

This release upgrades Ucommerce to .NET 10. All Ucommerce assemblies now target net10.0, and ASP.NET Core, Entity Framework Core, and the Microsoft.Extensions libraries have moved to their 10.x releases. Host applications must be updated to the .NET 10 runtime and built with the .NET 10 SDK. As part of this move, .NET 10 enforces dependency-injection scope validation more strictly; custom extensions that resolve scoped services from a singleton or root-provider context may now surface scope-validation errors that previously went unnoticed and should be reviewed.

The object-mapping layer has been migrated from AutoMapper to Mapperly. Mapping is now performed by compile-time, source-generated mappers rather than AutoMapper's runtime reflection. The AutoMapper.Extensions.Microsoft.DependencyInjection dependency has been removed and is no longer available to host applications through Ucommerce. This is a breaking change for any solution that extended Ucommerce with custom AutoMapper mappings — for example, registering custom Profile classes against the Ucommerce mapper, or injecting AutoMapper's IMapper into custom services. Such code will no longer compile or resolve and must be reworked. Custom mappings should be reimplemented as your own mapper (Mapperly or otherwise); Ucommerce no longer exposes a shared AutoMapper configuration to contribute profiles to.

Microsoft.Data.SqlClient has been upgraded to the 6.x line, which applies stricter TLS and server-certificate validation by default. Review your SQL Server connection strings as part of upgrading — connections to servers without a certificate trusted by the host may need Encrypt and TrustServerCertificate set explicitly to connect successfully.

**Improvements**

Mapping is now resolved at compile time via source generators, removing the runtime reflection and configuration-scanning previously performed by AutoMapper.

Dependencies have been updated to address security advisories.

**Bug fixes**

VAT/tax values were not populated correctly on the storefront (WebSite V1) cart and order responses. A partial mapping was overwriting the full mapping, leaving BasketViewModel.Vat always 0 and dropping order Vat/VatRate and shipment Id/ShipmentPriceTotal values. The mappings have been corrected so these values are returned as expected.

## Ucommerce 10.12.13

**Release Date:** 2026-06-15

**Bug fixes**

* Fix for a bug in the uCommerce back office UI when filtering by a custom date range (most visible on the Orders list). Applying a Custom date-range filter when no date range had previously been set caused an error notification to appear and the filter not to be applied. The cause was that the date picker assumed the selected range value was always an array; when no end date had been chosen the value was null, and indexing into it threw an error. The problem was intermittent as selecting a predefined range first (e.g. "Last 7 days") and then switching to Custom worked, because an end date was already present. Custom date-range filtering now works correctly even when no previous filter has been applied.

## Ucommerce 10.12.12

**Release Date:** 2026-05-19

**Important Changes**

This Release targets the `Ucommerce.Payments.Stripe` package, addressing bugfixes and routine security updates and a Stripe SDK Version upgrade.

It also includes a fix to `Ucommerce.Web.BackOffice` to allow the capturing of payments that are in `Authorized` State to become `Acquired` when the order is moved into `Completed` status and the providers Capture method is called by the pipeline.

**Improvements**

* The Stripe.net SDK dependency has been updated to the latest version.
* Dependencies have been updated to address security advisories.

**Bug fixes**

* The Stripe payment provider was using an incorrect definition lookup when building the checkout form, which caused the success URL and payment method configuration to not be resolved correctly. This has been fixed.
* When processing Stripe webhooks, the provider was not correctly distinguishing between payments configured for manual capture and those using automatic capture. This could cause the payment status to be set incorrectly after checkout completion. This has been fixed.
* When moving an order to Completed in the back office, manually-authorised Stripe payments were not being captured correctly, resulting in an error. The order status pipeline now correctly triggers capture for manual payment authorisations.
* Stripe webhooks for event types that the payment provider implementation does not handle (such as `payment_intent.created`) were previously returning a non-2xx response, causing Stripe to retry delivery of the event for up to three days. The provider now correctly returns 200 for unhandled event types.
* A potential race condition where two threads could call `.ToFacets()` simultaneously and cause inconsistent response data has been fixed.

## Ucommerce 10.12.11

**Release Date:** 2025-10-13

**Important Changes**

The default product index model field `RelatedProductIds` has been replaced by `RelatedProductsByRelationType` . This new field is a dictionary of relation type guids as keys, and a list of product guids as values.

* This will help getting specific product relations of specific types, instead of just all related products. To mimic the old field, simply take all values within the dictionary and ignore the keys.

**Improvements**

* When sorting by field on the product table, it is now sorted server-side instead of on-page. Meaning it now works with pagination.
* In the payment provider base class, the method `AddOrderStatusAuditMessage` is now virtual, allowing it to be overwritten in each payment provider implementation

## Ucommerce 10.12.10

**Release Date:** 2025-08-22

**Improvements**

* When entering the details page of a variant product, from the parent product, the page now starts scrolled to the top.
* The product display name is no longer required. The name will fallback to the product name in case of no display name.

**Bug fixes**

* When editing entities as a logged in user, the audit username is now set correctly.
* When updating definition fields on entities, the audit for that entity is now updated correctly.
* When creating a quick search, the soft deleted definitions will no longer show up.
* When creating a quick search, searching through definitions now works correctly.
* The store field is now returned in the response from GET order via the headless API, to correctly reflect [the documentation page](/readme/headless/reference/orders).

## Ucommerce 10.12.9

**Release Date:** 2025-07-08

**Improvements**

* When implementing an authentication provider, there now is a new backoffice security setting `SetCookieExpirationTime` to determine how often the user cookie should expire
  * The new default is 4 hours, up from 1 hour.
  * When the user cookie expires, the user is now prompted to reload the page, instead of getting redirect errors.
* It is now possible to select the text from the order logbook entries
* The order logbook entries are now sorted on entry ID as a secondary, in the cases where the **created on** values is the same

## Ucommerce 10.12.8-hotfix6

**Release Date:** 2025-06-10

**Important Changes**

* The price group IDs in the price index now consistently include all valid price groups associated with a price, even those that have a separate price set for the same product.
  * This change was implemented to address previous inconsistencies, where only price groups containing prices for the same product within the same index batch were filtered out.

**Bug fixes**

* Resolved an issue with indexing price entities when they are excluded from the product index.
* Fixed a problem that occurred when indexing a product after adding a new price.
* Updated the latest migration script for adding languages to the language table to correctly handle duplicate culture codes originating from the country table.

## Ucommerce 10.12.7

**Release Date:** 2025-05-20

**Breaking**

* **Core**: `CountryEntity` no longer contains information about languages/culture code (the `Culture` property). Instead, this information is controlled by a new entity called `LanguageEntity` backed by the new table `uCommerce_Language`. If your code is using the `ILanguageService` interface, this will not impact your solution, but custom code referencing `CountryEntity` might need to be updated.

**Improvements**

* **Indexing**: Index search models in batches to improve performance and reduce the memory footprint. Example run with 131,000 prices and 60,000 products went from about 12 minutes using up to 1.5GB memory to about 4 minutes using up to 500Mb memory for a complete scratch-index on a Mac M2 Pro with 32GB.
* **Indexing**: You can now adjust the chunk size and max parallelism used when writing documents to Elasticsearch. See [Configuration](/readme/search-and-indexing/configuration) for more details.

**Bug fixes**

* Fixed issue with displaying images and thumbnails on the product list view in backoffice.

## Ucommerce 10.12.6

**Release Date:** 2025-04-25

**Improvements**

* **Indexing**: Enable case-insensitive searching for wildcard searches:\
  `productsSearchable = productsSearchable`\
  `.Where(x => x.DisplayName == Match.Wildcard($"*{request.SearchTerm}*", true));`\
  \&#xNAN;*NB: This is only available for wildcard searches.*

**Bug fixes**

* Fixed issue with decimal numbers in promotions. This was caused by `decimal.Parse()` using the machine's default culture info, which does not always align with the culture used to save definition field values.\
  New extension methods are available whenever you need to convert a definition field value to a number type (e.g. `decimal`). See details here: [Converting (string) field values to numbers](/readme/definitions/what-is-a-definition#converting-string-field-values-to-numbers).

## Ucommerce 10.12.5

**Release Date:** 2025-04-04

**Improvements**

* **Performance**: Improved the performance when adding an item to the cart.

**Bug fixes**

* Fixed issue with the AddToCart pipeline not hydrating all nested properties needed by the CalculateCart pipeline called internally.

## Ucommerce 10.12.4

**Release Date:** 2025-04-01

**Bug fixes**

* Fixed issue with missing `CampaignName` and `PromotionName` on `OrderDiscountEntity` due to performance improvements made in 10.12.3.
* Fixed issue with promotions when an active promotion's criteria triggers but no discounts exists on the promotion.
* Fixed issue with getting active promotions when an active promotion's criteria triggers a free gift discount, but the gifted product is not part of the input's list of products.
* Fixed issue with getting active promotions, where the `Discounts` collections of the temporary order lines could be `null`.

## Ucommerce 10.12.3

**Release Date:** 2025-03-26

**Improvements**

* **Performance**: Improved the performance when adding an item to the cart.

**Bug fixes**

* Fixed an issue with getting active promotions, where the `OrderProperty` collection could be `null`.

## Ucommerce 10.12.2

**Release Date:** 2025-03-21

**Improvements**

* **Headless API**: The endpoint for getting active promotions related to specific products now groups the products and related promotions together.

## Ucommerce 10.12.1

**Release Date:** 2025-03-20

**Improvements**

* **Performance**: Definitions are now cached in memory to boost performance of Ucommerce. The default cache duration is 5 minutes. The value can be configured via appSettings `Ucommerce:Persistence:Caching:Duration` . To avoid cold start and cache misses, a new interface `IPreHeatCacheAction` can be used to put data in the cache before it is needed. The action will run on a schedule 5 seconds shorter than the specified duration to make sure the cache is re-populated before it expires.
* **Dependency Injection**: Ucommerce now has a `Decorate` extension method on `IServiceCollection` that allows you to decorate an existing DI registration with extra logic. Internally in Ucommerce, it is the mechanism used to decorate the `GetDefinitions` pipeline with caching using the `CachingPipelineDecorator` . Other pipelines can be cached using

  ```
  builder.Services
      .Decorate<IPipeline<XXXInput, XXXOutput>>((decorated, sp) =>
          new CachingPipelineDecorator<XXXInput, XXXOutput>(
              sp.GetRequiredService<IMemoryCache>(),
              sp.GetRequiredService<IOptionsMonitor<PersistenceOptions>>(),
              decorated));
  ```

**New Features**

* **Customer management**: You can now see, create, update and delete addresses on organizations in the backoffice. These addresses can be used to prefill a dropdown of possible addresses in the checkout flow in the store front.
* **Headless API**: New endpoint for getting active promotions related to specific products.

**Bug fixes**

* Fixed an issue with order lines not using the multilingual `DisplayName` property of the products, but the default `Name` property. Now `Name` is only used as a fallback when no `DisplayName` is set.
* Fixed an issue where discounts would sometimes disappear when adding shipping information to a cart.
* Fixed an issue where clicking on a section title (left-hand side of details pages in the backoffice) would not scroll to the selected section.
* Added missing payment information to `GetOrderViewModel` return by the `GetOrder` headless API endpoint
* Fixed an issue where changing the variant list columns would store the changes but not rendered in the UI
* Fixed missing (re-)indexing of products and categories when modifying categories from the product details page

## Ucommerce 10.12.0

**Release Date:** 2025-03-03

**Improvements**

* **Order management:** You can now define sections to group dynamic properties. Dynamic properties' sort order is used within the section and sections can be sorted on the page. This was introduced for products in [10.10.0](#ucommerce-10.10.0) but is now available for orders as well.
* **Headless API:** The performance of the promotions-flow is improved significantly when calculating a cart, while having many active promotions.
* **Product Information Management:** The quick search list is now searchable with pagination.

**Bug fixes**

* Fixed an issue with validation expressions for enum types when the enum value was `null`.
* Fixed missing order line discounts hydration when getting an order using `GetOrderInput`.
* Fixed an issue with the UI when selecting currencies for deletion.
* Fixed missing order line properties hydration when running the default checkout-flow.
* Fixed unnecessary `NotNull` constraint on `DataTypeEnumDescriptionEntity.Description`. The value can now be null.

## Ucommerce 10.11.1

**Release Date:** 2025-02-21

**Improvements**

* **Product Information Management:** It is now possible to search through the quick search list.

**Bug fixes**

* Fixed an issue where price indexing would have duplicate data if multiple batches where fetched.
* Fixed an issue where an incorrect guid would be used to fetch price definitions in payment integrations.

## Ucommerce 10.11.0

**Release Date:** 2025-02-18

**Breaking changes**

* Due to performance issues using the [price index](/readme/search-and-indexing/indexing/indexing-prices), we've changed the way prices are indexed. This means that the `PriceSearchModel` has changed significantly:
  * The property `PriceGuid` is now available simply as `Id` replacing the random value generated previously.
  * The property `PriceGroupGuid` has been renamed to `PriceGroupGuids` . It is now a list of GUIDs for all price groups the price is valid for. See [Using the Price Index](/readme/search-and-indexing/indexing/indexing-prices#using-the-price-index) for details.
  * The properties `Tax`, `TaxRate`, and `PriceInclTax` are not longer available. See [Calculating Tax](/readme/search-and-indexing/indexing/indexing-prices#calculating-tax) for details.

**New Features**

**Catalog Management:** You can now define the order of products in a category by drag and drop.

* The list of products on `CategorySearchModel` is now sorted based on the `SortOrder`
* `ProductSearchModel` now contains a new property `CategoriesWithSortOrder` containing all categories and the product's `SortOrder` in that category.
  * `CategorySearchModel` list of products is now sorted by default by this property.

**Improvements**

* **Product Information Management:** Sorting product lists in the backoffice now supports sorting on multilingual fields.
* **Order Management**: The filter can now be saved for later visits.
* **Core:** Ucommerce no longer log sensitive data and EF Core queries by default. See [Logging](/readme/miscellaneous/logging) for details.
* **Headless API**: A new endpoint has been added to get order details from a payment GUID. See [Orders](/readme/headless/reference/orders#get-order-by-payment-guid) for details.
* **Headless API**: The `CreatePayment` and `Checkout` pipelines now accept Guid for entities on input. This change has also been released as a patch for 10.9 (10.9.6) previously.
* **Headless API**: Improved cart calculation performance for shops with many active campaigns.

**Bug fixes**

* Fixed a LINQ error that happened when some entities were saved.
* Product lists in the backoffice returned status code 500 when sorting on some fields.
* Fixed issue where, when in the root folder of a media picker, it's possible to click *back* infinitely.
* Fixed issue where creating a payment would try to save the payment method.
* Fixed issue creating new sections for the backoffice.

## Ucommerce 10.10.0

**Release Date:** 2025-01-23

**Breaking changes**

* As part of finally fixing the issues with definition inheritance, the pipelines to get one or more definitions now returns an immutable copy of the definition-system related entities (.e.g. `DefinitionEntity`, `DefinitionFieldEntity`and `DataTypeEntity` ). This means that entity properties (e.g. `ProductProperty`, `PaymentMethodProperty` and `EntityProperty`) can no longer have a direct reference to the `DefinitionFieldEntity` it relates to. Instead they now have a `DefinitionFieldGuid` uniquely identifying the field it relates to.

**New Features**

* **Product Information Management:** You can now create, update, delete, and use product quick searches. Quick searches means you can now search for any dynamic property value, not just name, sku, etc. Quick searches can be private or shared with everyone.
* **Product Information Management:** You can now define sections to group dynamic properties. Dynamic properties' sort order is used within the section and sections can be sorted on the page.
* **Product Information Management:** You can now toggle variants on in the product list. This is especially useful when using a quick search that targets variant properties, but can also be used for regular searches.

**Improvements**

* The `AddressEntity` was not auditable. This means you can now inspect the `CreatedBy`, `CreatedOn`, `ModifiedBy`, and `ModifiedOn` to see who created or modified an address.
* The `OrderEntity` is now added to the `OrderProcessingOutput` used in all order processing pipelines. This makes it easier for developers to follow our best-practices of treating the input as immutable and always use the output when making modifications to objects.
* Various UI improvements.
* Removed dependency on the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) package

**Bug fixes**

* Fixed an issue with boolean fields in the Organization area not always being able to save when updated.
* Fixed an issue with updating fields in the Organization area when the new value is empty.
* Fixed wrong `RenderInEditor` setting for `DisplayName` in products app.
* Fixed issue where adding an image to a product would add it twice in the backend.
* Fixed an issue where undoing changes to phone number would not always be saved.
* Fixed an issue with the decimal separator for the number editor. It now aligns with the currency editor using "." (dot) as the separator.
* Fixed an issue in the Orders app, where the filters tab would sometimes show an incorrect number of filters set when reloading the page.
* Fixed an issue where the `LastModifiedMiddleware` for definitions would log exceptions for some definition types.
* Fixed an issue where the `AddToCart` pipeline would always add a new line when called using the Product`Guid`. It now respects the `AddToExistingOrderLine` flag on `AddToCartInput`.
* Fixed an issue where the tax rate from the price group was not used as a fallback when the tax rate on the input was not set.
* Fixed an issue where `AddToCart` with a negative quantity could be used to remove the order line. Use `DeleteCartOrderLine` pipeline instead.
* Fixed an issue where recalculation of the cart total etc. was not triggered when the cart was emptied.
* Dynamic properties are now correctly initialized with the default value set up for the field.
* Deleting a product family now correctly deletes the variant products related to the family.

## Ucommerce 10.9.6

**Release Date:** 2025-01-30

**Improvements**

* The `CreatePayment` and `Checkout` pipelines now accept Guid for entities on input.

## Ucommerce 10.9.5

**Release Date:** 2025-01-10

**Bug fixes**

* The content service now works correctly.
  * [Check out this new doc, for more information on how to implement your own content service.](/readme/extensions/change-service-behavior/content)
* Fixed an issue where updating non-multilingual definition field properties created duplicate properties.

## Ucommerce 10.9.4

**Release Date:** 2025-01-06

**Bug Fixes**

* Fixed issue where facets on product variant definition fields of type `IEnumerable<string>` would throw an exception when fetching facets from the product index.
* Fixed issue with *Percent off order lines* discount being added multiple times to the list of discounts on order lines when the quantity changes.

## Ucommerce 10.9.3

**Release Date:** 2024-12-19

**Bug Fixes**

* Resolved a critical issue with definition inheritance that could inadvertently destroy the definition inheritance.

  **Affected Versions:** All releases from **10.7.0** to **10.9.2**. Use of these versions is **strongly** discouraged if any definition inheritance is utilized.\
  **Please note**: If you have used any of the affected versions, the relationships between your definitions and definition fields may have been updated incorrectly if using inheritance.

  * A **10.8.2** version is also available with this same fix applied.

## Ucommerce 10.9.2

**Release Date:** 2024-12-12

**Improvements**

* The apply and remove promotion code, headless endpoints can now return a mini cart view to save you from requesting the cart again after applying or removing a promotion code.\
  Add the query parameter `views=miniCart` to your request to get the mini cart.

**Bug Fixes**

* It is now required to select cart/order definition on a store, as well as customer definition.
  * These define the definitions used, when one of these entities are created automatically through checkout.
* Fixed a bug where the validation of address entities required first and last names, which did not match the entity description and the database schema. First and last names are no longer required by the validator.
* We now properly display audit information for customer center entities.
* Corrected display of floating point numbers in the backoffice
  * Currently, we only support using '.' as a decimal separator. We plan to also enforce this in the **Number Editor** in a future release.
* Indexing of floating point numbers in search has been corrected
  * Related to the fix above.
* Various UI bug fixes

## Ucommerce 10.9.1

**Improvements**

* When using the [price index](/readme/search-and-indexing/indexing/indexing-prices), the source price group of a price is now included. This is useful when working with derived price groups.\
  Example: *Base prices* is a price group containing all list prices and *Customer A* is a customer-specific price group, derived from *Base prices*, containing discounted prices for only some products. Previously, all prices derived from *Base prices* would only contain the `ProductGuid` as well as the `PriceGroupGuid` of the *Customer A* price group. Now, the price will also have `SourcePriceGroupGuid` containing the `PriceGroupGuid` of *Base prices*, making it easy to determine the origin of a price.

**Bug Fixes**

* Fixed a bug where a price would only be indexed for a single price group even if it was derived from a base price group into multiple derived price groups.
* Fixed styling issue on the products list view.
* Fixed a bug in a migration script that prevented definition fields with the same name as a previous, soft-deleted, field to exist.
* Fixed a bug in a migration script so that it deletes the right duplicate definition field when migrating from Ucommerce Classic.
* Order lines are now correctly associated with the discounts applied to them.
* Non-multilingual fields no longer saves the culture code, which prevented the value from showing up when using another culture code.
* Product families are now displayed when selected as a criteria for a promotion. Previously product families could be selected but would never show on the list of selected criteria.

## Ucommerce 10.9.0

**Release Date:** 2024-12-02

**Breaking changes**

* We have added a unique index on a definition's field names. This means that fields on the same definition can no longer have the same name. If you have such an issue in your data, you might encounter the error:\
  `Ucommerce database migration failed with message The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.uCommerce_DefinitionField' and the index name 'UX_uCommerce_DefinitionField_DefinitionId_Name'. The duplicate key value is (1, Organization). The statement has been terminated., in script 117 - Create missing unique indexes on definition fields`\
  See [common database issues](https://dev.ucommerce.net/pages/S5EHKHyZ7ul86xBIq2E0#id-10.9.1-duplicate-definition-field-names-on-definitions) for more details.<br>

**New Features**

* New *Customer groups* area in the *Customer Center* app. This allows you to see, update and create new customer groups as well as adding and removing customers from a group.
* New customer-related criteria.
  * New criteria are available for promotions and price groups. These will allow for targeting specific customer groups or organizations.
* New customer group and organization picker modals are available.
  * These modals are now available through the definitions system. To use them anywhere, set your definition field's data type to be any of the two pickers.

**Improvements**

* `Content.ChildrenCount` is now nullable to support integration with systems that do not supply it, e.g. Umbraco Delivery API. When `ChildrenCount` is zero, you can't navigate to the folder, otherwise, the navigation arrow will show.
* Improved *Catalogs* app so that it handles 100.000+ price groups gracefully.
* Scratch indexing now uses the [DistributedLock.SqlServer](https://www.nuget.org/packages/DistributedLock.SqlServer) implementation as the distributed lock by default. The `NullDistributedLockProvider`, which simply logs an error, has now been deprecated.

**Bug Fixes**

* Updating and deleting a catalog now updates the category index correctly.
* *The Catalogs* app now displays the new value when changing the name of a price group.
* Promo codes no longer disappear in the UI when going to the *unique* tab of the promo code generation dialog.
* The Category picker in promotion criteria no longer scrolls indefinitely.
* Adding, removing, or changing definitions fields through EF Core now correctly updates the definition's `ModifiedOn` property meaning it will invalidate the cache and reload the definition in the backoffice UI.
* `IEnumerable<string>` now works as expected in index definitions.
* Creating a catalog now also sets the *Allowed price groups*.
* Referencing a (soft-)deleted definition on an entity no longer breaks the backoffice UI.
* Fixed a filtering issue when adding a product to an order in the backoffice UI.
* Fixed an issue with selecting a price group for a payment method and then de-selecting it.
* Custom properties on a product family no longer show up on variants.
* Some error messages were not shown when creating customers and organizations

## Ucommerce 10.8.1

**Release Date:** 2024-11-18

**Bug Fixes**

* Fixed an issue where ElasticSearch would sometimes attempt to access an internal method from a Microsoft library.

## Ucommerce 10.8.0

**Release Date:** 2024-11-04

**New Features**

* A whole new *Customer center* app in the back office. This app contains two areas: *Customers* and *Organizations*. *Customers* allow you to see, update, and create new customers while *Organizations* allow you to see, update, and create new organizations as well as adding and removing customers from an organization.

**Performance Improvements**

* Improved performance of orders and promotions apps.
* Improved search indexing speed.
* Improved performance and usability of the Products list.

**Bug Fixes**

* Headless Access and Refresh Token duration settings are now respected.
* Headless Access and Refresh token clock skew default value changed to 30 seconds instead of the default 5 minutes set by Microsoft. The value can be configured via appSettings (`Ucommerce:Website:Headless:AccessTokenClockSkew`).
* Fixed an issue where a null valued definition in the database could break the pages.
* Fixed an issue where missing database indexes could cause migration errors.

## Ucommerce 10.7.1

**Release Date:** 2024-10-16

**Performance Improvements**

* Improved performance of definition caching.
* Improved performance of store, catalog and product apps.

**Bug Fixes**

* Fixed an issue where a selected product definition would show as *removed* when clicking *change definition.*
* Fixed an issue with retrieving a cart after adding shipping information.
* Fixed an issue with definition inheritance with more than one parent definition.
* Fixed nullability for promotion name in the database. A default value is set if the name is null when upgrading.
* Fixed an issue with licensing when Visual Studio starts sending request(s) before the application has fully initialized the license.
* Fixed an issue with writing license information to the database when license is not valid.

## Ucommerce 10.7.0

**Release Date:** 2024-10-04

**Breaking changes**

* **Definitions**: (Re-)enablement of definition inheritance has resulted in a major rewrite of how entities works with their definitions to make sure that the inheritance is always considered. This means that the `Definition` property is no longer available on definition-based entities. Instead use the new `DefinitionGuid` property to look up the definition using the new *get definition pipeline* (`IPipeline<GetDefinitionInput, GetDefinitionOutput>`).

**New Features**

* **Product Picker:** New Product Picker Widget which can be used inside your CMS.
* **Prices:** Prices can now be indexed in their own index to avoid transferring large amounts of superfluous prices from the product index. See [Indexing Prices](/readme/search-and-indexing/indexing/indexing-prices) for more details.
* **Products:** You can now add a product to any categories from inside the products app.
* **Carts & Orders:** Carts and Orders are now definition based, which means that it's possible to create your own definitions with custom fields now. The definition is set on a per store basis.
* **Discounts:** You can create your own discount types with the definition system now.

**Bug Fixes**

* Fixed sizing of price groups container in settings.
* Added payment reference id to the payment section of an order.
* Fixed an issue where it was not possible to have categories with the same name across multiple stores.
* Fixed performance issue when retrieving a cart through the transactions API because of a missing index.
* Fixed an issue where a selected definition would show as *removed* when clicking *change definition.*

## Ucommerce 10.6.0

**Release Date:** 2024-08-30

**Breaking changes**

* `Ucommerce.Web.Core.Constants.CriteriaTypeConstants` has been renamed to `Ucommerce.Web.Core.Constants.PromotionCriteriaTypeConstants` to make it clearly distinct from the new price group criteria constants.

**New Features**

* **Price Groups:** Price groups can now have criteria, allowing for more flexible control over access to specific price groups. Learn more about how to [use price group criteria](/readme/miscellaneous/price-group-criteria) and [create custom price group criteria](/readme/extensions/custom-price-group-criteria).
  * ***Note:** Price groups can now be enabled or disabled. All existing price groups will be enabled when upgrading. By default, new price groups will be disabled when created.*
* **Shipping Methods:** Shipping methods are now definition-based, providing enhanced customization options.

**Bug Fixes**

* **Promotion Criteria:** Fixed an issue that prevented updates to various definition-based promotion criteria. Data validation now works correctly.
* **Catalog Definition Update:** Resolved an issue preventing updates to catalog definitions.
* **Product Categorization:** Fixed a bug that blocked certain products from being added to categories.
* **Price Group Tax Rates:** Corrected an issue where creating a price group with a 0% tax rate was not possible. The backoffice now also provides a clearer error message when updating tax rates.
* **Backoffice Display:** Fixed a visual bug in the backoffice that displayed "(removed)" after every selected definition name.
* **Cart Custom Pricing:** Addressed an issue where a null `priceGroup` value was used when adding an item with a custom price to the cart.
* **Order Line Properties:** Fixed a bug that caused an exception when deleting an order line property from a cart with multiple order lines.
* **Cart Shipment Updates:** Resolved an issue where updating or deleting order lines on a cart with shipment information could throw an exception.

## Ucommerce 10.5.3

**Release Date:** 2024-08-20

#### Bug Fixes

* Fixed a bug where it was not possible to add a product to the cart after adding shipping information.

## Ucommerce 10.5.2

**Release Date:** 2024-08-05

#### Bug Fixes

* Fixed a bug where it was not possible to set a custom price on a product.

## Ucommerce 10.5.1

**Release Date:** 2024-07-30

#### Breaking Changes

* **Property Splitting Character Alignment:** The character used for splitting properties in the database has been standardized across Ucommerce. All splitting will now be done using the `;` or `|` characters. Adjustments will be necessary if you have custom logic related to these properties. Most changes involve replacing `,` with `;`.
* **API Controller Request Model Updates:** Most backoffice API controllers have had their request models updated. All parameters that refer to a GUID are now correctly named "Guid" instead of "Id" in property names. This change may impact your project if you use the backoffice controllers.

#### Improvements

* **Grid View Enhancement:** The new grid view for the price group list now retains your column reordering preferences.
* **Improved User Information Formatting:** The "modified by" and "created by" information in sub-menus has been reformatted for more clarity.
* **Enhanced Order Validation Messages:** Clearer messages are now provided when a user is not allowed to create an order in a store or when trying to add an invalid order line.

#### Bug Fixes

* **Currency Update Fix:** Resolved an issue where updating the currency on a base price group would not propagate to child price groups.
* **Catalog Criterion Dropdown Error:** Fixed an error that could occur with the catalog criterion dropdown.
* **Currency ISO Code Validation:** Addressed an unnecessary validation of the currency ISO code in the backoffice.
* **Price Group Update Issue:** Corrected an issue where the price group in the products list would not update properly.
* **Migration Table Name Length Issue:** Fixed a migration issue where the old `uCommerce_CategoryTarget` table could contain names that exceeded the allowed length in the new table, ensuring compatibility for all name lengths.
* **Custom Category Picker Registration:** Fixed an issue where a custom category picker could not be correctly registered via the definitions system.

## Ucommerce 10.5.0

**Release Date:** 2024-07-08

**Breaking Changes**

* Removed the dependency on the [Microsoft.AspNetCore.Mvc.NewtonsoftJson](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson) package.

**Features**

* **Custom Definition-Based UI Editors**: Added support for creating custom editors for the Ucommerce backoffice. [Learn how to create your own editor](/readme/extensions/custom-editor-ui).
* **Price Group Inheritance**: When creating a new price group, selecting a base price group is now possible. [Learn more about price group inheritance](/readme/miscellaneous/price-group-inheritance).
* **Price Group List UI Changes**: The price group list view was redesigned.
  * The new design makes use of pages instead of infinite scroll, which could cause performance issues in some cases.

**Improvements**

* Now using the built-in System.Text.Json for (de-)serialization instead of Newtonsoft.Json

**Bug Fixes**

* Resolved an issue where deleting a product relation could result in an error.

**Notes**

* The [data importer tool](/readme/data-import), introduced in version 10.3.0, has been updated to support price group inheritance. The import model now includes a new property for defining a base price group.

## Ucommerce 10.4.0

**Release Date:** 2024-06-18

**Features**

* **Definitions-Based Promotion Criteria:** Promotion criteria are now definitions-based, allowing for easier extension of criteria. For more information, [Discover how to set up definitions for promotions](https://dev.ucommerce.net/ucommerce-next-gen/extensions/extending-criteria).

**Bug Fixes**

* **Logbook Entry for Cart:** Resolved an issue where adding a logbook entry for a cart was not possible.
* **Order Number Series:** Order number series on the store is now a required field and will not fail due to undo actions.
* **UI Flickering:** Fixed an issue causing the UI to flicker when changing values on a promotion.
* **Navigation Header:** Addressed a problem where the left-side header navigation was unresponsive on the product page.

## Ucommerce 10.3.0

**Release Date:** 2024-06-07

**Breaking changes**

* The connection string setting has been moved from `Ucommerce:ConnectionStrings:UcommerceConnectionString` to `Ucommerce:Persistence:ConnectionString` to consolidate all persistence options in one place.
  * Additionally, the `Ucommerce:Persistence:ConnectionTimeout` option is now properly utilized to set the maximum duration (in seconds) for a database query before it times out. This improvement is particularly beneficial when migrating large databases from version 9.7 to 10.

**Features**

* **Prices Bulk Editing:** The "Price Groups" section in settings has been renamed to "Prices". This area still includes price groups and price group editing, but now also allows for bulk editing of prices within each price group. This change aims to simplify the use of price groups and streamline the mass editing of prices.
* **Introducing the new data importer tool!** This tool allows you to seamlessly import products, prices, and more into Ucommerce from external sources. For more details, [see Data Import](/readme/data-import).

  *Note: This is the initial release of the tool. Future updates and improvements are planned.*

#### Bug Fixes

* **Order Page Optimization:** Fixed an issue causing the orders page to timeout when too many orders were loaded. The fetching process has been optimized for speed, and the timeout limit has been increased.
* **Error Message Display:** Error messages now correctly appear in the back office when an error occurs during the creation of a price group.
* **Pipeline Task Exception Handling:** An exception is now thrown if you attempt to add a pipeline task before or after itself, unless the instances have different IDs. Previously, this operation was allowed but produced unexpected results.
* **Database Migration Debugging:** During database migrations, the inner exception is now surfaced, making debugging easier.
* **Product Page Price Groups:** Fixed an issue where the product page would only load a maximum of 12 price groups. *Note: We are aware of another potential issue affecting this area and will address it in the next release.*
* **Free Gift Reward Editor:** The loading indicator in the Free Gift reward editor no longer displays indefinitely.
* **Promo Code Download:** It is now possible to download a list of promo codes again.
* **Cart default data:** The default carts had invalid data, causing errors. This has been adjusted.

## Ucommerce 10.2.1

**Release Date:** 2024-05-03

**Bug Fixes**

* (Migrations) Added safeguards to migration logic that could break migrations in rare cases.

## Ucommerce 10.2.0

**Release Date:** 2024-04-25

**Breaking changes**

* (Pipelines) Pipelines have been improved. If you have any custom pipeline tasks or are executing any pipelines, you will need to change your code.
  * `IsSuccessful` has been deprecated. It is replaced by `EnsureSuccess`. We believe that the new name better reflects the fact that a `PipelineException` is thrown if any errors occur during the execution of the pipeline. Also, in case `EnsureSuccess` does not throw a `PipelineException`, it returns the output from the pipeline execution.
  * `PipelineTaskArgs` has been removed. `PipelineContext` now contains the input and output instead. The `PipelineContext` has been made generic to support this change.\
    In addition, the `CancellationToken` has been moved out of the `PipelineContext` and is now a part of the `Execute` method signature to make your IDE recognize its existence.\
    Method signature before:

    ```
    Task Execute(PipelineTaskArgs<TInput, TOutput> subject, PipelineContext context);
    ```

    Method signature now:

    ```
    Task Execute(PipelineContext<TInput, TOutput> context, CancellationToken cancellationToken);
    ```
  * Executing a pipeline no longer requires an output or `PipelineTaskArgs` object. To run a pipeline, you only need to provide the input. When the pipeline has executed, we recommend calling `EnsureSuccess` on the `PipelineExecutionResponse`, to make sure no errors occured and get the output from the pipeline.

**Features**

* (UI extensibility) It is now possible to inject custom web components into the Ucommerce backoffice. [Learn more about Custom UI components](/readme/extensions/extend-the-backoffice/custom-ui-components).

**Bug Fixes**

* (Indexing) The product indexer now works correctly when indexing products outside of categories.
* (Pipelines) The checkout and order processing pipelines are now discoverable in the pipeline discoverability endpoint. [Learn more about pipelines and discoverability](/readme/how-to/discover-pipelines-and-their-tasks).

## Ucommerce 10.1.2

**Release Date:** 2024-04-09

**Bug Fixes**

* (Licensing) The application will no longer shut down end of June even if a valid license key is given

## Ucommerce 10.1.1

**Release Date:** 2024-03-22

**Improvements**

* (Migration) Migration script execution speed and reliability is improved

**Bug Fixes**

* (Backoffice) Changing language will now update labels on custom fields.
* (Backoffice) Lazy loading fixed in the products app now works regardless of screen height.
* (Backoffice) The result count when searching will now show in the correct format.
* (Backoffice) After updating a product the product list will update with the correct values.
* (Search) Sorting on product unit price now sorts the result correctly. This was caused by the index definition defining the values as long instead of floating points.

## Ucommerce 10.1.0

**Release Date:** 2024-03-08

**Breaking changes**

* We improved setting up an external identity provider for our Backoffice. See [BackOffice Authentication documentation](/readme/backoffice-authentication) for details.

**Features**

* It is now possible to assign store permissions to individual users.
* A user will be created or updated when using an external identity provider. See [BackOffice Authentication documentation](/readme/backoffice-authentication) for details.
* It is now possible to rebuild the search index and/or check the status from Backoffice -> Settings -> Search - rebuild index.
* It is now possible to undo the deletion of promotions.
* It is now possible to undo the update of a criterion or discount.

**Bug Fixes**

* Undo widgets in Backoffice will disappear if the route changes.

## Ucommerce 10.0.1

**Release Date:** 2023-02-23

**Features**

* It is now easier to add and edit the checkout pipelines through the `PaymentBuilder.CheckoutPipelines` property. See [Checkout Pipelines documentation](/readme/extensions/extending-pipelines/checkout-pipelines) for details.
* It is now possible to add and edit order processing pipelines through the `PaymentBuilder.OrderProcessingPipelines` property. See [Order processing Pipelines documentation](/readme/extensions/extending-pipelines/order-processing-pipelines) for details.
* When a user searches within any searchable list in the back office, it is now possible to change the focus on search results using the arrow keys and to open a selected item by pressing the Enter key.
* A new type; `UserDefinedEnum`, was created to be used for index definitions on fields containing user-defined enums.
* Using the `UserDefinedEnum` type, multilingual user-defined enums can now display the appropriate display name while using the enum's value for queries.

**Bug Fixes**

* Order processing pipelines are now triggered correctly on order status updates.
* Fixed an issue where the selection of a catalog criteria in the promotions page caused an error.
* The product page -> Display name is now correctly set as required.
* The product page -> No longer displays the variants section for products that can not have variants.
* Product page -> Focusing and unfocusing the Long Description now only sends a PATCH request if any changes were made.
* `CatalogLibrary.GetFacets()` returns the expected facets now.
* Faceted search returns the correct products.
* Using the `UserDefinedEnum` type, multilingual user-defined enums are now displayed correctly.
* Parent products no longer index all language versions of enum variant properties.

**Notes**

* Raw search capabilities for Elasticsearch have been moved out of `Ucommerce.Extensions.Search.Abstractions`. They are now available as extension methods on `IIndex` using the namespace `Ucommerce.Search.Elastic.Extensions`.\
  \&#xNAN;*Note: The extension methods will throw an exception if the `IIndex` instance is not `ElasticsearchIndex`*
* The unused table **`uCommerce_ProductCatalogGroupTarget`** has been removed from the database. This table was used to link a promotion to a store. This is now done using the campaign.

## Ucommerce 10.0.0

**Release Date:** 2023-02-08

**Features**

* Changing the priority of promotions is now undoable.
* Deleting a campaign is now undoable.
* Adds convenience accessors to `UCommerceDBContext` for
  * Catalogs,
  * Customers,
  * PriceGroups,
  * ShippingMethods,
  * Users,
  * Campaigns,
  * Carts,
  * Orders,
  * Products,
  * Categories and
  * Stores

**Breaking changes**

* Migrated the search driver from `Elasticsearch.Net` and `NEST` `7.17.5` to `Elastic.Clients.Elasticsearch` `8.11.0`
  * There will be a need to update any existing search configurations. Please take a look at the [Configuration documentation](/readme/search-and-indexing/configuration) for more information.
* Upgraded from .Net 7 to .Net 8
* Changed signature of `GetPrices` in `IPriceCalculationService` - removing intermediate value object
* Renamed `WebSiteControllerBase` to `HeadlessControllerBase` for better readability for partner developers.
* The default back office URL moved from `/ucommerce-ui/` to `/ucommerce/`

**Bug fixes**

* Price group validation now happens before updates in headless calls - meaning a wrong price group guid will now actually block the update of the entity.
* Fixed an issue where having no stores at all would cause errors in the back office.
  * If no store exists, the sidebar tabs for catalogs, products, orders, and promotions are now hidden.
* Removed some corrupted data from the default database migration, which caused issues in the default campaign.
* Fixed an issue with payment provider callbacks not being registered correctly.

## Alpha

<details>

<summary>Alpha Releases</summary>

## Alpha 13

### Build 4021

**Breaking Changes**

* The navigation property called `Properties` of `DefinitionFieldEntity` has been removed.
* The navigation property called `ProductDescriptionProperties` of `ProductDefinitionFieldEntity` has been removed.

#### Features

* It is now possible to see users' permissions when navigating to the settings app.
* When clicking the user permissions item on the settings app, it is now possible to see a list of available users.
* The user entity has been extended with `Name` and `IsAdmin` properties. As part of the migration `Name` is set to `ExternalId`.
* The settings area of the backoffice is now only available to users with admin privileges.

The user permissions settings will be added to the backoffice in the next release.

#### Bugs

* Fixed an issue where the ElasticSearch cloud options were not set to use the correct serializer. This meant that custom-defined fields would not be added to the index as they should when they are added to the index definition.
* Fixed an issue with date pickers on custom definition fields.

#### Improvements

* Guid generation for new entity objects has been moved from an EF Core `SavingChanges`-interceptor to object creation time.
* Fewer database calls are needed for security checks.
* Added **cascade on delete** for relevant foreign key constraints in the database.
* Added `Ucommerce.Web.Core.Constants.DateFormattingConstants.DEFAULT_DATE_TIME_FORMATTING` as a constant for date formatting in Ucommerce.
* Added helper method `ToUcommerceString()` as an extension method for `DateTime` and `DateTimeOffset` to convert time to UTC and format it using `Ucommerce.Web.Core.Constants.DateFormattingConstants.DEFAULT_DATE_TIME_FORMATTING`. This aligns with how dates persisted in V9.
  * This method (or the constant above) should be used when importing dates into definitions to make sure that the dates are displayed as expected in Ucommerce.

## Alpha 12

### Build 4019

#### Features

* Search Adorners are now available. This allows you to add custom data to search models. See [Custom Data](/readme/search-and-indexing/indexing/custom-data) for more details.
* It is now possible to create, update, edit, and delete order number series from the Backoffice → Settings app → order number series.

#### Improvements

* ElasticSearch Implementation now handles large data sets better.
* Added logging of execution time for indexing service.

## Alpha 11

### Build 4017

#### Features

* The promotions engine now applies to carts based on the promotions set up in the Backoffice.
* Changing the priority of campaigns is now undoable.
* Deleting campaigns is now undoable.
* The list of campaigns is now searchable.

#### Bugs

* Clicking on "create new campaign" to open the modal will now show the loading state until ready.
* Creating a new campaign from the Backoffice will now have all the required fields marked with "required".
* Creating a new campaign from the Backoffice will now have fields disabled while sending a POST request.

#### Other improvements

* Scratch indexing was slow for categories containing a large number of products.

## Alpha 10

### Build 4016

#### Breaking

* Renamed all pipeline tasks that save changes using the DbContext to "SaveChangesPipelineTask" to keep naming consistent

#### Features

* It is now possible to create and replace an Index Definition to configure what information will be indexed to ElasticSearch (e.g., custom definition fields).
  * More here: [Index Definitions](/readme/search-and-indexing/indexing/index-definitions)
* It is now possible to implement product filtering using Faceted Search based on price or any custom definition field.
  * More here: [Facets](/readme/search-and-indexing/indexing/facets)
* It's now possible to search for menu items in the settings app, and pressing enter will open the highlighted menu item.
* It's now possible to see, create, edit, and delete product relations from the Backoffice → Settings app → Product relations menu.
* For all selectable lists in Backoffice, when we start selecting a row, it will switch to select mode, which means the cursor will change to a check sign, and when clicking any row, it will check/uncheck the row instead of opening the edit page.

#### Bugs

* Fixed: When editing order billing or shipping through Backoffice, the country and shipping methods wouldn't show up correctly.
* Fixed: On the orders app, when a user starts selecting orders to change status, the orders with different statuses should be disabled.
* Fixed: Product indexer could not find related categories.
* Fixed: Category indexer could not find related products.
* Fixed: Product indexer now correctly indexes parent products.
* Fixed: Existing relations between products and categories when adding a new product to a category would result in duplicates.
* Fixed: Filtering issue for user-defined multilingual enums when fetching products for indexing.
* Fixed: User-defined multilingual enums would not be saved correctly when changed in the Backoffice.

#### Other improvements

* Fewer database lookups are needed when indexing a category.

#### Known bugs

* Product relation names can't be longer than 50 characters. The backend will throw an exception, but the UI doesn't notify the user of the problem.

## Alpha 9

#### Breaking

* Refactored the UpdateCart pipeline to CalculateCart and SaveOrder pipeline to CalculateOrder - and updated all other pipelines using those
  * Added the possibility to use entities on the inputs instead of entity GUIDs
  * Added the possibility to optionally not save database changes in these pipelines (they always saved before)

#### Features

* Added loading and error states to the product app.
* Search terms, sort options and render types (search or navigate) are part of query params in backoffice (the user is able to share the search result URL)
* It is now possible to see the price groups menu item in the settings of the backoffice, and a list of price groups when navigating into it.
* It is now possible to add a new price group directly from the backoffice.
* It is now possible to delete a list of price groups and undo deletion in the backoffice settings.
* It is now possible to edit a price group and undo changes when clicking the undo button after editing from backoffice.
* Improved index consistency using a persistent queue of entities to add/update/remove from the index.
  * Interval/schedule and lock duration for the indexing can be set on `SearchOptions`. Defaults are 5 seconds (`IndexingInterval`) and 30 seconds (`IndexingLockDuration`).

#### Bugs

* Fixed an issue from alpha 7, where stripe and adyen definitions were no longer added correctly when migrating from an old database.

## Alpha 8

#### Breaking

* The authentication changes in this alpha mean you need to update your project, see [this gist](https://gist.github.com/Skidaddle99/6b30952a43ac51aa86d49b1c6f2fe9f9) for details.

**Features**

* It is now possible to implement [authentication for the backoffice](/readme/backoffice-authentication)
* It is now possible to [get an overview of the registered pipelines and tasks](/readme/how-to/discover-pipelines-and-their-tasks)

## Alpha 7

#### Breaking

* DefaultRoundingService now uses `MidpointRounding.`**`AwayFromZero`**&#x69;nstead of `MidpointRounding.ToEven`(C# standard). See [Midpoint values and rounding conventions](https://learn.microsoft.com/en-us/dotnet/api/System.Math.Round?view=net-7.0#midpoint-values-and-rounding-conventions) for further details
* `IRepository` has been removed. Instead, use `UcommerceDbContext`to interact with EF Core directly

#### Features

* It is now possible to create, see, edit, and delete (CRUD) currencies from the backoffice, settings app, and currencies menu.
* It is now possible to create, see, edit, and delete (CRUD) shipping methods from the backoffice, settings app, and shipping methods menu.
  * It is possible to edit the general properties of a shipping method as well as pricing and restrictions.
* It is now possible to see and change the image on a payment method through the backoffice (requires custom IImageService).
* It is now possible to update which pipeline a payment method uses after its creation.

#### Bugs

* Custom, non-multilingual definition field updates are now persistent again.
* Category Search Models list of product IDs now reflects the IDs of the products and not the category product relations.
* Categories and payment methods now get their display names set in all languages on creation.
* Categories with "display on site" set to false are not indexed; they are not shown in the indexed products' category list.
* It is now possible to create a category on the default store again.
* Changed the name of the category field "Show Prices Including VAT" to "Show Prices Including Tax", and updating the field is now persistent again.
* When generating promo codes, the amount field is now limited to 6000, just like the API, effectively avoiding a silent fail in the backoffice when going over this limit.
* A custom definition field on a product definition now correctly defaults to the data type editor instead of an editor property, effectively meaning that the field now appears consistently in the backoffice.
* Missing Tax Rate and incorrect net price calculated for products. (Thank you Gratsiela!)
* Better error messages.
* Better differentiated empty state messages depending on order status for order lines and discounts section.
* Content Picker Editors: Dynamic rendering of related entity type names in translations, replacing the previous hardcoded “product” label in the sub-header.
* The item name in the breadcrumbs navigation is truncated now if it's more than 42 characters.

## Alpha 5

#### Main Features

* The pricing section has been added to the payment method edit page.
* It is now possible to delete a payment method and undo that deletion.
* It is now possible to deep link to a product
* It is now possible to edit the store, country and price group related restrictions on payment methods

#### Bug fixes

* Clicking on a product variant now redirects you to it again.
* Product criteria no longer throws an error when adding a product.
* "Spend more than" criteria now handles too large numbers properly.
* Account payment method no longer throws 500 after a fresh install.
* Display name for categories, catalogs, products and payment methods, can no longer be empty.
* Updating, adding or deleting an order line now live-updates the log book.
* Validation errors of Product upon creation is now displayed properly.
* Dropdown items are now easier to click.
* Get payment methods method in transaction library now returns hydrated payment method.
* Get payment methods endpoint in headless now respects price group restriction.
* Default payment method Account now has the relevant information to function as intended.
* Removing Display names completely is no longer possible on any Entity through back office.

## Alpha 3

#### Main Features

* All new Api Acces page for configuring headless properties like client id, client secret, and a list of URIs that should be whitelisted.
* You can change the client secret for your stores in the UI, granting a much faster headless setup.
* You can add URLs to your whitelist for headless through the ui, granting a much smoother experience than earlier alpha builds.
* You can remove URLs from the URL whitelist.

#### Bug fixes

* Adding specific discounts to a promotion no longer throws an error.
* Adding a product to a product criteria no longer throws an error.
* The country field in the orders app is now more responsive.
* Create store modal now closes when clicking cancel.
* Creating a category now normalizes the sort order as expected.
* Moving a category to another category now sorts them correctly.
* A negative quantity in `AddToCart` will decrease the amount on an order line or delete it if the value drops to 0 or below.
* Variant products will now correctly show in the Backoffice that they inherit parent prices if no prices are set.
* Store pickers are now reactive; new stores will appear immediately.
* Deleted stores will immediately be removed from store pickers.
* Undo button now disappears after deleting an entity.
* Multiple requests are no longer sent when getting stores on high-resolution devices.

## Alpha 2

#### Main Features

* Payment Method Settings, the settings area of the backoffice now has a section for configuring your payment methods. It's possible to see payment methods, create new ones, and edit the properties for a payment method.
* Support for acquiring payments with Stripe as provider.
* Support for refunding payments with Stripe as provider.
* Support for cancelling payments with Stripe as provider.
* New template for an MVC project with Ucommerce with a single index page, scratch indexing and pages for categories, products, adding products as well as viewing and clearing a cart.

#### Bug fixes

* It's now possible to change the SKU of a product in backoffice again.
* Store domain dropdown in backoffice now supports different port numbers.
* Store domain dropdown in backoffice is no longer triplicated.
* Creating a cart now includes it's order lines.
* Re-ordering catalogs and categories no longer throws a server error.
* It's now possible to change order status from the backoffice again.
* It's now possible to update the long description on a product in backoffice again.

</details>


