Transitioning from Sitecore Experience Commerce to OrderCloud: Promotions

Reading Time: 10 minutes

In this article, we will review and compare promotions between Sitecore Experience Commerce and OrderCloud to facilitate developers looking to transition from XC to OrderCloud as well as identify a path for migration of existing XC solutions to OrderCloud.

We will look at a high-level comparison of architecture, functionality, and data models, with greater focus on transitioning from Sitecore Experience Commerce, meaning identifying a close path to parity in OrderCloud, using the Habitat catalog and SXA Storefront.

Note: This article is a little outdated with the release of Promotion Enhancements. Please see the official documentation for latest updates.

Conceptual Architecture and Features

Qualifications/Conditions vs Eligible Expressions

The Business Tools application in XC provides a basic wizard dialog for building up qualifications for a promotion. XC has over thirty promotion qualifications (or conditions) to construct common promotion scenarios without customisation.

For a complete list of available qualifications, see Promotions qualifications.

Figure 1: Add quailification wizard in XC.
Figure 2: Promotion qualifications view in XC.

OrderCloud promotions provide an EligibleExpression property, which accepts a free-text logic expression to qualify/validate a promotion as eligible against an order, using custom syntax outlined in Rule-Based Promotion Expressions.

Figure 3: Eligible expression in OrderCloud.

Looking at table 1, we see the native support for utilising entities and objects in qualifications/eligible expressions for each platform. For qualification types that are not natively supported by the OrderCloud platform, it is possible that support can be achieved through customisation. For more details, see Extending Promotion Functionality > Custom Promotion Logic.

Qualification Type \ PlatformXCOrderCloud
Product InventoryYesNo
Cart Line/Line ItemYesYes
Cart/OrderYesYes
CustomerYesYes
Customer Order HistoryYesNo
Date and TimeYesPartial
Table 1: Native qualification type support between XC and OrderCloud.

Benefits/Actions vs Value Expressions

Similar to qualifications, the benefits (or actions) in XC’s Business Tools also uses a wizard dialog to facilitate building out the promotion’s benefits, with over 10 pre-defined benefits available.

For a complete list of available benefits, see Promotion benefits.

Figure 4: Add benefit wizard in XC.
Figure 5: Promotion benefits view in XC.

OrderCloud promotions have a ValueExpression property, which use the same Rule-Based Promotion Expressions as the EligibleExpression property.

Figure 6: Value expression in OrderCloud.

Is Exclusive vs Can Combine

Promotions in XC have an Is Exclusive flag, which is used in the Promotion Evaluation and Application Logic to determine eligible promotions and prioritisation over non-exclusive promotions.

In OrderCloud, promotions use a can combine flag with similar behaviour, however the single biggest difference is that exclusive promotions do not take priority over non-exclusive (can combine) promotions.

Order Level vs Line Item Level Promotions

Both XC and OrderCloud promotions allow promotions to apply discounts at the order level or line item level, however, in calculating the promotion’s qualifications against the order or line item total properties, XC’s calculations are dynamically updated with each promotion discount calculation, therefore leveraging the Engine’s Promotion Priority Rules.

OrderCloud calculates each promotion discount against static totals, therefore regardless of the order in which promotions are calculated, it will not impact the final discount calculations, as per Promotion Calculation Logic.

Promotion Prioritisation

As touched upon above in Order Level vs Line Item Level Promotions, XC has a set of Promotion Priority Rules that determine the order promotions are applied.

OrderCloud’s Promotion Prioritisation Logic does not prioritise the order of promotions applied to an order, which is effectively not required as is utilises static order and line item totals meaning that promotion discount values will always be calculated the same, regardless of the order they are processed.

Promotion Assignments

For promotions in XC to be available to customers in a storefront, the Commerce Engine groups promotions into promotion books, which are associated to catalogs. The catalog is then associated to a storefront, which will be associated to a security domain that contains customers, to complete the customer to promotion connection.

Promotion books can be associated to multiple catalogs to be utilised in other storefronts.

In OrderCloud, promotions have more explicit assignments, allowing direct assignments to the buyer.

OrderCloud promotions can also be configured to target buyer user groups for more refined promotion targeting. More information can be found in Personalized Shopping > Targeted Promotions.

Figure 6: XC vs OrderCloud promotion assignment architecture.

Automatic vs Coupon-Triggered Promotions

The Commerce Engine’s promotions can be classified as automatic or coupon-triggered. Coupon-triggered promotions are evaluated as a preliminary qualifier during promotion qualification, which requires the coupon code to be applied to the cart, while the automatic promotions pass this qualifier gate and continue the promotion evaluation process, effectively allowing a promotion to be able to be applied to a cart based on the cart state and promotion qualifications alone and not via some explicit request to apply the promotion. For futher details, see Promotion Evaluation and Application Logic.

An XC promotion can be added to a cart even if it has yet to qualify for its benefit.

Currently, only coupon-triggered promotions are native to the OrderCloud platform. In a slightly different manner to XC, applying a promotion via a coupon code will be rejected by the platform if its currently not eligible, e.g. does not meet its eligible expression (qualification rule), falls outside the start or expiration date, does not adhere to the rules for the CanCombine flag, etc.

Private vs Public Coupons

The Commerce Engine breaks down coupons into private and public coupons, where public coupons are those that anyone may use, while private coupons are generated as single use coupons that the business can distribute to customers as they see fit, yet has no restrictions in the Commerce Engine as to who can use the coupons, although some smarts can be configured in the promotion’s qualifications and customisations will allow specific business requirements to be met.

Figure 7 shows the general setup of promotions with public vs private coupons, where customers with the relevant promotion assignments can apply the promotions to their cart.

Figure 7: Overview of private vs public coupons on XC promotions.

In OrderCloud, although promotions don’t have an explicit private/public flag against them, they can still be configured to behave in a similar fashion.

For “public” coupon promotions, this would simply depend on the promotion assignment created. For example, at a global level the AllowAllBuyers flag will allow all users to apply the promotion to their order, or an explicit promotion assignment to one or more buyers and/or buyer user groups may fit the “public” coupon business requirement.

For “private” coupon promotion, defining the promotion assignments to target the subset of buyer users and leveraging the RedemptionLimitPerUser property (covered in more detail in Redemption Limits), we can restrict the amount of redemptions the users have for the promotion.

Figure 8: An approach to public vs private promotions in OrderCloud.

Redemption Limits

As mentioned in Private vs Public Coupons, in XC private coupons have a single usage limit. Public coupons, on the other hand, have no redemption limit, although the UsageCount is tracked against the coupon entity, which can be used in a customisation to limit promotion redemptions at a global level.

