---
title: Cart item discounts
related:
  - https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/catalog-discounts.md
  - https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/order-discounts.md
  - https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/free-shipping.md
  - https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/coupon-codes.md
  - https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/customer-eligibility-customization.md
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

> **License:** Advanced license required.
>
> Features described on this page require the Xperience by Kentico **Advanced** license tier.

Cart item discounts allow you to create custom promotion rules that discount units the customer already has in the shopping cart, based on the content of the whole cart. The reward of a rule can depend on several products and can land on more than one cart item. Cart item discounts are evaluated during [price calculation](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/price-calculation.md).

Use cart item discounts to implement:

- Quantity-based rewards (e.g., "Buy 3 coffees, the cheapest is half price")
- Product-triggered rewards (e.g., "Buy a laptop, get a mouse 50% off")
- Rewards spread across cart items (e.g., "Six coffees earn two discounted units", where the two units can belong to different products)

> **Tip:** **Cart items and units**
>
> A **cart item** is an entry in the shopping cart: one product with a quantity. A cart item holds one or more **units** of the product. For example, three coffees of the same type are one cart item with three units. In price calculation, a cart item is a `PriceCalculationRequestItem` on input and an `IPriceCalculationResultItem` on output. In an order, the same entry is saved as an order item, also called a line item.
>
> A cart item discount can cover all units of a cart item or only some of them.

Cart item discounts combine two capabilities: the reward can depend on the content of the whole cart, and it discounts specific cart items. Use cart item discounts when you need both. If you need only one of them, another discount type is a better fit:

- To discount a product based on the product alone (e.g., 10% off all coffees), use [catalog discounts](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/catalog-discounts.md).
- To discount the order total based on the order as a whole (e.g., 10% off orders over $100), use [order discounts](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/order-discounts.md).

