Promotion exclusions
You can determine which promotions cannot be combined with other promotions. For example, you are running a 20% off automotive supplies sale. Customers should not be able to combine this promotion with the Get cheapest item 50% off promotion on the same item.
Use order and unit-level exclusions
The Commerce Connect API supports both order-level and unit (item)-level exclusions. The components mentioned here are available in the EPiServer.Commerce.Marketing namespace.
Set the desired action through the ExclusionLevel property on the PromotionEngineSettings passed when running the PromotionEngine. Here are examples of using exclusion levels with the PromotionEngine.
Call the PromotionEngine directly
var cart = ServiceLocator.Current.GetInstance<IOrderRepository>().LoadOrCreateCart<ICart>(PrincipalInfo.CurrentPrincipal.GetContactId(), Cart.DefaultName);
ServiceLocator.Current.GetInstance<IPromotionEngine>().Run(cart, new PromotionEngineSettings() { ExclusionLevel = ExclusionLevel.Unit });Use the ApplyDiscounts extension method
var cart = ServiceLocator.Current.GetInstance<IOrderRepository>().LoadOrCreateCart<ICart>(PrincipalInfo.CurrentPrincipal.GetContactId(), Cart.DefaultName);
cart.ApplyDiscounts(_promotionEngine, new PromotionEngineSettings() { ExclusionLevel = ExclusionLevel.Unit });You can find another sample implementation in the Optimizely QuickSilver sample site, in the CartService class.
Order level exclusion
ExclusionLevel.Order, the default setting, ensures that two promotions excluding each other are not applied within the same OrderGroup. If both are applied, the higher-priority promotion is used, while the other promotion is ignored.
To exclude promotions, set a list of ContentReference to the ExcludedItems property of the PromotionData. The following samples illustrate this.
promotionData.ExcludedItems.Add(campaignLink);
promotionData.ExcludedItems.Add(anotherPromotionLink);Three types of ContentReference can be added to the ExcludedItems property:
- The SalesCampaign.CampaignRoot – This promotion cannot be combined with any other promotions.
- A campaign's ContentLink – This promotion cannot be combined with any promotions in that campaign.
- A promotion's ContentLink – This promotion cannot be combined with that specific promotion.
Unit level exclusion
With ExclusionLevel.Unit, two promotions that exclude each other can both apply to the same OrderGroup, but never to the same unit. A unit is a single item, not a line item. A line item with a quantity of 5 contains five units, and behaves the same as five line items with a quantity of 1.
Use unit-level exclusion when exclusion means "do not stack these promotions on the same item," rather than "never apply both promotions to this order." For example, a brand-specific promotion and a category-wide promotion can both apply to a cart, but not to the same product.
Note The behavior of unit-level exclusion changed in EPiServer.Commerce 14.47.0. If your site uses ExclusionLevel.Unit, see Changes in Commerce 14.47.0. If your site uses the default ExclusionLevel.Order, these changes do not affect you.
As with order-level exclusion, you exclude promotions by adding ContentReference values to the ExcludedItems list.
promotionData.ExcludedItems.Add(campaignLink);
promotionData.ExcludedItems.Add(anotherPromotionLink);How the promotion engine applies unit-level exclusion
The promotion engine evaluates promotions in priority order, highest first. For each promotion, the engine does the following:
- Finds the free units. A free unit is a unit that no promotion in an exclusion relationship with the current promotion has used. Units used by promotions that do not exclude the current promotion are still free.
- Tests the promotion's condition against free units only. If the condition cannot be met in full from free units, the promotion does not apply. There is no partial redemption.
- If the condition is met, applies the promotion and marks all units it used as consumed.
The following rules apply:
- The direction of the exclusion does not matter. "P1 excludes P2," "P2 excludes P1," and both together produce the same result. The higher-priority promotion uses units first, and the lower-priority promotion applies to the units that are left.
- A promotion consumes every unit it uses, not only the units it discounts. Units that meet a promotion's condition are consumed, even when they are charged full price. For example, in "Buy 3, get 10% off 1," all three units are consumed.
- Free-item promotions follow the same rules. Buy X get Y free, Get cheapest item free, and Get most expensive item free promotions only receive units that are left over.
Order and shipping promotions
Unit-level exclusion only applies to promotions whose DiscountType value is LineItem. Order and shipping promotions apply to the whole order, so the engine cannot split units between them and a line-item promotion.
When one side of an exclusion is an order or shipping promotion, the engine excludes the line-item promotion in full, as it does with ExclusionLevel.Order. This applies regardless of which promotion declares the exclusion.
Example
Preconditions
- Promotion P1 is "Buy 3 of product A and get a 10% discount on 1 product A". Its redemption limit is 1.
- Promotion P2 is "Get 5% off product A".
- P1 has a higher priority than P2.
- P1 or P2 excludes the other, or both exclude each other.
- An order group has the following line item: SKU = A, quantity = 10, unit price = 100.
Result
- P1 gives one unit a 10% discount.
- The 2 other units used by P1 are charged full price. They are consumed by P1, so P2 cannot use them.
- The remaining 7 units are available for P2, and each receives a 5% discount.
Units | Promotion | Unit price | Line total |
|---|---|---|---|
1 | P1 reward (-10%) | 90 | 90 |
2 | P1 condition (full price) | 100 | 200 |
7 | P2 (-5%) | 95 | 665 |
10 | | | 955 |
Note The sequence in which promotions are executed is determined by their priority.
Changes in Commerce 14.47.0
EPiServer.Commerce 14.47.0 corrects several inconsistencies in unit-level exclusion. These changes affect your site only if you set ExclusionLevel.Unit, either globally through PromotionOptions.PromotionExclusionLevel or for individual calls through PromotionEngineSettings.
Change | Previous behavior | New behavior | Effect on price |
|---|---|---|---|
One-way and two-way exclusions | The result depended on which promotion declared the exclusion. Sometimes the lower-priority promotion was dropped completely. | The direction of the exclusion does not matter. The lower-priority promotion applies to units the higher-priority promotion did not use. | Larger discounts |
Line-item promotions that exclude order or shipping promotions | Depending on the setup, both promotions could apply. | The line-item promotion is excluded in full. | Smaller discounts |
Consumed units | Only units that received a discount were consumed. | All units that meet the condition and all units that receive the reward are consumed. | Smaller discounts |
Free-item promotions | Buy X get Y free, Get cheapest item free, and Get most expensive item free promotions could use units that other promotions had already used. | These promotions only receive leftover units. | Fewer free items |
Reporting excluded promotions | BuyOneGetOneDiscount returned Excluded with no redemption. Other promotion types were inconsistent. | A line-item promotion left with no free units returns Fulfilled with one redemption whose status is FulfillmentStatus.Excluded. | None |
Test order totals in a test environment before you upgrade, using carts that match your active promotions. Focus on mutual exclusions, line-item promotions that exclude order or shipping promotions, and buy X or free-item promotions.
Update your code
If you upgrade to Commerce 14.47.0 or later and use unit-level exclusion, review the following:
- IsDoubleExclusion is obsolete. Replace calls to OrderFormPriceMatrix.IsDoubleExclusion with OrderFormPriceMatrix.IsExcluded, which handles one-way and two-way exclusions the same way. IsDoubleExclusion will be removed in a future major version.
- Custom free-item processors. GetAffectedEntries in GetFreeItemsProcessorBase now only returns product codes that still have free units. If your custom processor overrides this method, it may receive fewer batches than before. Test any logic that assumes all cart units are available.
- Custom reward applicators. Apply in RewardApplicatorBase<T> no longer marks an excluded gift redemption as fulfilled. Check that custom applicators that extend this class do not rely on the previous behavior.
- Code that lists applied promotions. A promotion whose units were all consumed by excluding promotions still returns Fulfilled, but its redemption has the status FulfillmentStatus.Excluded. It saves nothing and adds no gift items. Filter out these promotions in cart messages, order confirmations, order notes, and analytics:
var applied = rewardDescriptions.Where(r =>
r.Status.HasFlag(FulfillmentStatus.Fulfilled) &&
r.Redemptions.Any(x => x.Status != FulfillmentStatus.Excluded));