In OrderCloud, promotions have a RedemptionLimit property to configure a global limit across all users and a RedemptionLimitPerUser property to set a redemption limit per user (both with the relevant promotion assignments).

Approval Workflow

XC has approval workflow logic built into promotions, which requires a business user with the PromotionerManager role to approve a promotion prior to it being made available.

Figure 7: Promotion approval workflow.

OrderCloud has no native approval workflow for promotions, however we could use the following approach to replicate the XC functionality in OrderCloud (with the additional retract action for business users who have submitted a promotion for approval prematurely and want to amend the promotion without intervention from the promotion manager).

Figure 8: An XC approval workflow equivalent in OrderCloud.
  • xp.Status will track the current promotion’s status. The Disabled state has been promoted to an explicit status.
  • Pre-hook webhooks would be utilised for all relevant promotion endpoints, for the middleware to validate the user’s custom PromotionManager role where applicable and also ensure that the xp.Status transition is valid.

Disabling a Promotion in Non-Approved States

For non-Approved statuses, these are effectively inactive promotion states that we will need to safeguard against, i.e. inactive promotions cannot be applied to orders and should be invalidated for orders not in an Unsubmitted or AwaitingApproval state. The approach we will look to take is to override the promotion’s ExpirationDate property.

For the Draft and ReadyForApproval states, the ExpirationDate can be set with the StartDate and the intended expiration date can be set on the xp as ActualExpirationDate. When the promotion is promoted to an approved state, the ExpirationDate can be replaced with the ActualExpirationDate.

With the StartDate being the equivalent of the ExpirationDate, the promotion can never be evaluated to being active.

For the Disabled state, we can set the ExpirationDate to the current date/time, while the xp.ActualExpirationDate still maintains the original expiration date for historical and reconciliation purposes.

For this approach, “inactive” promotions will throw a Promotion.Expired error code, which in conjunction with the xp.Status could be translated into a more user-friendly and promotion status-specific error message for front end applications, and/or allow for more appropriate error handling.

Triggering Promotion Evaluation

In XC, promotions were evaluated whenever the CalculateCartPipeline was called, which typically occurs when retrieving the cart or modifying the cart.

In OrderCloud, promotions are validated, calculated, and invalidated by the platform when certain endpoints are called. For detailed information, see Promotion Validation, Calculation, and Invalidation.

Extending Promotions

It is possible to fully customise Commerce Engine by adding, modifying and removing pipelines and pipelines and pipeline blocks to meet bespoke business requirements.

Extending promotions in OrderCloud requires a building the solution around the API rather than being able to customise the platform directly as per the Commerce Engine . Depending on business requirements a few approaches have been documented in Custom Promotion Logic.

XC to OrderCloud Promotion Cheatsheet

As a quick reference for re-writing XC promotion qualifications and benefits as eligible expressions and value expressions in OrderCloud, the following tables provide either the available OrderCloud expressions, or are flagged that customisation are required to achieve, with some instances providing sample approaches to how the customisation could be achieved.

Qualifications to Eligible Expressions

Product Inventory Qualifications

Line Level: true

Qualification/ConditionEligible Expression
Inventory Item Stock Count In [specific] Location [compares] to [specific value]?Inventory evaluation requires extending the API functionality via middleware.
Is Item In Stock?Inventory evaluation requires extending the API functionality via middleware.
Is Item in Stock in [specific] Location?Inventory evaluation requires extending the API functionality via middleware.
Is Item Out of Stock?Inventory evaluation requires extending the API functionality via middleware.
Is Item Out of Stock in [specific] Location?Inventory evaluation requires extending the API functionality via middleware.
Is Cart Item Available?Inventory evaluation requires extending the API functionality via middleware.
Is Cart Item [specific] Quantity Available?Inventory evaluation requires extending the API functionality via middleware.
Is Item Pre-orderable?item.Product.xp.PreOrderable = true
Although pre-order functionality is not native to the platform, a pre-order flag may be set on the product’s xp to achieve this promotion.
Is Item Pre-orderable in [specific] Location?Inventory evaluation requires extending the API functionality via middleware.
Is Item Back-orderable?item.Product.xp.Backorderable = true
Although back-order functionality is not native to the platform, a back-order flag may be set on the product’s xp to achieve this promotion.
Is Item Back-orderable in [specific] Location?Inventory evaluation requires extending the API functionality via middleware.

Cart Line Qualifications

Line Level: true

Qualification/ConditionEligible Expression
Cart Item Quantity [compares] to [specific value]?item.ProductID = <value> and item.Quantity <comparison operator> <value>
Cart Item Quantity is in [min] [max] Range?item.ProductID = <value> and item.Quantity >= <min value> and item.Quantity <= <max value>
Cart Item Subtotal [compares] to [specific value]?item.ProductID = <value> and item.LineSubtotal <comparison operator> <value>
Cart Item is in [specific] category?item.product.incategory(<category ID>)
OR
item.incategory(<category ID>)

Cart Qualifications

Qualification/ConditionEligible Expression
Cart Has included/excluded [specific] item(s)? (Items Collection)items.Any(<value>) and not item.Any(<value>)
Cart Has Items?Order.LineItemCount > 0
Cart Has [count] Items?Order.LineItemCount = <value>
Cart Any Item Has [Tag]?Arrays are not currently supported in OrderCloud, however this could be achieved via converting tags to flags or strings for direct evaluation.
Cart Any Item Subtotal [compares] to [specific value]?items.any(LineSubtotal <comparison operator> <value>)
Cart Subtotal [compares] to [specific value]?Order.Subtotal <comparison operator> <value>
Cart Has Fulfillment?Order.xp.SelectedShipMethodID <> null
Requires maintaining a copy of the SelectedShipMethodID from the ShipEstimateResponse on the order’s xp.
Is Context Currency [specific value]?Order.Currency = <value>

Customer Qualifications

Qualification/ConditionEligible Expression
Is Cart Contact Currency [specific value]?Order.FromUser.Locale.Currency = <value>
Is Cart Contact Customer ID [specific value]?Order.FromUser.ID = <value>
Is Cart Contact Language [specific value]?Order.FromUser.Locale.Language = <value>
Is Cart Contact Registered?Order.FromUser.ID = <Default Context User>

Order History Qualifications

While the orders count and orders total may be able to be updated on the customers xp, keeping track of individual items purchased in the xp would be considered abuse as this can scale out of control and breach the maximum character length.