> **Note:** **Cart item discounts only discount what is already in the cart**
>
> A cart item discount never adds products to the cart. It reduces the price of units the customer added, and the customer can't accept or reject the reward. Cart item discounts also aren't evaluated for prices in the catalog. They run only in the shopping cart and checkout [calculation modes](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/price-calculation.md#calculation-modes).

## General promotion capabilities

Cart item discounts use the general promotion configuration and evaluation framework, so you can use the following capabilities:

- [Coupon codes](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/coupon-codes.md) – Optionally require a discount code for redemption.
- [Customer eligibility](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/customer-eligibility-customization.md) – Restrict the promotion to all visitors, registered members only, or a custom eligibility rule.
- [Activation scheduling](https://docs.kentico.com/documentation/business-users/manage-commerce-stores.md#create-a-promotion) – Set start and end dates for time-limited offers.

## Cart item promotion rule overview

A **promotion rule** defines the logic that decides which cart items a cart item discount affects and by how much. You implement promotion rules as classes that inherit from `CartItemPromotionRuleBase`.

> **Info:** **Terminology – Discounts vs. Promotions**
>
> In the documentation and the administration interface, store managers work with cart item discounts. In code, the related classes use "Promotion" in their names (e.g., `CartItemPromotionRuleBase`, `CartItemPromotionCandidate`). This is because the underlying system uses a unified promotion framework.

The rule receives the calculation data for the whole cart. It answers once per calculation, not once per cart item:

- Which cart items get a discount
- How many units of each cart item are discounted
- How much money comes off each discounted unit

The rule only states what it offers. It doesn't need to know about catalog discounts or other cart item discounts.

In the default implementation, the system decides which offer wins in a dedicated calculation step, the item promotion resolution step, which runs after the steps that record the candidates for catalog and cart item promotions (see [How discounts compete](#how-discounts-compete)).

### Promotion candidate

A **promotion candidate** represents a discount that a rule offers on one cart item. The rule returns a list of `CartItemPromotionCandidate` objects, one for every cart item it discounts. Each candidate contains:

- `ProductIdentifier` – The cart item whose price the candidate reduces.
- `UnitPriceDiscountAmount` – The amount taken off the unit price of each discounted unit. This is an amount of money, not a percentage.
- `DiscountedQuantity` – The number of units of the cart item that receive the discount. These units don't receive any catalog discount. The remaining units of the cart item keep their catalog price, including any catalog discount.
- `PromotionTriggerItems` – The cart items that earned the reward, each with a `ProductIdentifier` and the `Quantity` of units that count toward the reward. Don't include the units that the candidate already discounts. The system stores the trigger items with the order (see [Order creation](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/checkout-process/order-creation.md#promotion-handling)) and shows them in the administration.

When the cart doesn't qualify, return an empty list.

A list is necessary because one promotion can reward more than one cart item. For example, a rule that discounts the cheapest unit for every three coffees can discount units on two different products, and it returns a candidate for each.

Return at most one candidate for each cart item. A rule applies the same discount to every unit it discounts. It can't discount the first unit of one cart item by 50% and the second by 25%. All discounted units of one cart item share the same `UnitPriceDiscountAmount`. Candidates for the same cart item don't combine in the default implementation. They would compete and only the best one would apply (see [How discounts compete](#how-discounts-compete)).

### Promotion rule properties

Promotion rules can define configurable properties that store managers set when creating promotions in the administration interface. Properties are defined in a separate class implementing `IPromotionRuleProperties` and are decorated with [editing components](https://docs.kentico.com/documentation/developers-and-admins/customization/extend-the-administration-interface/ui-form-components/editing-components.md) to generate the UI.

Unlike catalog, order, and free shipping rules, cart item rules don't have a base properties class. You always implement `IPromotionRuleProperties` yourself.

### How discounts compete

In the default implementation, the catalog promotion and cart item promotion steps record the candidates that the rules return, and they don't apply any of them yet. After both steps finish, the item promotion resolution step (`IItemPromotionResolutionPriceCalculationStep`) decides which candidates price each cart item. This step runs before the line subtotals step, which calculates the prices from the applied candidates. A single unit never receives both a catalog discount and a cart item discount. The customer always gets the better one.

For every cart item, the system selects:

1. The **best catalog candidate** – The candidate with the highest `UnitPriceDiscountAmount`.
2. The **best cart item candidate** – The candidate that saves the most compared to the catalog discount on the units it discounts. The saving is `(UnitPriceDiscountAmount - catalog UnitPriceDiscountAmount) × DiscountedQuantity`.

The best cart item candidate applies only when it saves more than the catalog discount on the units it covers. If it saves the same or less, the catalog discount stays on the whole cart item. When the cart item candidate applies, the catalog discount prices only the units it doesn't cover, and isn't applied at all when it covers every unit.

When more than one candidate targets the same cart item, only the one with the greatest saving applies. This holds whether the candidates come from different promotions or from the same rule, so cart item discounts never combine on one cart item. Different cart items can be discounted by different promotions. When two candidates are equal, the most recently created promotion wins.

The following example uses three coffees that cost $10, $9, and $8, each with a 10% catalog discount, and the promotion "Buy 3, the cheapest is half price":

| Coffee | Catalog discount (10%) | Cart item discount | Applied            | Cart item price |
| ------ | ---------------------- | ------------------ | ------------------ | --------------- |
| $10    | $1.00                  | None               | Catalog discount   | $9.00           |
| $9     | $0.90                  | None               | Catalog discount   | $8.10           |
| $8     | $0.80                  | $4.00              | Cart item discount | $4.00           |

The third coffee costs 50% of its original price, because the cart item discount saves more than the catalog discount on the same unit.

A cart item discount can also discount only some of the units of a cart item. When a customer buys three identical coffees at $10 with a 10% catalog discount, the promotion "Buy 3, the cheapest is half price" discounts one of the three units, so the cart item has units with two different prices. The discounted unit costs $5.00, and the other two keep the catalog price of $9.00 each. The cart item costs $23.00.

## Create cart item promotion rules

Implementing a cart item promotion rule involves creating two components:

1. **Properties class** – Defines configurable settings that store managers can adjust in the administration interface (e.g., the number of units required, the discount percentage). The properties class implements `IPromotionRuleProperties`.
2. **Logic class** – Contains the rule's evaluation logic. It decides which cart items to discount and calculates the candidates. The logic class inherits from `CartItemPromotionRuleBase`.

These two components work together: the properties class stores the configuration, and the logic class uses those configured values to evaluate the cart during price calculation.

![Cart item promotion rule class hierarchy showing the relationship between properties and logic components](https://docs.kentico.com/docsassets/documentation/cart-item-discounts/cart-item-promotion-rule-hierarchy.drawio.svg "Cart item promotion rule class hierarchy showing the relationship between properties and logic components")

> **Tip:** **Sample implementation**
>
> See **DancingGoatCartItemPromotionRule.cs** and **DancingGoatCartItemPromotionRuleProperties.cs** in the Dancing Goat [project template](https://docs.kentico.com/documentation/developers-and-admins/installation.md#available-project-templates) for a sample rule that discounts the cheapest units in a scope of categories, products, or tags.

### Define promotion rule properties

Implement `IPromotionRuleProperties` and use [editing component](https://docs.kentico.com/documentation/developers-and-admins/customization/extend-the-administration-interface/ui-form-components/editing-components.md) annotations to generate the input fields for each property. The following example lets store managers set how many units the customer needs to buy and how much the cheapest unit is discounted:

```csharp title="Cart item promotion rule properties"
using CMS.Commerce;

using Kentico.Xperience.Admin.Base.FormAnnotations;

// ...

public class CheapestUnitDiscountProperties : IPromotionRuleProperties
{
    [NumberInputComponent(Label = "Number of units required", Order = 1)]
    [MinimumIntegerValueValidationRule(2)]
    [RequiredValidationRule]
    public int MinimumQuantity { get; set; } = 3;


    [DecimalNumberInputComponent(Label = "Discount % on the cheapest unit", Order = 2)]
    [MinimumDecimalValueValidationRule(0)]
    [MaximumDecimalValueValidationRule(100)]
    [RequiredValidationRule]
    public decimal DiscountPercentage { get; set; } = 50;
}
```

To limit a rule to selected products, categories, or tags, add properties that store managers fill in with those products, categories, or tags, and filter the cart items by them in `GetPromotionCandidates`. To display fields conditionally, use [visibility conditions](https://docs.kentico.com/documentation/developers-and-admins/customization/extend-the-administration-interface/ui-form-components/ui-form-component-visibility-conditions.md#assign-visibility-conditions-using-attributes), for example the `VisibleIfEqualTo` attribute.

The Dancing Goat sample combines a `Scope` drop-down list with a property for each kind of selection, and shows only the property that matches the chosen scope.

### Implement the promotion rule

The rule inherits from `CartItemPromotionRuleBase` and uses the following generic type parameters:

1. `TPromotionRuleProperties` – The rule's [properties class](#define-promotion-rule-properties). Must implement `IPromotionRuleProperties`.
2. `TPriceCalculationRequest` – The price calculation request type.
3. `TPriceCalculationResult` – The price calculation result type.

Implement the following members:

- `GetPromotionCandidates(IPriceCalculationData<TPriceCalculationRequest, TPriceCalculationResult> calculationData)` – Required. Returns the list of [promotion candidates](#promotion-candidate) for the whole cart.
- `GetDiscountValueLabel()` – Optional. Returns a short label that shows the discount value in the administration listing, for example "50%". The method returns `null` by default.

The following sample discounts one unit, the cheapest in the cart, once the cart contains the required number of units. The other required units become trigger items:

```csharp title="Cart item promotion rule"
using System;
using System.Collections.Generic;
using System.Linq;

using CMS.Commerce;

// ...

public class CheapestUnitDiscountRule
    : CartItemPromotionRuleBase<CheapestUnitDiscountProperties,
        PriceCalculationRequest, PriceCalculationResult>
{
    public const string IDENTIFIER = "Acme.CheapestUnitDiscount";


    public override IReadOnlyList<CartItemPromotionCandidate> GetPromotionCandidates(
        IPriceCalculationData<PriceCalculationRequest, PriceCalculationResult> calculationData)
    {
        // The promotion needs at least two units to discount one of them
        if (Properties.MinimumQuantity < 2)
        {
            return [];
        }

        IEnumerable<IPriceCalculationResultItem<ProductIdentifier, ProductData>> cartItems =
            calculationData.Result.Items;

        // Orders the cart items from the cheapest unit price
        List<IPriceCalculationResultItem<ProductIdentifier, ProductData>> items = cartItems
            .OrderBy(item => item.ProductData.UnitPrice)
            .ToList();

        int totalUnits = items.Sum(item => (int)item.Quantity);

        // The cart doesn't qualify
        if (totalUnits < Properties.MinimumQuantity)
        {
            return [];
        }

        IPriceCalculationResultItem<ProductIdentifier, ProductData> cheapestItem = items[0];

        // The other required units earn the reward, so they become the trigger items
        List<PromotionTriggerItem> triggerItems =
            GetTriggerItems(items, cheapestItem, Properties.MinimumQuantity - 1);

        return
        [
            new CartItemPromotionCandidate
            {
                ProductIdentifier = cheapestItem.ProductIdentifier,
                UnitPriceDiscountAmount = cheapestItem.ProductData.UnitPrice * Properties.DiscountPercentage / 100m,
                DiscountedQuantity = 1,
                PromotionTriggerItems = triggerItems
            }
        ];
    }


    private static List<PromotionTriggerItem> GetTriggerItems(
        IEnumerable<IPriceCalculationResultItem<ProductIdentifier, ProductData>> items,
        IPriceCalculationResultItem<ProductIdentifier, ProductData> discountedItem,
        int requiredTriggerUnits)
    {
        int remainingTriggerUnits = requiredTriggerUnits;
        List<PromotionTriggerItem> triggerItems = [];

        foreach (IPriceCalculationResultItem<ProductIdentifier, ProductData> item in items)
        {
            // Stops when all required trigger units are collected
            if (remainingTriggerUnits == 0)
            {
                break;
            }

            // The discounted unit itself is never a trigger unit
            int availableTriggerUnits = (int)item.Quantity - (item == discountedItem ? 1 : 0);

            // Takes as many units from this item as are available, up to the units still needed
            int unitsToAdd = Math.Min(availableTriggerUnits, remainingTriggerUnits);

            if (unitsToAdd > 0)
            {
                triggerItems.Add(new PromotionTriggerItem
                {
                    ProductIdentifier = item.ProductIdentifier,
                    Quantity = unitsToAdd
                });

                remainingTriggerUnits -= unitsToAdd;
            }
        }

        return triggerItems;
    }


    public override string GetDiscountValueLabel()
    {
        return $"{Properties.DiscountPercentage}%";
    }
}
```

The rule picks the cheapest unit by comparing the unit prices of the products. It doesn't read or reproduce catalog discounts, and it doesn't round the discount. A rule only states what it offers. The system compares the offer with the catalog discount on the same cart item (see [How discounts compete](#how-discounts-compete)) and rounds the line subtotal.

> **Note:** The sample treats unit quantities as whole numbers.

### Register the promotion rule

Use the `RegisterPromotionRuleAttribute` assembly attribute with the following parameters to register the rule:

1. A unique string identifier for the rule.
2. `PromotionType.CartItem` to indicate this is a cart item promotion.
3. A display name shown in the administration interface.

Without registration, the rule doesn't appear in the administration interface. Until at least one cart item rule is registered, the **Cart item discounts** section of the **Promotions** application asks you to implement one.

```csharp title="Register a cart item promotion rule"
using CMS.Commerce;

// ...

[assembly: RegisterPromotionRule<CheapestUnitDiscountRule>(
    identifier: CheapestUnitDiscountRule.IDENTIFIER,
    promotionType: PromotionType.CartItem,
    name: "Discount on the cheapest unit")]
```

The registered type must inherit from `CartItemPromotionRuleBase`. Registering any other type for `PromotionType.CartItem` throws an `ArgumentException`.

After implementing and registering a cart item promotion rule:

1. The promotion rule appears in the administration interface when [creating new cart item discounts](https://docs.kentico.com/documentation/business-users/manage-commerce-stores.md).
2. Store managers can configure the promotion and set activation dates.
3. Active promotions are automatically evaluated during price calculation in the shopping cart and checkout.
4. The system resolves the returned candidates against catalog discounts and applies the best ones.

### Filter promotion applicability

Override the `IsApplicable` method to add high-level conditions that determine whether the promotion should be evaluated at all. This method runs **once per promotion** before `GetPromotionCandidates`, making it useful for performance optimization when you can skip the entire promotion early.

The evaluation pipeline works as follows:

1. **Get active promotions** – The system retrieves all active cart item promotions for the calculation date.
2. **Check requirements** – The system validates the coupon code and customer eligibility. Promotions that don't pass are skipped.
3. **Check IsApplicable** – For each remaining promotion, `IsApplicable` is called once. If it returns `false`, the promotion is skipped entirely.
4. **Get candidates** – For promotions that pass, `GetPromotionCandidates` is called once for the whole cart. The system records every returned candidate on its cart item.
5. **Resolve** – The system decides which candidates apply, as described in [How discounts compete](#how-discounts-compete).

If a promotion's rule isn't registered in the system, the calculation throws an `InvalidOperationException`.

Use `IsApplicable` for checks that apply to the entire promotion context. For example, to restrict a promotion to registered members:

```csharp title="Restrict promotion to registered members"
using System.Threading;
using System.Threading.Tasks;

using CMS.Commerce;
using CMS.DataEngine;

// ...

private readonly IInfoProvider<CustomerInfo> customerInfoProvider;

public MembersOnlyCheapestUnitDiscountRule(IInfoProvider<CustomerInfo> customerInfoProvider)
{
    this.customerInfoProvider = customerInfoProvider;
}

public override async Task<bool> IsApplicable(
    IPriceCalculationData<PriceCalculationRequest, PriceCalculationResult> calculationData,
    CancellationToken cancellationToken)
{
    BuyerIdentifier? buyerIdentifier = calculationData.Request.BuyerIdentifier;

    if (buyerIdentifier == null)
    {
        return false;
    }

    // The shopping cart identifies the buyer as a member,
    // but the price calculation of a created order identifies the buyer as a customer
    if (buyerIdentifier.Kind == BuyerIdentifierKind.Member)
    {
        return true;
    }

    // A customer linked to a member account is a registered member
    CustomerInfo? customer = await customerInfoProvider.GetAsync(buyerIdentifier.Value, cancellationToken);

    return customer?.CustomerMemberID > 0;
}
```

### Access the promotion ID

All promotion rules inherit the `PromotionID` property from `PromotionRuleBase`. Use it within `IsApplicable` or `GetPromotionCandidates` to look up promotion-specific data or enforce per-promotion business rules. For an example, see [Access the promotion ID](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/catalog-discounts.md#access-the-promotion-id).

## Access promotion data in calculation results

After price calculation, you can access promotion information from the result to display discounts to customers or for reporting purposes.

Recorded cart item candidates are stored in the `PromotionData.CartItemPromotionCandidates` collection of each result item. Each candidate is wrapped in `IPriceCalculationPromotionCandidate<CartItemPromotionCandidate>`, which exposes the promotion ID, the coupon code that activated the promotion (if any), the `Applied` flag, and the candidate itself. The collection contains all recorded candidates, and `Applied` marks the one that won.

The same pattern applies to `PromotionData.CatalogPromotionCandidates`. A cart item can have an applied catalog candidate and an applied cart item candidate at the same time, because each prices a different set of units.

```csharp title="Access cart item promotion data"
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using CMS.Commerce;

// ...

// Both services are injected through the constructor
private readonly IPriceCalculationService<PriceCalculationRequest, PriceCalculationResult> priceCalculationService;
private readonly IPromotionCoveredQuantityCalculator promotionCoveredQuantityCalculator;

// ...

public async Task DisplayCartItemDiscounts(
    PriceCalculationRequest request,
    CancellationToken cancellationToken)
{
    PriceCalculationResult calculationResult =
        await priceCalculationService.Calculate(request, cancellationToken);

    foreach (IPriceCalculationResultItem<ProductIdentifier, ProductData> item in calculationResult.Items)
    {
        // Finds the applied cart item promotion (if any)
        IPriceCalculationPromotionCandidate<CartItemPromotionCandidate>? appliedPromotion =
            item.PromotionData.CartItemPromotionCandidates.FirstOrDefault(candidate => candidate.Applied);

        if (appliedPromotion != null)
        {
            CartItemPromotionCandidate candidate = appliedPromotion.PromotionCandidate;

            // The number of units of the cart item that the cart item discount discounts
            decimal discountedUnits = promotionCoveredQuantityCalculator.GetCoveredQuantity(item, appliedPromotion);

            // The amount taken off the cart item by the cart item discount
            decimal discountAmount = candidate.UnitPriceDiscountAmount * discountedUnits;
        }
    }
}
```

> **Warning:** **Use the number of discounted units, not the cart item quantity**
>
> A discount can apply to only some of the units of a cart item. Multiplying `UnitPriceDiscountAmount` by the quantity of the cart item over-counts the discount. This applies to catalog candidates too, because the catalog discount applies only to the units that a cart item discount leaves. Use `GetCoveredQuantity` of `IPromotionCoveredQuantityCalculator` to get the number of discounted units for catalog and cart item candidates. The method returns 0 for candidates that aren't applied.

## Extend promotion candidate objects

You can extend `CartItemPromotionCandidate` to include additional data needed for display or business logic, in the same way as for [catalog promotion candidates](https://docs.kentico.com/documentation/developers-and-admins/digital-commerce-setup/promotions/catalog-discounts.md#extend-promotion-candidate-objects). For example, the following class stores a display label that you can show to customers:

```csharp title="Extended cart item promotion candidate"
using CMS.Commerce;

// ...

public class DisplayableCartItemPromotionCandidate : CartItemPromotionCandidate
{
    public string DisplayLabel { get; init; } = string.Empty;
}
```

Return an instance of the extended class from `GetPromotionCandidates` of your [promotion rule](#implement-the-promotion-rule). The extended class inherits the required properties of the base class, so set them in the object initializer:

```csharp title="Return the extended candidate from the rule"
using CMS.Commerce;

// ...

return
[
    new DisplayableCartItemPromotionCandidate
    {
        ProductIdentifier = cheapestItem.ProductIdentifier,
        UnitPriceDiscountAmount = cheapestItem.ProductData.UnitPrice * Properties.DiscountPercentage / 100m,
        DiscountedQuantity = 1,
        PromotionTriggerItems = triggerItems,
        DisplayLabel = "Cheapest unit discount"
    }
];
```

To access the custom data in calculation results, cast the `PromotionCandidate` property of the applied candidate:

```csharp title="Access the extended candidate"
using System.Linq;

using CMS.Commerce;

// ...

foreach (IPriceCalculationResultItem<ProductIdentifier, ProductData> item in calculationResult.Items)
{
    IPriceCalculationPromotionCandidate<CartItemPromotionCandidate>? appliedPromotion =
        item.PromotionData.CartItemPromotionCandidates.FirstOrDefault(candidate => candidate.Applied);

    // Checks if the candidate is a 'DisplayableCartItemPromotionCandidate'
    if (appliedPromotion?.PromotionCandidate is DisplayableCartItemPromotionCandidate displayable)
    {
        string label = displayable.DisplayLabel;
        // Process the label...
    }
}
```