Qualification/ConditionEligible Expression
Current Customer Has Purchased [specific] Item?Evaluation requires extending the API functionality via middleware.
Current Customer Has Purchased Item with [specific] Tag?Evaluation requires extending the API functionality via middleware.
Current Customer Orders Count [compares] to [specific value]?Order.FromUser.xp.OrdersCount <comparison operator> <value>
Requires maintaining the count of orders on the buyer user’s xp.
Current Customer Orders Total [compares] to [specific value]?Order.FromUser.xp.OrdersTotal <comparison operator> <value>
Requires maintaining the sum of order totals on the buyer user’s xp.

Shop Qualifications

Qualification/ConditionEligible Expression
Is Shop Currency [specific value]?Order.Currency = <value>
Is Shop Language [specific value]?Order.FromUser.Locale.Language = <value>
Is Shop Name [specific value]?Order.xp.Storefront = 'Storefront'
Requires customising the storefront to apply the storefront name to the order’s xp.

Date Qualifications

Qualification/ConditionEligible Expression
Current Date Has Passed?<value> > now(0)
It is likely that the promotion’s StartDate should be sufficient.
Is Current Day?Evaluation requires extending the API functionality via middleware.
Is Current Month?Evaluation requires extending the API functionality via middleware.

Benefits to Value Expressions

Cart Benefits

Benefits/ActionsValue Expression
Get Free ShippingOrder.ShippingCost
Get Free GiftRequires extending the API functionality via middleware.
Get Cart Subtotal [specific] Amount Off<value>
Get Cart Subtotal [specific] Percent OffOrder.Subtotal * <percentage value>

Cart Line Benefits

As can be seen below, a number of benefits can be converted into the same, more basic value expression in OrderCloud as it retains context of the line item for evaluation, whereas XC typically requires the condition to be re-evaluated in the benefit to be applied correctly.

Benefits/ActionsValue Expression
Get Cart Any Item Sell Priceitem.UnitPrice - <value>
Get [specific] Amount Off Item Subtotal<value>
Get Cart Any Item Subtotal [specific] Amount OffSee above.
Get Cart Item in Category [specific] Amount OffSee above.
Get Cart Item Subtotal [specific] Amount OffSee above.
Get [specific] Percentage off Item Subtotalitem.LineSubtotal * <percentage value>
Get Cart Any Item Subtotal [specific] Percent OffSee above.
Get Cart Item in Category [specific] Percent OffSee above.
Get Cart Item Subtotal [specific] Percent OffSee above.

References

Continue the Series

  1. Transitioning from Sitecore Experience Commerce to OrderCloud: Customers to Buyer Users
  2. Transitioning from Sitecore Experience Commerce to OrderCloud: Customers and Buyers – API Access
  3. Transitioning from Sitecore Experience Commerce to OrderCloud: Catalogs and Categories
  4. Transitioning from Sitecore Experience Commerce to OrderCloud: Sellable Items To Products
  5. Transitioning from Sitecore Experience Commerce to OrderCloud: Inventory and Pricing
  6. Transitioning from Sitecore Experience Commerce to OrderCloud: Carts to Unsubmitted Orders and Carts
  7. Transitioning from Sitecore Experience Commerce to OrderCloud: Fulfillments to Shipping
  8. Transitioning from Sitecore Experience Commerce to OrderCloud: Tax and Payments
  9. Transitioning from Sitecore Experience Commerce to OrderCloud: Orders
  10. Transitioning from Sitecore Experience Commerce to OrderCloud: Order Workflow and Minions
  11. Transitioning from Sitecore Experience Commerce to OrderCloud: Promotions

OrderCloud: Working with Promotions

Reading Time: 10 minutes

In this article, we will review how the OrderCloud platform applies promotion discounts on an order and its line items, identify when promotions are validated, calculated, and invalidated, and approaches to working with promotions to ensure robust implementations through error handling and extending promotions.

Introduction

The promotions resource is one of the few OrderCloud resources that is currently backed by the rules engine, with each promotion utilising two rule expressions. The EligibleExpression is a boolean expression, which will evaluate whether an order is eligible to have the promotion applied, and a ValueExpression, which evaluates the order and returns a monetary value, which is then used in determining the order and line level PromotionDiscounts.

Promotions that have been applied to orders are referred to as order promotions, which contain static a discount value Amount and reference data for the underlying promotion, which can allow a promotion entity to be updated and impact existing order promotions upon re-evaluation.

Order Level vs Line Item Level Promotions

Order Level Promotions

With the LineItemLevel promotion property set to false, the promotion intends to calculate the order promotion’s Amount (discount) to be applied to the order’s PromotionDiscount, aggregated with all other promotion discounts.

Expressions written for order level promotions can target properties within the order and line items models, however line items require are targeted specifcally via the items functions, e.g. order.Subtotal > 100 and items.any(ProductID = 'ABC').

In the following example, the order worksheet shows the calculated line item level order promotions being applied to the order’s PromotionDiscount, affecting the Total, which also contains line level discounts.

{
  "Order": {
    "ID": "OrderLevelPromotionOrder",
    ...
    "Subtotal": 100,
    "PromotionDiscount": 40, // (25 + 15) order level discounts
    "Total": 60,
    ...
  },
  "OrderPromotions": [
    {
      "Amount": 25,
      "LineItemID": null,
      "ID": "promo1",
      "LineItemLevel": false,
      "Code": "promo1",
      "EligibleExpression": "order.ID = 'OrderLevelPromotionOrder'",
      "ValueExpression": "25",
      ...
    },
    {
      "Amount": 15,
      "LineItemID": null,
      "ID": "promo2",
      "LineItemLevel": false,
      "Code": "promo2",
      "EligibleExpression": "true",
      "ValueExpression": "15",
      ...
    },
    ...
  ],
  ...
}

Line Item Level Promotions

Alternatively, promotions can be configured as a line item level promotion by setting the LineItemLevel property to true. Line item level promotions are used to apply the promotion discount to specific line items that meet the rule configured under the EligibleExpression. While the order model can be targeted within the expressions, the line item are evaluated individually with their own context and do not have any knowledge of each other. The order promotion’s Amount will be aggregated to the line item’s PromotionDiscount as well as the order’s PromotionDiscount.

To target the line items, the item.<property> syntax and the item functions are used, e.g. item.ProductID = 'ABC' or item.incategory('category1').

In the following example, the order worksheet shows the calculated line item level order promotions being applied to the order and line items’ PromotionDiscount, affecting the Total and LineTotal properties.

{
  "Order": {
    "ID": "LineItemLevelPromotionOrder",
    ...
    "Subtotal": 200,
    "PromotionDiscount": 55, // 30 (line item level) + 25 order level
    "Total": 145,
    ...
  },
  "LineItems": [
    {
      "ID": "LineItemID1",
      "ProductID": "ABC"
      ...
      "PromotionDiscount": 30, // (20 + 10) line item level discounts
      "LineTotal": 70,
      "LineSubtotal": 100,
      ...
    }
  ],
  "OrderPromotions": [
    {
      "Amount": 20,
      "LineItemID": "LineItemID1",
      "ID": "promo2",
      "LineItemLevel": true,
      "Code": "promo2",
      ...
      "EligibleExpression": "item.incategory('category1')",
      "ValueExpression": "item.LineSubtotal * .2",
      "CanCombine": true,
      ...
    },
    {
      "Amount": 10,
      "LineItemID": "LineItemID1",
      "ID": "promo3",
      "LineItemLevel": true,
      "Code": "promo3",
      ...
      "EligibleExpression": "item.ProductID = 'ABC'",
      "ValueExpression": "10",
      "CanCombine": true,
      ...
    },
    ...
  ],
  ...
}

The Can Combine Exclusivity Flag

The promotion’s CanCombine flag dictates how promotions can be applied to an order. With the flag set to true the promotion can be applied to an order along with other promotions that have the CanCombine flag also set to true. When the flag is set to false, it is treated as an exclusive promotion that can only be added to an order in isolation.

The initial promotion that has been applied to an order determines whether additional promotions can be applied to an order.

In table 1 and table 2, we see the results of applying five promotions consecutively, with a combination of CanCombine flags set to true and false. Table 1 initially applies a promotion that can be combined with other promotions and therefore results in multiple promotions being applied to the order with the exclusive (CanCombine is false) promotions being rejected. Conversely, table 2 shows that by applying an exclusive promotion first, no other promotions can be added to the order.

PromotionCanCombineAcceptedOrder Promotions Applied
Promotion 1trueYesPromotion 1
Promotion 2trueYesPromotion 1, Promotion 2
Promotion 3falseNoPromotion 1, Promotion 2
Promotion 4trueYesPromotion 1, Promotion 2, Promotion 3
Promotion 5falseNoPromotion 1, Promotion 2, Promotion 3
Table 1: Example applying multiple CanCombine (true) promotions to an order.
PromotionCanCombineAcceptedOrder Promotions Applied
Promotion 3falseYesPromotion 3
Promotion 1trueNoPromotion 3
Promotion 2trueNoPromotion 3
Promotion 5falseNoPromotion 3
Promotion 4trueNoPromotion 3
Table 2: Example applying an exclusive CanCombine (false) promotion to an order.

API Promotion Logic Overview

Promotion Validation, Calculation, and Invalidation

In working with the promotions resource of the OrderCloud platform, understanding how internal promotion logic impacts an order will help identify where middleware and front end applications should be expecting to handled errors.

There are three aspects to promotions that need to be considered:

  • Validate / Evaluate: Whether or not an endpoint will trigger validation/evaluation of existing promotions.
    These endpoints may throw API errors for invalidated promotions, which could occur due to any of the following reasons listed below in Can Invalidate? as well as a promotion expiring expiring due to lapsed time.
  • Calculate: Whether or not an endpoint will attempt to calculate the current value of all order promotions. Where the promotion amount has been overridden from the Calculate Order integration event, these are considered frozen and will not be updated.
  • Can Invalidate?: Whether or not an endpoint can cause order promotions to be in an invalid state. This occurs as the OrderCloud platform does not validate the order promotions after its transaction, and therefore will not be able to inform the calling system of the possible invalidation in the API response. Invalidation may occur from:
    • An order has been updated or a line item has been added/updated/removed, resulting in the order no longer meeting the eligible expression criteria.
    • A promotion has been updated by a business user, e.g. eligible expression, start date, end date, etc., which would no longer be valid for an order and/or its line items.

With these aspects of promotion logic defined, table 3 documents all endpoints relevant to promotions and promotion invalidation.

EndpointValidate / EvaluateCalculateCan Invalidate?
Add Promotion to OrderY1Y1N
Remove Promotion from OrderNNN
Line Item AddedY2YY
Line Item Updated (PUT / PATCH)Y2YY
Line Item DeletedNNY
Order Updated (PUT / PATCH)Y3YY
Calculate OrderYYN
Validate OrderYNN
Submit OrderYNN
Approve OrderYYN
Update Promotion (PUT / PATCH)NNY4
Delete PromotionNNN5
Delete Promotion AssignmentNNN4
Table 3: Overview of OrderCloud API promotion behaviour
  1. The promotion being added will be added to the existing order promotions for processing.
  2. Although the line item models are updated in memory, the promotion validation occurs against the database models, therefore even if an changes to an order line items would make an ineligible promotion eligible after the transaction, the API will still throw a Promotion.NotEligible API error.
  3. The order model is updated in memory prior of promotion validation, allowing an order with ineligible promotions to be resolved with the order update request.
  4. Promotion validation relating to promotion assignments only occurs during the Add Promotion to Order endpoint and is not validated for subsequent validation measures, therefore the promotion may continue to apply and calculate for the order if the promotion assignment has been removed.
  5. Deleted promotions are removed directly from the database, which will remove the deleted promotion from any unsubmitted order or null out the promotion ID of a submitted order.

Promotion-Related API Errors

When the OrderCloud API validates / evaluates order promotions, table 4 documents the possible API errors that can be returned in the API Response that the implementer may want to handle as a global catch all or have error handling built for specific API errors.

API ErrorReason
NotFound1Order or promotion was not found.
Promotion.OwnerIDMustMatchOrderToCompanyID1The order is being directly purchased from a suppiler, however the supplier is not the owner of the promotion.
Promotion.NotYetValidPromotion StartDate is later than current date/time.
Promotion.ExpiredPromotion ExpirationDate is earlier than current date/time.
Promotion.ExceedsUsageLimitPromotion RedemptionCount is greater than RedemptionLimit or user’s redemption count greater than RedemptionLimitPerUser.
Promotion.CannotCombineMore than one promotion has been applied to the order and one or more promotions have CanCombine set to false.
Promotion.AlreadyAdded1The promotion being added is already applied to the order.
Promotion.NotEligibleThe EligibleExpression has been evaluated to false.
Table 4: Overview of promotion-related API Errors.
  1. Add Promotion to Order endpoint only.

Promotion Calculation Logic

For promotion calculation logic, when evaluating expressions against the order’s Total or line item’s LineTotal, the totals are not updated with any promotion discounts previously applied/calculated. This is to prevent one order’s promotion invalidating or incorrectly calculating subsequent order promotions.

Table 5 shows an example of two promotions calculated by the OrderCloud platform when applied to an order.

Promotionorder.TotalDiscount
$10 off order where order.Total > 9010010
10% off order where order.Total > 9010010
Result8020
Table 5: Total not including existing discounts (OrderCloud behaviour).

To see what would happen if the totals were updated per promotion calculation, let’s look at the following scenarios.

If the promotion discounts were updated on a running order total, table 6 shows that EligibleExpressions on subsequent promotions could have evaluated to false, resulting in them not being eligble.

Promotionorder.TotalDiscount
$10 off order where order.Total > 9010010
10% off order where order.Total > 9090Not Eligible
Result9010
Table 6: Example promotion validation/evaluation where the total includes existing discounts (which is not OrderCloud behaviour).

Similarly, if we focus on the promotion discount values alone, we see that if OrderCloud were to use a dynamic total that included the previous discounts then the results could be affected by the order they are evaluated in, which is represented in table 7 and table 8.

Promotionorder.TotalDiscount
10% off order10010
$10 off order9010
Result8020
Table 7: Example promotion discount calculations where the total includes existing discounts (which is not OrderCloud behaviour).
Promotionorder.TotalDiscount
$10 off order10010
10% off order909
Result8119
Table 8: Example promotion discount calculations where the total includes existing discounts, calculated in a different order (which is not OrderCloud behaviour).

Promotion Prioritisation Logic

When the OrderCloud platform is evaluating an order with multiple order promotions, regardless of whether the promotions are order level or line item level, there are currently no smarts to prioritise one promotion over another. As a result, there’s no guarantee that order promotions will always be evaluated in a consistent order. With the promotion calculation logic not rolling discounts into the order and line totals, this should not affect the outcome of the final order and line item discount values.

Extending Promotion Functionality

Handling Promotion Invalidation

From table 3, we now have an understanding of the API endpoints will trigger order promotion validation, in turn revealing invalidated order promotions, which our middleware and front end applications would ideally handle gracefully, but requires custom implementation to achieve.

In the following example, we take the approach of wrapping any OrderCloud endpoint, which will validate/evaluate the order promotions, in a try catch and by creating logic specifically for handling a promotion that is no longer eligible our calling system can silently resolve these issues and then attempt to call the intended endpoint again, without the previous API error.

As there could be multiple API errors that follow up on subsequent calls, we allow for multiple retries, however this is just a guide that would likely be amended to cater for the specific business requirements of any given solution.

Promotion Evaluation Endpoint Wrapper

var hasError = false;
var retryCount = 0;
do
{
    try
    {
        hasError = false;
        await orderCloudClient.<Evaluate Endpoint>(); // Add Promotion to Order, Line Item Added, etc.
    }
    catch (OrderCloudException ex)
    {
        hasError = true;
        retryCount++;
        if (ex.HttpStatus == HttpStatusCode.BadRequest &&
            ex.Errors.Any(error =>
                error.ErrorCode == OrderCloud.SDK.ErrorCodes.Promotion.NotEligible))
        {
            var codes = ex.Errors.Select(error => (error.Data as Promotion)?.Code);
            // Resolve all promotion errors, e.g. remove promotions,
            // record removed promotions to notify the user, etc.
        }
    }
} while (hasError && retryCount < 3);

Overriding Line Item Promotion Discounts

It’s possible that the way a business handles line item discounts differs to the calculation logic of OrderCloud and the rules engine, e.g. promotion discounts may be rounded or weighted across line items, which can be supported through the use of the order calculate order checkout integration event.

The calculate integration event provides the ability to create LineItemOverrides.PromotionOverrides, which supports overriding promotion amounts. The overriden Amount will be applied to the respective order promotion Amount and a hidden frozen flag will be set to true to prevent any future order promotion calculations from reverting back to the value calculated from the ValueExpression.

{
  "Order": {
    "ID": "LineItemLevelPromotionOrder",
    ...
    "Subtotal": 200,
    "PromotionDiscount": 39.95, // 19.95 (line item level) + 20 order level
    "Total": 160.05,
    ...
  },
  "LineItems": [
    {
      "ID": "LineItemID1",
      ...
      "PromotionDiscount": 19.95, // (9.95 + 10) line item level discounts
      "LineTotal": 80.05,
      "LineSubtotal": 100,
      ...
    }
  ],
  "OrderPromotions": [
    {
      "Amount": 9.95,
      "LineItemID": "LineItemID1",
      "ID": "promo2",
      "LineItemLevel": true,
      "Code": "promo2",
      ...
      "EligibleExpression": "item.incategory('category1')",
      "ValueExpression": "item.LineSubtotal * .2",
      "CanCombine": true,
      ...
    },
    
    ...
  ],
  "OrderCalculateResponse": {
    "LineItemOverrides": [
    {
      "LineItemID": "LineItemID1",
      "PromotionOverrides": [
        {
          "PromotionID": "promo2",
          "Amount": 9.95
        }
      ],
      "Remove": null
      }
    ],
    "HttpStatusCode": 200,
    "Succeeded": true
  },
  ...
}

To remove a line item promotion’s override, the order calculate integration event will need explicitly return the respective LineItemOverrides item with the Remove property set to true. Omitting it from the request will not remove the hidden frozen flag, meaning the overrides will still be applied to the order promotions and will not be calculated from the ValueExpression.

Implementing the Calculate Integration Event

Where promotion line item overrides are required in a solution, it is likely that you would need to call the integration event immediately after any API endpoint to calls the calculate endpoint in order to ensure that discounts are kept in sync as per the following code snippet.

await orderCloudClient.<Calculate Endpoint>(); // Add Promotion to Order, Line Item Added, etc.
await orderCloudClient.IntegrationEvents.CalculateAsync(OrderDirection.Outgoing, orderID);

Custom Promotion Logic

While OrderCloud does not support creating custom functions for the rules engine’s expression evaluation, we have to look for alternate paths achieve custom promotion logic in an OrderCloud solution.

Copy Data to Order or Line Item xp

If the order or line item models are lacking the object models for creating expression queries against, e.g. selected shipping methods, then the first approach is to copy data onto the order or line item xp. This may be considered xp abuse, but a necessary evil, especially for calculated and transient data.

In taking this approach, you will also be responsible for maintaining the data integrity of this duplicate data to ensure it does not become stale when the platform validates order promotions during API endpoint requests listed in table 3.

With the introduction of premium order search, do not forget that xp property data types must be consistent across all orders to prevent the index from breaking.

Pre-Hook Webhooks

When needing to replace a promotion’s EligibleExpression with custom promotion logic, webhooks can be leveraged to intercept the Add Promotion to Order requests with the following high-level approach.

  1. Create a promotion
    1. Set EligibleExpression as true so that always evaluates to true.
    2. Use xp to store the flag of the promotion custom logic name, e.g. promotion.xp.CustomLogicRule = 'MyRule'.
  2. Create a pre-hook webhook for the Add Promotion to Order endpoint.
  3. For the application receiving the payload (see figure 1):
    1. Call get order worksheet for order and line item models.
    2. Call get promotion for promotion model, if applicable.
    3. Validate the requested promotion requires custom logic and execute the custom logic to determine if the order is eligible.
    4. Set the webhook response proceed property with the eligibility state of the order promotion, to inform the OrderCloud API whether to apply the promotion or not.
  4. In order to re-validate custom logic for flagged promotions, for any API call that triggers promotion validate / evaluate (from table 3):
    1. All flagged promotions would need to be removed and then re-added to trigger the pre-hook custom validation again, prior to calling the evaluate endpoint.
Figure 1: Example pre-hook webhook for implementing custom promotion logic.

References

OrderCloud: The Rules Engine

Reading Time: 4 minutes

In this article, we will review how OrderCloud leverages the rules engine to extend platform logic, including the resources currently backed by the engine and the syntax used to create rule-based expressions.

Overview

The rules engine is integrated into the OrderCloud platform as a means of extending platform logic via custom rule-based expressions. The expressions are defined in free-text properties using a custom syntax, consisting of OrderCloud models, operators and predefined functions, which are evaluated by the rules engine and used for “if-then” logic in the OrderCloud platform.

Below documents the resources that currently leverage the rules engine, along with high-level “if-then” logic.

ResourceIfThen
PromotionsEligibleExpressionCreate promotion discount of ValueExpression.
Order Approvals – Buyer Approval RulesRuleExpressionProgress through the order approval workflow.
Order Returns – Seller Approval RulesRuleExpressionProgress through the order returns workflow.

For example, the following promotion requirement has been translated into respective expressions.

10% off (up to $20) orders greater than $100 and containing product with ID ABC

EligibleExpressionorder.Total > 100 and items.any(ProductID = 'ABC')
ValueExpressionmax(order.Total * 0.1, 20)

Syntax

To ensure that you construct expressions correctly, the following syntax rules will need to met to avoid errors and unexpected behaviour.

  • Expressions use dot notation to navigate an object construct, e.g. order.xp.MyCustomProperty.
  • String values must be enclosed in straight single quotes ('), e.g. 'My Value'.
  • DateTime values must be enclosed in hashes (#), using the US date format, e.g. #6/24/2023#.
  • Parentheses (()) may be used to enclose sub-expressions and control order of execution, e.g. order.Total > 100 and (order.xp.MyProperty = 'this' or order.xp.MyProperty = 'that').

Operators

Operator TypeAvailable Options
Comparison=/==<>/!=, < , ><=>=
Logicalandor and not
Mathematical+-, */ and %

Models

While order and line item entities are the primary models in expressions, additional models can be accessed via child models. The following table shows how to access available models, which can also be identified throughout OrderCloud’s API reference documentation, including orders, line items, and both can be found under the order worksheet.

ModelSyntax
Orderorder.<property>
Line Itemitem.<property>
Productitem.Product.<property>
Product.<property>
in items.<function>(arg)
Variantitem.Variant.<property>
Variant.<property>
in items.<function>(arg)
User (Order Owner)order.FromUser.<property>
Billing Addressorder.BillingAddress.<property>
Shipping Addressitem.ShippingAddress.<property>
ShippingAddress.<property>
in items.<function>(arg)
Source Inventory/Seller/Supplier Locationitem.ShipFromAddress.<property>
ShipFromAddress.<property>
in items.<function>(arg)

Functions

Functions provide a means of evaluating specialised rules and conditions that are built into the rules engine. These pre-defined functions are documented below.

Items Functions

Items functions will have an implicit item context, within the function, therefore the item. prefix is omitted from any condition of the function argument.

FunctionReturnsDescription
items.any(arg)booleanReturns true if any line item meets the specified arg condition.
e.g. items.any(ProductID = 'ABC')
items.all(arg)booleanReturns true if all line item meets the specified arg condition.
e.g. items.all(ProductID = 'ABC')
items.quantity(arg)numberReturns the sum of line item quantities where the line item meets the specified arg condition.
e.g. items.quantity(ProductID = 'ABC') > 2
items.count(arg)numberReturns the number of line item where the line item meets the specified arg condition.
e.g. items.count(ProductID = 'ABC') > 2
items.total(arg)numberReturns the sum of all line item LineSubtotals where the line item meets the specified arg condition.
e.g. items.total(ProductID = 'ABC') > 100
product.incategory(args)booleanReturns true if the product for the line item being evaluated is assigned to one or more of the n category ID arguments.
e.g. items.any(product.incategory('cat1', cat2'))

Item Functions

The item functions along with any item specific condition can only be utilised in expressions for line item level promotions.

FunctionReturnsDescription
item.product.incategory(args)booleanReturns true if the product for the line item being evaluated is assigned to one or more of the n category ID arguments.
e.g. item.product.incategory('cat1', cat2')
item.incategory(args)booleanThe same as item.product.incategory(args), but with a shortened expression to save characters.
e.g. item.incategory('cat1', cat2')

Order Functions

The current order functions are limited to order approvals and usage should be limited to the order approvals – buyer approval rules resource to prevent any unexpected behaviour.

FunctionReturnsDescription
order.approved(ruleID)booleanReturns true if the ruleID has previously been approved for the order.
e.g. order.approved('rule1')

Order Return Functions

The current order return functions are limited to order returns, therefore usage should be limited to the order returns – seller approval rules resource to prevent any unexpected behaviour.

FunctionReturnsDescription
orderreturn.approved(ruleID)booleanReturns true if the ruleID has previously been approved for the order return.
e.g. orderreturn.approved('rule1')

General Functions

FunctionReturnsDescription
min(arg1, arg2)numberReturns the smaller of the two arg values.
e.g. min(order.LineItemCount, 5)
max(arg1, arg2)numberReturns the larger of the two arg values.
e.g. max(20, order.Total * 0.1)
now(arg)date/timeReturns a date/time with +/- arg value in days.
e.g. order.DateCreated < now(-5)

Limitations and Gotchas

As powerful as the rules engine can be, it’s not fool proof. If you are not careful with the way in which expressions are constructed, you may find yourself spending a lot of time troubleshooting unexpected behaviour due to a poorly written expression. The following limitations and gotchas will assist in mitigating both expression creation and troubleshooting efforts:

  • Expressions are limited to 400 characters.
  • Expressions do not support checking against null or whitespace values.
  • Arrays cannot be evaluated in expressions.
  • Inventory cannot be evaluated in expressions.
  • No validation against invalid result types for expressions. For example, the ValueExpression expects a decimal result, however the expression order.IsSubmitted = false would result to a boolean and return a 0 amount.
  • There is no way to extend or customise the rules engine directly, however leveraging webhooks it is possible for middleware to implement custom logic to fulfill any business requirements not covered by the rules engine.
  • Avoid creating conditions on properties that will change between order states as this could cause promotions to become invalidated and corrupt an order. For example, if using the condition order.Status = "Unsubmitted" where order approvals are active, when the order does get submitted, the order promotion will become invalidated.
  • Value expressions will be rounded to 2 decimal places, to the nearest number. This means that it’s possible that minor rounding issues could occur when applying multiple promotion calculations. For example, the following tables show two orders made with a line item level promotion to provide 5% each subtotal of each line item. As can be seen in the Totals row, the aggregate promotion discount has a +/-0.01 discrepancy.
UnitPriceQuantityLineSubtotal5% Promotion
Raw Value
5% Promotion
Rounded Value
9.9519.950.49750.50
9.9519.950.49750.50
9.9519.950.49750.50
Total329.851.49251.50
Order 1 promotion example.
UnitPriceQuantityLineSubtotal5% Promotion
Raw Value
5% Promotion
Rounded Value
9.95329.851.49251.49
Total329.851.49251.49
Order 2 promotion example.

References

Sitecore Experience Commerce: Promotion Evaluation and Application Logic in XC 10

Reading Time: 4 minutes

In this article, we will review the breaking changes to promotions that have been introduced in XC 10, considerations for upgrading from a XC 9.X solution, and the updated business logic that the Commerce Engine utilises to evaluate and apply promotions.

Note: References to date will indicate both date and time throughout this article.

Introduction

Previously in Sitecore Experience Commerce: Promotion Evaluation and Application Logic, we had identified that promotions calculations were broken down into cart line level and cart level promotions, of which cart line level promotions were calculated and applied prior to cart level promotions.

In Sitecore Commerce 10, there are 3 key changes that are important to understand, especially when upgrading solutions to XC 10, as the changes to rules for promotion application means that cart line and cart subtotal and total calculations may differ.

  1. Exclusive promotions are now exclusive across the cart line level and cart level promotions.
  2. Promotion Priority indexes have been introduced, allowing business users to specify a priority level that will take precedence in the initial ordering of promotions during evaluation. The lower the promotion priority value, the higher the promotion is evaluated (let it sink in for a minute).
    1. For exclusive promotion application, automatic promotions are addressed prior to coupon promotions, then Promotion Priority is the decider for determining the exclusive promotion within the group regardless of whether it’s a cart line level or cart level promotion.
    2. For applying multiple non-exclusive promotions, cart line level promotions, still take precedence over cart level promotions, while the Promotion Priority is a secondary factor.
 
Sitecore.Commerce.Plugin.Carts
ICalculateCartPipeline (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Carts.ClearCartBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Carts.CalculateCartSubTotalsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Fulfillment.CalculateCartLinesFulfillmentBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Fulfillment.CalculateCartFulfillmentBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Promotions.CalculateCartPromotionsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Promotions.RemoveUnwatedFreeGiftsFromCartBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Tax.CalculateCartLinesTaxBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Tax.CalculateCartTaxBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Carts.CalculateCartTotalsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Payments.CalculateCartPaymentsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
     ------------------------------------------------------------
     Plugin.Carts.WriteCartTotalsToContextBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)

Promotion Evaluating Logic

The following diagram shows the pipelines and pipeline blocks that are called during the process of evaluating the applicable promotions and additional filtering for exclusive promotion evaluation.

Figure 1. Promotion qualification evaluation pipeline logic

The following steps make up the evaluation process:

  1. Pre-Evaluate Promotions: Removes any previous selected free gift cart lines from the cart and adds them as a unique object by type to the commerce context. This is too ensure that these cart lines are not utilised when evaluating applicable promotions.
  2. Search For Promotions: Performs a search on the promotions index and retrieves the promotion entities from the commerce database:
    • Index Search Filters:
      1. By Promotion Entity Type: Filters down to the promotion entity type, i.e. excludes promotion book indexes.
      2. By Valid Date: Filters out promotions that do not fall within the Valid From/To dates based on effective date.
      3. By Catalogs: Filters out promotions where the associated catalog does not match any of the catalogs that are associated to any of the cart line items.
    • Post-Search Filters on Entity List:
      1. Filter Not Approved Promotions: Removes promotions that are not approved and, if the promotion is disabled, where the effective date is prior to the updated date (the date the promotion was disabled). The latter rule is to allow the promotion to be active when reviewed the storefront at a previous point in time.
  3. Filter Promotions By Items: Removes promotions where the cart contains no sellable items marked as included in the ItemsCollection qualification or where the cart contains any of the sellable items marked as excluded in the ItemsCollection qualification.
  4. Filter Promotions By Coupon: Removes promotions that require a coupon that has not been applied to the cart.
  5. Evaluate Promotions: Filters out promotions where promotion qualification and benefit rules are not applicable to the current cart.
  6. Filter Promotions With Coupons By Exclusivity: If exclusive coupon promotions are present in the promotions list, the list will be filtered down to a single exclusive coupon promotion. The exclusive promotion will be determined with the lowest Promotion Priority value, followed by the Added date that their corresponding coupons were applied to the cart, in the event of a tie breaker.
  7. Filter Promotions By Exclusivity: If exclusive automatic promotions are present in the promotions list, the list will be filtered down to a single exclusive automatic promotion. The promotion will be determined by the lowest Promotion Priority value, followed by the earliest (oldest) Valid From date, and in the event of multiple promotions sharing the same lowest Promotion Priority value and earliest Valid From date the promotion that was created earliest will take win.
  8. Post-Evaluate Promotions: Adds the previously selected free gift cart lines back to the cart; retrieved from the commerce context.

Promotion Priorisation Rules

While the previous section covered how promotions are evaluated, and also provided some insight into promotion priorisation, we will now cover the prioritisation rules.

The following diagram shows the logic used to determine which promotion(s) to apply to the cart.

Figure 2. Flow chart of promotion application logic

The blue boxes represent logic for maintaining free gift selections and don’t impact the application of the promotions, however this shows where this logic is handled.

There are essentially 3 steps that make up the promotion application process:

  1. Apply a single exclusive automatic promotion.
    • The promotion will be determined by the lowest Promotion Priority value, which will fallback to the earliest (oldest) Valid From date, and then the earliest Created date the promotion that was created, in the result of tied values.
    • If a promotion is applied here, no further promotions are applied.
  2. Apply a single exclusive coupon promotion.
    • The promotion will be determined by the lowest Promotion Priority value, with the Added date that their corresponding coupons were applied to the cart as a fallback to manage tied values.
    • If a promotion is applied here, no further promotions are applied.
  3. Apply all non-exclusive promotions.
    • The promotion order will be determined by cart line level promotions being applied first and cart level promotions applied second, both ordered by ascending Promotion Priority value. Where Promotion Priority values are equal, promotions are furthered ordered as follows:
      1. Automatic promotions, ordered by earliest (oldest) Valid From date, and in the event of multiple promotions sharing the same earliest Valid From date the promotion that was created earliest will win.
      2. Coupon promotions, ordered by earliest Added date that their corresponding coupons were applied to the cart.

Can Promotions Be Configured to XC 9.X Logic?

If Promotion Priorities are not utilised, the promotion logic remains largely the same as cart line level promotions are still applied before cart level promotions. The key difference is the promotion logic now ensures a sole exclusive promotion, which cannot be reverted by configuration and will require customisation.

References

Sitecore Experience Commerce: Promotion Evaluation and Application Logic

Reading Time: 4 minutesIn this article, we will review the default business logic that the Commerce Engine utilises to evaluate and apply promotions.

Note: References to date will indicate both date and time throughout this article.

Introduction

Before we get into the details around promotions, there are a few things we need to understand.

    • Promotions are separated into cart line level and cart level promotions, determined by the promotion benefits configured to each promotion. While multiple benefits can be added to promotions, additional benefits after the first can only be of the same benefit type.
    • Cart line calculations (subtotals, fulfillment fees, promotion discounts, taxes, totals) are evaluated and applied prior to the and cart calculations.
      Sitecore.Commerce.Plugin.Carts
      ICalculateCartLinesPipeline (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.ClearCartLinesBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.ClearCartBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.CalculateCartLinesSubTotalsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Fulfillment.CalculateCartLinesFulfillmentBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Promotions.CalculateCartLinesPromotionsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Tax.CalculateCartLinesTaxBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.CalculateCartLinesTotalsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
      -----------------------------------------------------------------
      Sitecore.Commerce.Plugin.Carts
      ICalculateCartPipeline (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.CalculateCartSubTotalsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Fulfillment.CalculateCartFulfillmentBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Promotions.CalculateCartPromotionsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Tax.CalculateCartTaxBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.CalculateCartTotalsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Payments.CalculateCartPaymentsBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
           ------------------------------------------------------------
           Plugin.Carts.WriteCartTotalsToContextBlock (Sitecore.Commerce.Plugin.Carts.Cart => Sitecore.Commerce.Plugin.Carts.Cart)
      
    • Exclusive promotions apply against the benefit type only, therefore it is possible to apply a cart line level exclusive promotion and a cart level exclusive promotion at the same time.

Evaluating Promotions

The following diagram shows the pipelines and pipeline blocks that are called during the process of evaluating the applicable promotions and additional filtering for exclusive promotion evaluation.

There are essentially 10 steps that make up the evaluation process:

  1. Search For Promotions: Retrieves all promotions.
  2. Filter Promotions By Valid Date: Removes promotions that do not fall within the Valid From/To dates based on effective date.
  3. Filter Not Approved Promotions: Removes promotions that are not approved and, if the promotion is disabled, where the effective date is prior to the updated date (the date the promotion was disabled). The latter rule is to allow the promotion to be active when reviewed the storefront at a previous point in time.
    Note: From 9.0.1, the GlobalPromotionsPolicy was introduced to allow promotions to be previewed in the storefront prior to submitting a promotion for approval.
  4. Filter Promotions By Items: Removes promotions where the cart contains no sellable items marked as included in the ItemsCollection qualification or where the cart contains any of the sellable items marked as excluded in the ItemsCollection qualification.
  5. Filter Promotions By Book Associated Catalogs: Removes promotions where the catalog, associated to its promotion book, does not match any of the catalogs associated to the sellable items of the cart lines.
  6. Filter Promotions By Benefit Type: Removes promotions where the type of benefits configured to the promotion does not match the benefit type being evaluated. i.e. Cart line level benefits (CartLineActions) and cart level benefits (CartActions) for CalculateCartLinesPipeline and CalculateCartLinesPipeline respectively.
  7. Filter Promotions By Coupon: Removes promotions that require a coupon that has not been applied to the cart.
  8. Evaluate Promotions: Filters out promotions where promotion qualifications and benefit rules do are not applicable to the current cart.
  9. Filter Promotions With Coupons By Exclusivity: If exclusive coupon promotions are present in the promotions list, the list will be filtered down to a single exclusive coupon promotion. The promotion will be determined by the Added date that their corresponding coupons were applied to the cart.
  10. Filter Promotions By Exclusivity: If exclusive automatic promotions are present in the promotions list, the list will be filtered down to a single exclusive automatic promotion. The promotion will be determined by the earliest (oldest) Valid From date, and in the event of multiple promotions sharing the same earliest Valid From date the promotion that was created earliest will take win.

Promotion Priorisation Rules

While the previous section covered how promotions are evaluated, and also provided some insight into promotion priorisation, we will now cover the prioritisation rules.

The following diagram shows the logic used to determine which promotion(s) to apply to the cart.

There are essentially 3 steps that make up the application process:

  1. Apply a single exclusive automatic promotion.
    • The promotion will be determined by the earliest (oldest) Valid From date, and in the event of multiple promotions sharing the same earliest Valid From date the promotion that was created earliest will take win.
    • If a promotion is applied here no further promotions are applied.
  2. Apply a single exclusive coupon promotion.
    • The promotion will be determined by the Added date that their corresponding coupons were applied to the cart.
    • If a promotion is applied here no further promotions are applied.
  3. Apply all non-exclusive promotions.
    • The promotion order will be determined by:
      1. Automatic promotions ordered by earliest (oldest) Valid From date, and in the event of multiple promotions sharing the same earliest Valid From date the promotion that was created earliest will take win.
      2. Coupon Promotions ordered by earliest Added date that their corresponding coupons were applied to the cart.

References