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

Transitioning from Sitecore Experience Commerce to OrderCloud: Order Workflow and Minions

Reading Time: 8 minutes

In this article, we will review and compare the order workflows 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 also review minions from XC and how this these processes can be brought over into OrderCloud to facilitate order workflow automation.

We will look at a high-level comparison of architecture, functionality, and data models, where applicable, 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.

Order Workflow

In XC, the default order workflow demonstrates the capabilities of post-order placement processing where order status transitions are achieved through manual intervention or automated processes (minions). It is intended to be extended and customised to cater for the unique business requirements of each project.

While figure 1 represents the default order workflow, XC can be customised to create or remove order statuses, and create or remove the triggers that transition an order between statuses, e.g. manual intervention via business users (customer service representatives) or automated processes such as minions.

Figure 1: XC’s order status workflow.

To provide clarity regarding each order status, XC’s definitions are:

  • Pending: Initial order state – order paid, no fulfillment.
  • OnHold: Prevents order from being progressed by minions. Customer service representatives may modify, line items, payments, shipments, etc.
  • WaitingForAvailability: One or more items in the order are not available. This status is expected for items on backorder or preorder.
  • Released: Intended status for indicating the order passed validation and is ready for shipping. It is also typically used in customisations to the workflow to indicate the order has been exported to an external order management system (OMS).
  • Problem: An order that has an unexpected status when processed by minions. Manual intervention by customer service representatives is available to transition the order back into a state to be picked up my the minions again, however this typically indicates a bug within custom implementations, which requires developer intervention.
  • Completed: Order has been shipped, has had entitlements generated, and has nothing left to process. Complete orders are eligible for the return mechandise authorisation (RMA) processes.
  • Cancelled: Order has been cancelled and can no longer be processed by minions or customer service representatives.

In OrderCloud, the order status workflow differs to XC’s in that order submission can include an approval process, which is typically a B2B order scenario, while the post-order placement workflow for order status transitions is minimal, as per figure 2.

To implement a custom order workflow, we will cover an approach below in Custom Order Status in OrderCloud.

Figure 2: OrderCloud’s order status workflow.

OrderCloud’s order status definitions are:

  • Unsubmitted: Created but has not yet been submitted.
  • Open: Submitted but not yet fulfilled.
  • AwaitingApproval: On hold awaiting approval.
  • Declined: Submitted but has been declined.
  • Completed: Submitted and all the line items on the order have been shipped.
  • Canceled: Order can no longer be submitted.

Removing the order statuses for approval and the cart equivalent “Unsubmitted” status, we see that OrderCloud simply transitions between Open (XC’s Pending) and Completed or Canceled statuses.

Figure 3: OrderCloud’s order status workflow (XC equivalent).

Custom Order Status in OrderCloud

To achieve an XC equivalent order workflow in OrderCloud, the missing order statuses may be introduced under OrderCloud’s Open status, using xp to represent the custom status. Typically, this would be achieved by PATCHing the order’s xp, using either middleware as a proxy to support wrap OrderCloud API endpoints with custom logic, or have middleware perform validation via pre-hook webhooks to prevent invalid status transitions.

There is no restriction in modifying the workflow or introducing new statuses via xp, only considerations for ensuring that the workflow transitions from end-to-end as intended.

Figure 4: The XC workflow equivalent in OrderCloud.

Transaction and Payment Status

We previously have covered the payment concepts between XC and OrderCloud in Tax and Payments, but what we didn’t cover was the statuses that belong to these entities and how they can be utilised in the SXA storefront.

For Sitecore Experience Commerce, the federated payment components have a TransactionStatus property, which is intended on tracking the raw statuses specific to the payment provider. They are typically used only for transparency and reconciliation purposes, rather than driving business logic in XC.

Figure 5: The default workflow for transaction status of federated payments.

Sales activities have a PaymentStatus property, which also is intended on tracking the transaction status in a more agnostic fashion by reducing payment provider statuses to more generic status buckets, which can then be used to influence business logic. In Order Workflow, figure 1 shows that the payment status affecting the order status, namely by placing an order into a problem state where the payment status is set as Problem.

Figure 6 and 7 shows the workflows for payment sales activities in XC, which modifies existing sales activity for subsequent transactions, hence representing as transitions, however this is commonly customised to create a separate sales activity per transition for a complete historical view of events and reconciliation reporting.

Figure: 6: The default workflow for payment status of federated payments.
Figure 7: The default workflow for payment status of gift cards.

Translating XC’s payments over to OrderCloud payments resources, we see that transactions have the ResultCode property, which could be considered the equivalent to XC’s payment status, while XC’s transaction status could be stored on the payment’s xp. Looking at the payments resource in OrderCloud however we see that the intended architecture for transactions is to track each transaction for a payment rather than updating the its status, as the transactions can be created or deleted, but not updated.

Rather than porting XC’s architecture over to OrderCloud directly, there are some improvements we could introduce to reduce the complexity of the overall solution, which includes the business logic used in the order workflow and not just recreating the payment entities like for like. For example, if we treat transactions as a snapshot in time for a given payment, rather than a dynamic object representing only the most recent transaction, the ResultCode would probably be better can be used for the raw transaction status given by the payment provider.

Figure 8: Transaction ResultCode (status) workflow. Each status will be represented by its own transaction.

For the overall payment status, we can use the payment’s xp to maintain the most recent transaction status in a normalised form, agnostic of payment provider, which would simply be translated as part of the payment provider integration.

Figure 9: Payment status workflow example in OrderCloud.

Order Minions

Order minions are background jobs that process all orders in a given status to either perform some function on and/with the order, before transitioning it to its next status.

Pending Orders Minion

The pending orders minion is a shell for processing orders fresh from being placed. It is expected to be customised to fit each project’s requirements, and can be thought of as an extension of the create order process, delegated with performing asynchronous activities in a separate Commerce Engine running the minions role, which typically would run on dedicated resources. This is to mitigate any performance impacts on the Commerce Engine instance(s) running the shops role, which serves the storefront’s live traffic.

The pending order minion will usually be extended with processes such as exporting the order to an order management system (OMS), sending an order invoices via an email service provider, generating gift cards, activating subscriptions, and more.

In OrderCloud, we will review the order submit integration event and batch order processing approaches to process pending orders.

Order Submit Integration Event

The order submit integration event is a non-blocking asynchronous call from OrderCloud, calling a job runner application, allowing an order to be processed immediately without blocking the storefront/buyer application. The job runner application, in this case, will update the order’s custom status in the xp, so that other automated jobs can process the order appropriately within the order workflow.

Third party integrations, such as Azure Service Bus, may further faciliate mitigating transient issues, increase resilience, and offload processing power from the job runner application.

Figure 10: Order submit integration event.

Batch Order Processing

An alternate approach to integration events for processing orders is batch jobs, which is more closely aligned to pending orders minion behaviour, by leveraging the custom order status in the order xp and the ElasticSearch backed premium search for orders.

Figure 11: Batch processing pending orders.

Released Orders Minion

The released orders minion processes orders in the released state by creating shipments against the order, generating entitlements, capture payments, then transition the order into a completed state as depicted in figure 12. We don’t need to bring over fault pipeline blocks, and as discussed in Orders – Shipments, we will be able to fold the shipment generation together.

Figure 12: A comparison of XC’s released order process and an equivalent approach in OrderCloud.

To replicate the released orders minion in OrderCloud, we would need to follow the batch processing orders approach as described for the pending order minion equivalent behaviour.

While it might seem that the pending orders minion and released orders minion should be folded into a single process, the intention behind segregating these order states into separate processing jobs is due to the expected customisations, which may see manual triggering/transitioning or delayed processing between order states that will be considered against the project’s business requirements.

Figure 13: Batch processing released orders.

Waiting For Availability Orders Minion

The waiting for availability order minion simply iterates over orders in registered to the WaitingForOrders list and determines if all products on the order have availability, in which they are returned to the Pending status/list. It is intended to support back-order and preorder scenarios, however as these features aren’t completely functional, customisation is required to complete the implementation and cater for any unique business requirements.

Pre-order and backorder inventory features are not natively supported in OrderCloud and as XC’s implementation is also incomplete, we won’t cover how to port this across in detail, only to highlight that we can leverage the same batch processing approach we have already outlined as a framework for implementing this as an automated job.

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

Transitioning from Sitecore Experience Commerce to OrderCloud: Orders

Reading Time: 10 minutes

In this article, we will review and compare orders and related features 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.

Conceptual Architecture and Features

Journalling (Auditing)

The journalling functionality in XC takes a snapshot of an entity when it is persisted as a means of effectively producing a change log against the entity for auditing and historical purposes. The entities are registered to the commerce role configuration under the EntityJournalingPolicy, and only the order entity is registered to the policy as part of the habitat role configurations by default.

While OrderCloud does not currently expose its logs via the API, two approaches for implementing audit logs are webhooks and middleware proxy requests, both of which require a custom database to store the audit snapshots.

The Webhook Approach

While webhooks may turn out to be sufficient for your auditing requirements, there are considerations to be aware of to ensure that you don’t get caught out with these gotchas.

Regardless if you choose to use pre-hooks or post-hooks, OrderCloud may touch multiple entities within a single request that could impact the completeness of the implementation, leading to the inability for complete reconciliation and may be mistaken as data corruption.

The post-hook webhooks will forward on OrderCloud’s response and user token to the webhooks url (typically the middleware), The user token can be parsed to retrieve information about the user making the request.

Note: Assignment requests return a response code of 204 and an empty body. The assignment model will not be available in these requests.

The pre-hook webhooks on the other hand, capture the request body, however creating a log at this point would be premature as the OrderCloud API will typically rollback or not commit changes until the request has been fully validated and processed. Pre-hooks are also a blocking process, which will lead to performance impacts on your solution.

The Middleware Proxy Approach

Using middleware as a proxy for all OrderCloud requests that will have auditing attached may provide the most complete approach but may also come with the most complexity and performance implications.

The proxy request in middleware doesn’t mean you are limited to only calling the OrderCloud API in question, but can provide the opportunity to retrieve the entity prior to making changes, and then retreiving the entity after changes have been made for endpoints such as any PATCH endpoint, which doesn’t return the entity in the response. While this may seem great, you will be facing performance implications, which can only be managed through asynchronous calls to an extent. It is also possible to cater for multiple entities being touched in OrderCloud with this approach, but again will have performance implications.

Entitlements

In XC, entitlements can be considered as a product classification that typically represents a sellable item as a trackable unit of ownership or license, which may change from customer or ambient activity, such as redemptions or expiry. Upon purchase of a sellable item with an entitlement classification, the Commerce Engine will create an specialised entitlement commerce entity, which can then be further customised for extended functionality. for the spei include gift cards, installations, warrranties, services and subscriptions.

Figure 1: Entitlement creation from order.

The entitlements are created with entity references to the order and the customer entities, while the order and customer entities are also updated with entity references back to the entitlements to form two-way relationships.

Figure 2: Entitlement architecture in XC.

As OrderCloud does not have any native support for entitlements, we are still able to support this feature via customisation with considerations in mind. Most importantly, xp for any given entity is limited to 8000 bytes, therefore storing all entitlement data directly on the buyer user’s xp could see the character limit be exceeded over time as more entitlements are created for the buyer user. Instead, a third-party database, such as Cosmos DB, can be utilised to store the entitlement data, while only storing the reference id of the entitlement record against the buyer user’s xp, minimising the required xp storage.

Figure 3: Entitlement architecture in an OrderCloud solution.

Shipments

In Fulfillment to Shipping, we covered the fulfillment components and shipping estimates that are applied to orders to gather fulfillment information during the storefront checkout delivery step. Post order-placement, the actual fulfillment of the order may or may not be in alignment with representation of fulfillment in the storefront and can be controlled via customisation. For example, the shipping details may have been collected for the entire order and a flat fee charged, however business processes may see the order be fulfilled across multiple shipments. Any variance to actual shipping costs from the estimates would typically not be reconciled with the customer.

XC provides the shipment commerce entity and, as part of the Released Orders Minion, will generate a shipment for the order or for each line item, respective to whether cart-level or cart line-level fulfillments were applied to the cart during the checkout steps.

Figure 4: XC shipments for cart-level and cart line-level fulfillments.

In OrderCloud, shipment flexibility is increased as shipments are not bound to the ship estimates, like XC’s shipments to physical fulfillments. Shipments can be created against the entire order or a subset of line items with support for partial quantity fulfillment.

Figure 5: OrderCloud shipments for order-level and line-level fulfillments do not have to align to ship estimates.

Return Merchandise Authorisation (RMA)

RMAs in the Commerce Engine are another partial implementation to get faciliate development by providing the foundation. This is commonly an area of customisation to allow RMAs to fit with the business processes, e.g. restricting returns on certain items, calculating refunds, determining whether the inventory needs to be incremented on a return, etc. The RMA functionality allows customer service representatives to raise a return request, with a line item, quantity, and reason for return, and the RMA will be created with an approval process. The remaining functionality is introduced via customisation.

In OrderCloud, order returns are a recently released feature that has more complete functionality than XC’s RMA offering. A return can be created against multiple line items, which also consists of an approval workflow and custom refund calculations can be achieved via the OrderReturn integration event.

Functionality

The Storefront Checkout Order Submission Step

In XC, submitting an order is not just about converting a cart to an order, but performing necessary processes such as payment authorisation and validating the order is in a valid state prior to finalising the order. The following sections cover these processes that we would look to transfer into an OrderCloud implementation.

Turning our attention to the storefront functionality of the order confirmation checkout step from Carts to Unsubmitted Orders: Storefront User Journey, we will look at adding the email address and payment details to the cart/unsubmitted order, focusing on varying APIs of the two platforms.

Figure 6: The order submission aspect of the storefront user journey.

When submitting an order in XC, the Commerce Engine will perform validations and processes that accompany order creation, which is depicted at a high level in figure 7. The darker nodes represent functionality that is built into OrderCloud’s order submit endpoint, while the nodes in italics represent functionality we wouldn’t need to bring over to OrderCloud. This leaves the Authorise Payment and Assign Confirmation ID as the custom functionality we would need to incorporate into an OrderCloud solution.

Figure 7: A comparison of XC’s create order process and an equivalent approach in OrderCloud.
Order Validation

When submitting orders in XC, the Commerce Engine ensures the cart is in a valid state prior to converting it to an order. The validation can be tightened through customisation for business specific validation requirements, but for the default validation rules, the Commerce Engine will evaluate the following:

  • The payments total matches the cart’s grand total.
  • An email is assigned to the cart.
  • The cart has line items.
  • Applied coupons for promotions are eligible.
  • Applied promotions are eligible.
  • Inventory availability is not exceeded.

Similarly, OrderCloud’s submit order endpoint validates an order with the following:

  • The order has line items.
  • The order wasn’t already submitted.
  • The order has been calculated via integration event, if applicable.
  • All payments have been accepted.
  • Applied promotions are eligible.
  • Inventory availability is not exceeded.

The difference validate gaps between the two platforms that we will need to implement for parity:

  • The payments total matches the order’s grand total.
  • An email is assigned to the order.

For more details on the internal logic of OrderCloud’s order submit process, including validation, see Order Submit Logic.

Payment Capture
Credit Cards

The SXA storefront provides an auth-capture implementation for braintree payments where upon order submission the payment is authorised and during the released order minion the payment is captured.

Figure 8: High level credit card payment flow in XC.

In OrderCloud, the payments resource represents credit card payments and validation is independent of the order creation and validation processes. In figure 10, we see that the authorise payment is incorporated as part of the middleware’s order creation process.

Gift Cards

For the gift card implementation, the gift card amount is captured post order placement, during the pending orders minion.

XC’s gift card implementation is only intended as a sample, which would typically be replaced with an integration with a gift card provider, so we typically wouldn’t need to focus so much on the default XC implementation and instead focus on porting the custom integration.

Figure 9: High level gift card payment flow in XC.

For gift cards, in the previous article, Taxes and Payments, we identified that the gift cards were represented as spending accounts in OrderCloud.

Order Confirmation Id

The order’s OrderConfirmationId is generated as part of order creation, however this is commonly overwritten with an order number incrementor implementation for a user-friendly confirmation id or an external system (e.g. OMS) identifier for reconciliation purposes.

In OrderCloud, generating unique order IDs is a native feature. While entity IDs are generated with a unique alphanumeric characters, OrderCloud’s incrementors resource can be configured and referenced when explicitly assigning an entity ID. This is achieved by wrapping the incrementor ID in curly braces, which also supports prefixed and suffixed values for even further versatility.

// Request Body
{
  "ID": "MyPrefix-{MyOrderIncrementorID}"
}

// Response Body
{
  "ID": "MyPrefix-010010",
  ...
  "IsSubmitted": false,
  "xp": null
}

A few consideration when it comes to the order confirmation IDs:

  • In a multi-tenant solution, you can create a unique incrementor, which may create the same incrementor value, e.g. 010010, however when assigning an order ID, a unique storefront prefix can prevent duplicate IDs from being created, e.g. <Storefront 1>-{Storefront1OrderIncrementorID}.
  • If your solution cannot guarantee unique IDs for orders across the Marketplace, the ID can be stored on the order’s xp instead.
Order Submission by Middleware Proxy

In reviewing the order submission process between XC and OrderCloud, it’s important to understand that XC development methodology differs from OrderCloud in that XC allows the implementor to extend and modify the Commerce Engine directly, while OrderCloud opts to leave customisation to the implementor by way of middleware to supplement and enhance the platforms native behaviour. We will generally be able to achieve close parity when it comes to the happy paths, while unhappy paths will rely on the middleware to perform the order submit request along with surrounding custom logic.

In OrderCloud, we have the ability to extend platform functionality prior to execution by using middleware as a proxy, wrapping the OrderCloud API endpoints, or creating pre-hook webhooks to intercept an API request. Both approaches have merit and will typically be a design and implementation decision.

For more information on extending OrderCloud, see Flexible Fullfilment – Extending OrderCloud.

In our happy path only example, figure 10 provides an OrderCloud solution equivalent of XC’s create order process, by wrapping OrderCloud’s order submit and several other related endpoints, encompassing all aspects of the order submission process.

  1. By calling the order validate endpoint, we ensure that no stale data, in particular order calculations, are being retrieved from the cache of other API calls we will make, such as getting the order worksheet.
  2. The order worksheet is retrieved to perform custom validation.
  3. Payments are not stored on the order worksheet at this time, so it is necessary to retrieve payments separate to the order.
  4. For our custom validation:
    1. We identified in Tax and Payments that XC has an order-specific email, which may not be the user’s address, so we would need to confirm that the order-specific email has been provided on the order.
    2. We also acknowledged earlier that XC validates the cart’s payments total against the grand total, so this is another custom validation we can implement in our OrderCloud solution.
  5. While XC doesn’t create a sales activity until the post-order processing stage, OrderCloud transactions are required to keep track of payment activity.
  6. Patching the order request covers:
    1. Updating the order confirmation id with our incrementor id. It is possible that the order’s ID is generated upon initial creation instead of during the order creation process, however this will see submitted orders not necessarily coming in in a linear fashion and potentially many generated IDs being discarded due to order abandonment.
    2. Applying the initial custom order status, which we will visit in a later article.
Figure 10: An example approach for XC create order equivalent in an OrderCloud solution.

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: Order Submit Logic

Reading Time: 4 minutes

In this article, we will review the business logic that Sitecore OrderCloud utilises when the order submit endpoint (POST /orders/{direction}/{orderID}/submit) is executed for a better checkout design, implementation and error handling.

Introduction

When it comes to submitting an order that is either incomplete or has become invalid, the OrderCloud API provides fairly easy to understand error codes to facilitate troubleshooting. What is not provided however, is a complete list of error codes that one may expect from an endpoint in order to be proactive in catering for such situations.

While a global catch all may be the quickest and easiest way to approach order submission error handling, the user experience of being informed that “A technical difficulty has occurred. Please contact support” in your storefront’s checkout will likely have a negative impact on the conversion rate. In addition, the admin application tends to be neglected with the love required to support business users in identifying and rectifying the issues themselves, so more often than not it is the developer to the rescue, but there are ways we can prevent these scenarios from occurring.

As OrderCloud is always utilising the latest code base, it is possible that the documented processes can change over time without warning.

Order Submission Logic

The following sections covering order submission logic, will highlight most of the platform logic, but will omit more trivial details.

When an exception is thrown by the API, denoted by the red End terminators with given error code, any pending changes to the underlying database entities are rolled back.

Order Submit

The order submit logic is essentially the entry point for validating and processing a submitted order.

Figure 1: Order submit logic.

Validate Order

The validate order process not only used in the order submit process, but is the entire validation taking place for the order validate endpoints.

In figure 2, soft errors, represented by the red boxes, allows the API to validate other areas of the order and return errors in list form, which can see your overall solution being more performant with less back and forth to resolve all order validation errors.

The Deduct inventory sub-process will validate inventory is available for the order, but does not persist inventory changes as this can be found in figure 1, under Commit inventory.

Figure 2: Validate order logic.

Validate Buyer-Seller Relationship

The buyer-seller relationship effectively validates that the user is authorised to purchase an order directly from a supplier. The relationship can be implicit, i.e. the supplier can be configured with AllBuyerCanOrder set as true or the user is an admin user, or an explicit relationship between the buyer and supplier has been defined.

Figure 3: Validate buyer-seller relationship logic.

Validate Promotions

The validate promotions logic in figure 4 shows the relevant flow for the order submit process.

Figure 4: Validate promotions logic.

Validate Promotion

For each promotion applied to the order, it is validated during the order submit to ensure it is still eligible. Any and all errors found are added to the error list, which will be returned by the API as part of the validate order logic in figure 2.

Figure 5: Validate promotion logic.

Deduct Line Item Inventory

Order inventory will be validated to ensure that there is sufficient inventory for all order lines. Any and all errors found are added to the error list, which will be returned by the API as part of the validate order logic in figure 2.

Figure 6: Deduct line item inventory logic.

Validate and Adjust Spending Account

For the sub-process Validate and adjust spending accounts in figure 1, the following flowchart represents how each spending account is evaluated as they are iterated over for all applied payments for the order.

Figure 7: Validate and adjust spending account logic.

Summary

With this insight into the order submit processes, you should now be well equipped to identify how extending OrderCloud order submit functionality through webhooks, wrapped API calls, and integration events fit in with your solutions, as well as anticipate the many errors that can occur and be more proactive with more specific error handling over a global catch all implementation.

References

Sitecore Experience Commerce: Adding Custom Properties to Search in BizFx

Reading Time: 8 minutes

In this article, we will review how we can extend the search components in Business Tools (BizFx) to support search functionality using custom properties for its results.

This article will not consider locale-support.

Introduction

As the search functionality in BizFx is fairly primitive, from a business user perspective, and a common request is to provide customisation guidance for the search functionality in the Business Tools, we will use the Search component from the Merchandising dashboard for our example in extending the search functionality.

Figure 1: Search component in the Merchandising Dashboard.

This implementation will assume the sellable items that have custom properties added via composer templates, however you will be able to adapt the solution where properties have been added programatically with components or policies.

Figure 2: A sellable item extended with a composer template.

Implementation and Configuration

Updating the Solr Schema

We first need to add our custom property to the managed schema of our Solr cores related to our search scopes. These schemas are found in the file system where Solr is deployed, under server/solr/<search scope>/conf/managed-schema.

The commerce engine uses switch on rebuild so we need to update the managed schema for both CatalogItemsScope and CatalogItemsScope-Rebuild for this instance.

    <!-- CommerceEngine Catalog -->
    <field name="displayname" type="string" indexed="true" stored="true"/>
    <field name="datecreated" type="pdate" indexed="true" stored="true"/>
    <field name="dateupdated" type="pdate" indexed="true" stored="true"/>
    <field name="artifactstoreid" type="string" indexed="true" stored="true"/>
    <field name="parentcataloglist" type="string" indexed="true" stored="true" multivalued="true"/>
    <field name="variantid" type="string" indexed="true" stored="true"/>
    <field name="variantdisplayname" type="string" indexed="true" stored="true"/>
    <field name="inventorysetids" type="string" indexed="true" stored="true"/>
    <field name="productid" type="string" indexed="true" stored="true"/>
    <field name="name" type="string" indexed="true" stored="true"/>
    <field name="yearmanufactured" type="string" indexed="true" stored="true"/>

The managed-schema that would be created with the XC instance will be organised to easily identify the commerce entity fields. While fields can be added via Solr, the managed-schema will be regenerated in a less XC friendly manner and make long-term mainentance more difficult.

With our Solr schema updated, if we are working locally, we will need to restart the Solr service, otherwise you will need to manage the deployment of the schema to your deployment environments.

Populating the Index’ Custom Fields

The commerce indexes are created and updated by both the FullIndexMinion and the IncrementalIndexMinion. It is within these minions that we can inject our custom code to populate the new custom fields with our commerce entity properties.

We’ll create a new pipeline block, called InitializeExtendedSellableItemIndexingViewBlock, which we can see the Run() method in the following snippet. We perform the necessary validation and copy our entity properties to custom index fields.

public override EntityView Run(EntityView arg, CommercePipelineExecutionContext context)
{
    // 1. Validation
    Condition.Requires(arg, nameof(arg)).IsNotNull();
    Condition.Requires(context, nameof(context)).IsNotNull();

    var argument = context.CommerceContext.GetObjects<SearchIndexMinionArgument>().FirstOrDefault();
    if (string.IsNullOrEmpty(argument?.Policy?.Name))
    {
        return arg;
    }

    // 2. Prepare Entities
    var entityItems = argument.Entities?.OfType<SellableItem>().ToList();
    if (entityItems == null || !entityItems.Any())
    {
        return arg;
    }

    // 3. Prepare custom properties
    var scopeIndexablePropertiesPolicy = IndexablePropertiesPolicy.GetPolicyByScope(context.CommerceContext, context.CommerceContext.Environment, argument.Policy.Name);
    if (scopeIndexablePropertiesPolicy?.ComposerProperties == null || !scopeIndexablePropertiesPolicy.ComposerProperties.Any())
    {
        return arg;
    }

    var searchViewNames = context.GetPolicy<KnownSearchViewsPolicy>();
    var childViews = arg.ChildViews.OfType<EntityView>().ToList();

    // 4. Iterate over each entity
    foreach (var si in argument.Entities.OfType<SellableItem>())
    {
        // 5. Get existing document entity view
        var documentView = childViews.First(v => v.EntityId.EqualsOrdinalIgnoreCase(si.Id)
            && v.Name.EqualsOrdinalIgnoreCase(searchViewNames.Document));

        // 6. Add custom fields
        AddComposerFields(si, documentView, scopeIndexablePropertiesPolicy);
    }

    return arg;
}

The highlighted section, 3. Prepare custom properties, is used for our AddComposerFields() method.

To add properties to the Solr document, we only need to register the field name and value to a new ViewProperty of the document EntityView and the platform will later translate and push this data into Solr. This would typically look like the following.

documentView.Properties.Add(new ViewProperty
{
    Name = "<Insert Field Name Here>",
    RawValue = "<Insert Field Value Here>"
});

In our AddComposerFields() implementation, we leverage our custom policy to identify and process fields from the composer templates.

protected void AddComposerFields(
    SellableItem si,
    EntityView documentView,
    IndexablePropertiesPolicy scopeIndexablePropertiesPolicy)
{
    // 1. Iterate over each composer template configuration
    foreach (var composerView in scopeIndexablePropertiesPolicy.ComposerProperties)
    {
        // 2. Get property value
        var composerEntityView = si.GetComposerViewFromName(composerView.Key);

        if (composerEntityView == null)
        {
            continue;
        }

        // 3. Iterate over each custom property to index
        foreach (var property in composerView.Value)
        {
            var value = composerEntityView.GetPropertyValue(property.PropertyName);

            if (value == null)
            {
                continue;
            }

            // 4. Add property to index document
            documentView.Properties.Add(new ViewProperty
            {
                Name = property.IndexFieldName,
                RawValue = value
            });
        }
    }
}

The implementation is made flexible by avoiding hard-coding any composer template or property names and instead using the custom IndexablePropertiesPolicy, which we append our configured policy to the SolrSearchPolicySet. In ComposerProperties we add our composer template name, e.g. "ManufacturedDetails", and then an array of composer property models, where the PropertyName represents the composer template property name and the IndexFieldName represents the Solr core field name.

{
  "$type": "Ajsuth.Sample.Catalog.Search.Engine.Policies.IndexablePropertiesPolicy, Ajsuth.Sample.Catalog.Search.Engine",
  "SearchScopeName": "CatalogItemsScope",
  "ComposerProperties": {
    "ManufacturedDetails": [
      {
        "PropertyName": "YearManfactured",
        "IndexFieldName": "yearmanufactured"
      }
    ]
  }
}

We register our pipeline block to both the full and incremental index minion pipelines.

.ConfigurePipeline<IIncrementalIndexMinionPipeline>(pipeline => pipeline
    .Add<Pipelines.Blocks.InitializeExtendedSellableItemIndexingViewBlock>()
        .After<InitializeSellableItemIndexingViewBlock>()
)

.ConfigurePipeline<IFullIndexMinionPipeline>(pipeline =>pipeline
    .Add<Pipelines.Blocks.InitializeExtendedSellableItemIndexingViewBlock>()
        .After<InitializeSellableItemIndexingViewBlock>()
)

By adding our InitializeExtendedSellableItemIndexingViewBlock after the InitializeSellableItemIndexingViewBlock rather than directly replacing and overriding the initial platform pipeline block, we may suffer a minor performance impact as we will need to interate over our entities another time, however we won’t have to copy platform code into our pipeline block, which will ultimately keep our code as clean as possible while preventing complexity for during upgrades.

We then follow our deployment process for our changes:

  1. Deploy our Solr managed-schemas and restart the Solr service to ingest the updated schemas.
  2. Deploying our Commerce Engine code to all instances.
  3. Bootstrap the Commerce Engine to ingest our IndexablePropertiesPolicy configuration.
  4. Restart the minions Commerce Engine instance to consume the updated policy set.
  5. Executing the Run FullIndex Minion – Catalog Items request from postman.
  6. Verify our Solr core now how our custom field indexed.
Figure 3: yearmanufactured property added to Solr core.

If you aren’t seeing the custom field for any indexed document check the *-Rebuild core as that may the current active core.

Searching Against Custom Fields in the Search Component

With the custom properties added to the indexes, we finally need to update the search component to include the new property as part of the search query.

Figure 4: Search does not query against the custom yearmanufactured field by default.

Using Solr Query Syntax

With no further customisation or configuration, we could use the Solr query syntax to apply to set the Search Term with the specific field query, e.g. yearmanufactured:2009, as the search component will process this as a raw query.

Figure 5: Search can query against the custom yearmanufactured field using Solr query syntax.

Including the Custom Field for SearchScope Queries

The search component queries against the Search Term properties appended to the _text_ field in Solr. If we wanted to add a custom field to default search query, then we can update the managed-schema of the Solr cores to include it. Below lists all of the fields that are used for the search queries as well as our custom field.

<copyField source="displayname" dest="_text_"/>
<copyField source="variantid" dest="_text_"/>
<copyField source="variantdisplayname" dest="_text_"/>
<copyField source="productid" dest="_text_"/>
<copyField  source="name" dest="_text_"/>
<copyField source="yearmanufactured" dest="_text_"/>

After deploying and restarting the Solr service, search component queries from the Business Tools will now query the custom fields as well in the default search.

Figure 6: Search queries against the custom yearmanufactured field by default with updated managed-schema.

Customising the Search Results

The last piece of the puzzle is to add our custom indexed fields to the Results entity view as seen below.

The Results entity view is populated with indexed commerce entity data and not raw commerce entity data.

First, we want to update the IndexablePolicy in the SearchPolicySet for our given search scope, with our custom field.

{
"$type": "Sitecore.Commerce.Plugin.Search.IndexablePolicy, Sitecore.Commerce.Plugin.Search",
"SearchScopeName": "CatalogItemsScope",
"Properties": {
  ...,
  "YearManufactured": {
    "TypeName": "System.String",
    "IsKey": false,
    "IsSearchable": true,
    "IsFilterable": true,
    "IsSortable": true,
    "IsFacetable": false,
    "IsRetrievable": true
  }
}

For our implementation purposes, we only need to be concerned about the IsRetrievable and IsSortable properties of the IndexableSettings object. The IsRetrievable property will allow us to provide a fallback property with an empty value, while the IsSortable property will configured the column to be sortable. Both follow the platform implementation pattern.

Next, I would like to tell you that we could extend the Results entity view simply by extending pipelines with additional pipeline blocks, but we will need to copy and replace the original pipeline blocks for the most consistent approach across commerce indexes. There is a ProcessDocumentSearchResultsBlock within the commerce plugins that will relate to the search scope, which we would copy into our solution. In this instance, we copy the pipeline block from the Sitecore.Commerce.Plugin.Catalog.dll.

Without re-writing the platform code, we will have to face a bit more rigid manual coding over a more automated, iterative approach to the IndexableSettings of our IndexablePolicy. The key area of the pipeline block we need to focus on is shown on line 5, where the results view properties are cleared. We need to copy our custom field from the properties before hand (1. Copy the field view property) and then repopulate the results view afterwards (3. Add desired indexed fields back into results view), which effectively strips the results view or the other indexed fields that we don’t want to render.

// 1. Copy the field view property 
var yearmanufactured = child.Properties.FirstOrDefault(p => p.Name.EqualsOrdinalIgnoreCase("YearManufactured"));

// 2. Clear the results view properties
child.Properties.Clear();

if (name != null)
{
    name.UiType = ViewsConstants.EntityLinkUiType;
}

// 3. Add desired indexed fields back into results view
child.AddViewPropertyToEntityViewOrDefault(name, retrievableProperties, CoreConstants.Name, typeof(string).FullName);
child.AddViewPropertyToEntityViewOrDefault(displayName, retrievableProperties, CoreConstants.DisplayName, typeof(string).FullName);
child.AddViewPropertyToEntityViewOrDefault(variantId, retrievableProperties, CatalogConstants.VariantId, typeof(string).FullName);
child.AddViewPropertyToEntityViewOrDefault(variantDisplayName, retrievableProperties, CatalogConstants.VariantDisplayName, typeof(string).FullName);
child.AddViewPropertyToEntityViewOrDefault(createdDate, retrievableProperties, CoreConstants.DateCreated, typeof(DateTimeOffset).FullName);
child.AddViewPropertyToEntityViewOrDefault(updatedDate, retrievableProperties, CoreConstants.DateUpdated, typeof(DateTimeOffset).FullName);
child.AddViewPropertyToEntityViewOrDefault(yearmanufactured, retrievableProperties, "YearManufactured", typeof(string).FullName);

We replace the platform pipeline block in SearchPipeline in our pipeline registrations.

.ConfigurePipeline<ISearchPipeline>(pipeline => pipeline
    .Replace<Sitecore.Commerce.Plugin.Catalog.ProcessDocumentSearchResultBlock, Pipelines.Blocks.ProcessDocumentSearchResultBlock>()
)

The platform pipeline block we are overriding is fully qualified here to to explicitly replace the correct pipeline block.

All that is left is to deploy the code, which is short looks requires:

  • Deploying our Commerce Engine code to all instances.
  • Bootstrap the Commerce Engine to ingest our updated IndexablePolicy configuration.
  • Restart the authoring Commerce Engine instance to consume the updated policy set.

Performing our search now, we will see that we have our custom field appended to the end of the Results entity view, which can be used to sort the results.

Figure 7: The Results entity view is customised with the yearmanufactured indexed field.

Summary

While the search component of BizFx was not developed with the intention of being configurable, we have identified a few ways to customise the search component, which will hopefully cover most business user scenarios and saves us from having to implement our own search component from scratch.

References

OrderCloud: Resource Dependency Cheat Sheet

Reading Time: 5 minutes

In this article, we will review the OrderCloud resources that we interact with directly or indirectly within the OrderCloud API to identify the dependencies that will guide the order of operations for implementations such as a data import. We will also identify a few platform nuances, or gotchas, to be aware of.

Introduction

We will start at the end, for quick reference, with the following dependency overview diagram shows OrderCloud resources, excluding the complete set of order resources, organised into priority groups. Each group number, represents the order in which data should typically be processed in an implementation to avoid any dependency conflicts in the OrderCloud API responses. For example, resources in group 1 would typically be executed before resources in group 2, group 2 before group 3, etc.

The overview diagram does not show the actual dependencies between resources, however in Resource Dependencies we break down each section (or logically grouped resources), aligned to the OrderCloud portal, to document the specific dependencies and note any nuances that should be considered when working with resources.

Figure 1: Resource dependency groups overview.

Resource Dependencies

In the following resource dependency graphs, the varying line colours are simply to differentiate the dependency references for each resource. The following legend also applies to the all graphs.

A resource reference is a reference in the resource’s url as opposed to the request body, e.g. /catalogs/{catalogID}/categories.

In order to understand the dependency graphs, we will look at the following example, where we want to import/sync supplier data via the OrderCloud API. If we were to attempt to create a supplier user where the supplier does not exist, we would encounter an EntityNotFound error in the API’s response.

Figure 2: Resource dependency example.

Using the dependency graph we can see that the supplier is a dependency and therefore if we aren’t planning on creating or updating the supplier then we should at least validate its existence before proceeding and with additional defensive programming we can proactively consider an alternate path of code logic instead.

private async Task&lt;User&gt; CreateSupplierUser(User supplierUser, string supplierID, string accessToken)
{
    var supplier = await oc.Suppliers.GetAsync(supplierID, accessToken);
    if (supplier == null)
    {
        // We implement our alternate code path here, for example:
        Logger.LogError($"Supplier '{supplierID}' should already exist. Supplier User '{supplierUser.ID}' will not be created.");
        return null;
    }

    return await oc.SupplierUsers.CreateAsync(supplierID, supplierUser, accessToken);
}

If we were importing all supplier information, using the dependency graph as a reference, we can identify the order of operations for our implementation to avoid dependency conflicts and optimise performance. For example, we know that we need to import suppliers before importing supplier users, while supplier addresses, suppliers users and supplier user groups can be safely imported asynchronously because they belong to the same dependency group.

// Group 1 dependencies
await ImportSuppliers();

// Group 2 dependencies
var supplierAddresses = ImportSupplierAddresses();
var supplierUsers = ImportSupplierUsers();
var supplierUserGroups = ImportSupplierUserGroups();
await Task.WhenAll(supplierAddresses, supplierUsers, supplierUserGroups);

// Group 3 dependencies
await ImportSupplierUserGroupAssignments();

Identifying the order of operations and employing defensive programming techniques will be critical in your solution design.

Authentication and Authorization Resources

Figure 3: Authentication and Authorization resource dependencies.

Seller

The seller resource dependencies have been broken out across two diagrams for clarity.

Figure 4: Seller resource dependencies.
Figure 5: Seller resource dependencies (cont.).

Buyers

The buyer resource has an implicit reference to the catalog resource as there is a nuance in the platform relating to buyer creation. When creating a buyer, the OrderCloud platform will create a catalog with the same ID. If a catalog with the same ID already exists, an IdExists error will be returned. However, if the catalog‘s ID is passed in as the DefaultCatalogID then the catalog will be associated to it instead, whereas if there is no catalog in existence an NotFound error will be returned.

Conditions1234
DefaultCatalogID providedNNYY
Catalog existsNYNY
Actions1234
Catalog created (201 response code)X
Catalog assigned to Buyer (201 response code)X
IdExists error (409 response code)X
NotFound error (404 response code)X
Table 1: Decision table for POST /buyers.
Figure 6: Buyer resource dependencies.

Suppliers

Figure 7: Supplier resource dependencies.

Product Catalogs

The product catalogs resource dependencies have been broken out across two diagrams for clarity.

Figure 8: Product Catalogs resource dependencies.

You will notice that the spec and spec option has a cyclical dependency. The nuance to the spec resource is that the DefaultOptionID cannot be assigned until after the spec option has been created, however it relies on the spec to exist. This chicken or egg scenario can be resolved by temporarily removing the DefaultOptionID before creating the spec and spec options then patching the value back onto the spec afterwards.

For the implicit references of Inventory > Product and Variant Inventory > Variant, the inventory objects updated on the Product and Variant input models, so they can be treated as being on the same level if applied together. However, the inventory cannot be created/updated without the existence on the Product/Variant entities.

// 1. Remove the DefaultOptionID from the spec prior to creation
var defaultOptionID = spec.DefaultOptionID;
spec.DefaultOptionID = null;
await orderCloudClient.Specs.CreateAsync(spec, accessToken);

// 2. Create the spec options
await ImportSpecOptions(specOptions);

// 3. Patch the spec's DefaultOptionID
await PatchSpecDefaultOptionID(spec, defaultOptionID);
Figure 9: Product Catalogs resource dependencies (cont.).

Orders and Fulfillment

For orders and fulfillment, we will focus on the promotion and seller approval rule resources as these a more typical of a Marketplace setup, whereasall other order resources typically represent historical order placement data, which is outside the scope of this article.

There is a seemingly cyclical dependency between the promotion and order resources, however the promotion-to-order is an implicit resource reference, which does not impact the validation logic for persistence of the promotion resource. Instead, it is used to identify the overall redemption count and user redemption count from orders placed and is used in the validation logic when applying a promotion to an order. When migrating data, excluding order data, between environments, this data inconsistency may be incorrectly identified as an environmental bug, which you can avoid troubleshooting with this knowledge.

Figure 10: Orders and Fulfillment resource dependencies.

Summary

Resource dependencies can be considered a huge gotcha when it comes to long-term maintenance. By having a sound knowledge of resource dependencies and nuances within the OrderCloud API, and employing defensive programming techniques, robust and performant ecommerce implementation can become a byproduct of good solution design.

References

Transitioning from Sitecore Experience Commerce to OrderCloud: Tax and Payments

Reading Time: 7 minutes

In this article, we will review and compare tax and payments 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.

Conceptual Architecture and Features

Tax

The Commerce Engine’s tax plugin is considered sample code that is sufficient enough to support basic tax requirements, however tax calculations will typically be customised to support the tax provider integrations that cater to tax laws for the countries and/or regions the storefront will be servicing.

The following list contains highlights of the tax plugin:

  • Tax is calculated as a cart adjustment when a fulfillment component is applied to the cart.
  • Tax is calculated as a cart line adjustment where a fullfillment component exists on the cart line.
  • XC waits for a cart to contain a fulfillment component before calculating taxes, as the delivery address is intended to identify applicable tax laws, but not catered for without customisation.
  • Sellable item pricing does not include tax.
  • Applying the tag taxexempt will omit sellable items from tax calculations.
  • Tax is calculated as a flat rate, i.e. tax is not dynamically calculated based on country/region tax laws.

The GlobalTaxPolicy provides the configuration properties of the sample tax plugin.

{
  "$type": "Sitecore.Commerce.Plugin.Tax.GlobalTaxPolicy, Sitecore.Commerce.Plugin.Tax",
  "DefaultCartTaxRate": 0.1,
  "DefaultItemTaxRate": 0.1
  "TaxExemptTagsEnabled":true,
  "TaxExemptTag":"taxexempt",
  "TaxCalculationEnabled": true, // Not hooked up
  "PriceIncudesTax": false, // Not hooked up
  "CalculateTaxBasedOn": "ShippingAddress", // Not hooked up
  "ShippingTaxClass": "CartItems", // Not hooked up
  "RoundAtSubTotal": false // Not hooked up
}

When it comes to tax in the OrderCloud platform, it is unopinionated on the methods of which to achieve tax calculations due to the numerous unique tax regulations around the world. Instead, OrderCloud allows the implementer to address tax calculations via the order calculate integration event, whether it be through integrations third party tax providers or complete custom implementation.

Figure 1 shows an overview of the order calculate integration event implementation in regards to calculating taxes. OrderCloud’s /calculate endpoint forwards the request along with the order worksheet to the middleware’s /ordercalculate endpoint. The middleware will then construct and return the OrderCalculateResponse model, containing the calculated TaxTotal, which OrderCloud then copy to the order’s TaxCost property. To track line item level taxes, calculated taxes can be stored in the OrderCalculateResponse‘s xp.

The TaxCost is used in OrderCloud’s calculations of the order Total, i.e. Total = Subtotal + TaxCost + ShippingCost - PromotionDiscount.

Figure 1: The order calculate integration event overview.

The calling system in figure 1 may be the client-side storefront or the middleware, depending on the implementation details.

Payments

Gift Cards vs Spending Accounts

XC provides a gift card payment type, which is integrated into the SXA storefront checkout to demonstrate end-to-end functionality of gift card payments. It is treated as a sample implementation as the functionality in the Commerce Engine is typically replaced with an integration to a gift card payment provider.

Reviewing OrderCloud’s supported payment types, spending accounts most closely resemble XC’s gift card implementation as it has the ability to track the available funds. One minor difference is that spending accounts are created under the context of a buyer and is therefore tied to the buyer users within that buyer, whereas gift cards in XC are globally accessible.

To differentiate spending accounts representing gift cards from other spending account types, xp can be leveraged to specify the type.

Figure 2: Gift card architecture comparison.

Global Payment Configuration

In an XC SXA implementation, payments are configured at a global level under sitecore/Commerce/Commerce Control Panel/Shared Settings, as depicted in figure 3. The configuration is split across Payment Option Types and Payment Options and linked via the PaymentOption Type property on the payment option item, however there really isn’t any discernable difference between the two within the SXA/Commerce Engine implementation.

Figure 3: Payment option to payment option type in the Sitecore Content Editor.

Instead of a BYO payment types approach, OrderCloud has defined three payment types, being purchase order, credit card, and spending account, with each type serving a different approach to handling order payments. For example, the credit card payment type would integrate with a payment service provider for perform transactions, while the spending account manages a balance maintained purely with OrderCloud, including adjusting the balance upon successful order submission.

Figure 4: Payment architecture overview.

As XC contains multiple Payment Option Types that could map to a single OrderCloud payment type, xp can be leveraged to represent multiple sub-types. The following table shows how XC’s Payment Option Types could map to Payment Types in OrderCloud.

Payment Option Type (XC)Payment Type (OrderCloud)xp (OrderCloud)
Card PaymentCredit Cardxp.Type: “Basic Card”
Federated PaymentCredit Cardxp.Type: “Braintree”
Gift Card PaymentSpending Accountxp.Type: “Gift Card”
Loyalty Card PaymentSpending Accountxp.Type: “Loyalty Card”

More information on payment types can be found in OrderCloud: Flexible Fulfillment > Apply Payments.

Storefront Payment Configuration

Payment options are also configured against an SXA storefront with a subset of those configured in the Shared Settings.

Figure 5: Storefront payment configuration.

While OrderCloud doesn’t have the context of storefronts, storefront-specific payment options will likely be a consideration for middleware’s logic for identifying applicable payments options.

Payment Components vs Payments

When it comes to the implementation, this is where the Commerce Engine plugins take the payment configurations and provide the functionality for the payment types. The sample Braintree plugin and the GiftCards plugin will add a FederatedPaymentComponent or a GiftCardComponent to the cart respectively, which will then be used to dictate how those payments are interacted with throughout the platform, during and post order creation.

OrderCloud uses the payments entity in a more concrete way, using the Type property to facilitate the payment behaviour. For example, selecting the spending account type will require the also specifying a spending account that is assigned to the buyer user via buyer or buyer user group assignment.

Figure 6: An order paid for using both a credit card and a gift card payment types.

Sales Activities vs Transactions

XC orders use sales activities to track the transaction activities of each order payment component while OrderCloud uses transactions for the same purpose.

Figure 7: Sales activities vs transaction architecture.

Functionality

The Storefront Checkout Billing Step

Turning our attention to the storefront functionality of the billing checkout steps from Carts to Unsubmitted Orders: Storefront User Journey, we will look at adding the email address and payment details to the cart/unsubmitted order, focusing on varying APIs of the two platforms.

Figure 8: The checkout billing aspects of the storefront user journey.

Viewing the interaction diagram in figure 9, the SXA storefront utilises the BillingDataJsonResult to provide the necessary data to render and facilitate payment option logic in the storefront. Once the user has submitted the payment details, all payment data is then sent to the Commerce Engine together to be added to the cart.

A few notes on the payment functionality:

  • Payments are only supported at the cart-level.
  • The email address is collected per order as the user may wish not to use the email address associated to their account (for registered users). This email is typically used for communications to the customer, such as email confirmation, invoicing, shipments, etc.
  • XC has logic to prevent a gift card sellable item to be purchased with a gift card payment.
Figure 9: The billing checkout step of the SXA storefront.

To replicate similar behaviour in an OrderCloud storefront implementation, figure 10 provides an approach to the billing checkout step.

  1. The /paymentoptions middleware endpoint retrieves the applicable payment options for the order, dependent on the storefront and business requirements.
  2. The email address is PATCHed to the order xp for specific order context.
  3. All payments can be added to the order via ../payments endpoint; the data sent may differ based on payment type.
Figure 10: An example approach for the delivery checkout step in an OrderCloud storefront.

Data Mapping

With the conceptual analysis above, we will now review what data mapping would look like for migration and from a comparison standpoint.

In the XC Entity/Component column, components are assumed to live on the primary XC entity being mapped.

OrderCloud IDs do not allow spaces. It is important that the IDs are parsed to remove/replace invalid characters consistently.

Spending Accounts

The following mapping shows how XC’s default implementation of gift cards can be translated into OrderCloud using spending accounts.

OC PropertyData TypeRequiredXC Entity/ComponentXC PropertyData TypeNotes
buyerIDstringYesN/AN/AN/A
IDstringNoGiftCardGiftCardCodestringUse format “GiftCard-<GiftCardCode>”
NamestringYesGiftCardNamestring
BalancedecimalYesGiftCardBalance.Amountdecimal
AllowAsPaymentMethodbooleanNoN/AN/AN/ASet to true.
RedemptionCodestringNoGiftCardGiftCardCodestring
StartDateDateTimeOffsetNoGiftCardActivationDateDateTimeOffset
EndDateDateTimeOffsetNoN/AN/AN/A
xpobjectNoN/AN/AN/A
xp.TypestringNoN/AN/AN/ASet to "GiftCard".
xp.InitialAmountdecimalNoGiftCardOriginalAmount.Amountdecimal
xp.CurrencystringNoGiftCardOriginalAmount.CurrencyCodestringRecommended for handling multi-currency validation.

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

Transitioning from Sitecore Experience Commerce to OrderCloud: Fulfillments to Shipping

Reading Time: 6 minutes

In this article, we will review and compare Sitecore Experience Commerce fulfillments and OrderCloud shipping 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.

Conceptual Architecture and Features

Fulfillment Configuration

Global Fulfillment Configuration

In an XC SXA implementation, fulfillments are configured at a global level under sitecore/Commerce/Commerce Control Panel/Shared Settings, as depicted in figure 1. The configuration is split across Fulfillment Option Types and Fulfillment Options and linked via the Fulfillment Option Type property on the fulfillment option item.

Figure 1: Fulfillment option to fulfillment option type in the Sitecore Content Editor.

This tiered architecture can be a little confusing, so each tier can be thought of as follows:

  • Fulfillment Option Type: The high-level approach in which the order will be fulfilled, e.g. Deliver to Address, Digital Delivery, Pick Up From Store, etc.
  • Fulfillment Option: The collection of methods for the type of fulfillment, e.g. Ship items. This could also be as considered a shipping vendor, such as DHL or FedEx.
  • Fulfillment Method: The specific method of delivery, which will be assigned to the order and may be accompanied by a fee.

The fulfillment fees associated to each fulfillment method are then configured in the Commerce Engine environment configuration under GlobalPhysicalFulfillmentPolicy.FulfillmentFees, which also supports multi-currency fees per fulfillment method.

"$type": "Sitecore.Commerce.Plugin.Fulfillment.GlobalPhysicalFulfillmentPolicy, Sitecore.Commerce.Plugin.Fulfillment",
"FulfillmentFees": {
  "$type": "System.Collections.Generic.List`1[[Sitecore.Commerce.Plugin.Fulfillment.FulfillmentFee, Sitecore.Commerce.Plugin.Fulfillment]], mscorlib",
  "$values": [
    {
      "$type": "Sitecore.Commerce.Plugin.Fulfillment.FulfillmentFee, Sitecore.Commerce.Plugin.Fulfillment",
      "Fee": {
        "$type": "Sitecore.Commerce.Core.Money, Sitecore.Commerce.Core",
        "CurrencyCode": "USD",
        "Amount": 15.0
      },
      "Name": "Ground",
      "Policies": {
        "$type": "System.Collections.Generic.List`1[[Sitecore.Commerce.Core.Policy, Sitecore.Commerce.Core]], mscorlib",
        "$values": []
      }
    },
    ...
}

Bringing this together into a friendly-ish diagram view, figure 2 shows the representation of fulfillment architecture across the Sitecore and Commerce Engine platforms.

Figure 2: Fulfillment architecture overview.

In OrderCloud, there are no fulfillment object equivalents to house and support fulfillment configurations. Instead, OrderCloud relies on the middleware application to provide its business logic, integrations, calculations, etc. to address the unique shipping business requirements for the OrderCloud storefront solution, which we cover in greater detail in Ship Estimates.

For now, figure 2 may be used as a reference point when evaluating how fulfillment/shipping data could be designed if business requirements identify that the custom middleware solution should persist shipping configuration data to a custom database.

Storefront Fulfillment Configuration

One last configuration requirement for an SXA storefront is to select the fulfillment options that will be applicable for that particular storefront.

Figure 3: Storefront fulfillment configuration.

Once again, OrderCloud lets the middleware manage the storefront specific shipping logic and calculations, which is covered in Ship Estimates, so this may just be a consideration for your custom middleware implementation.

Functionality

Ship Estimates

While OrderCloud depends on the middleware application to drive shipping data and behaviour, it does not leave the implementer high and dry. The order checkout integration event sees the /estimateshipping endpoint trigger the /ShippingRates endpoint of the middleware, handing it the order worksheet for context. The middleware can then execute its custom logic, which may include integrations to shipping providers, external systems, etc., to determine ship estimates.

The calling system in figure 4 may be the client-side storefront or the middleware, depending on the implementation details.

Figure 4: The order checkout integration event interaction overview.

The resulting ShipEstimateResponse model from the middleware’s /ShippingRates endpoint will be expected to consist of a breakdown of shipping estimates, representing a subset of line items and the shipping methods and costs that are available for the order.

In comparison to the SXA storefront, the ShipEstimateResponse model is basically the equivalent to the DeliveryDataJsonResult application model, which contains the ShippingOptions at the order and line levels.

Figure 5: The high-level view of ship estimates in a ShipEstimateResponse model.

For the integration event to trigger, the order checkout integration event will need to be configured with the middleware’s publicly accessible domain assigned to the CustomImplementationUrl of the integration event, and integration event’s ID assigned to the OrderCheckoutIntegrationEventID of the API Client.

The API Client used for the integration event will likely be the same client used for the Anonymous and Registered Users.

Figure 6: OrderCheckout integration event assignment to API client example.

The Storefront Checkout Delivery Step

Turning our attention to the storefront functionality of the delivery checkout steps from Carts to Unsubmitted Orders: Storefront User Journey, we will look at adding fulfillment details to the cart/unsubmitted order, focusing on varying APIs of the two platforms.

Figure 7: The fulfillment focused view of the storefront user journey.

Viewing the interaction diagram in figure 8, the SXA storefront utilises the DeliveryDataJsonResult to provide the necessary data to render and drive fulfillment functionality. Once the user has submitted the fulfillment details, all fulfillment data is then sent to the Commerce Engine together to be added to the cart.

A few notes on the fulfillment functionality:

  • Fulfillments can be set against the cart as a whole or for each and every cart line.
  • As sellable items can be classified as either a physical or digital item, XC contains logic to tie these to varying fulfillment types, which will collect different data relevant to its classification. For example, physical items require a physical fullfillment, consisting of a physical delivery address and the fulfillment method id, while digital items require digital fulfillments, consisting of recipient email address, gifting message and the fulfillment method id.
Figure 8: The delivery checkout step of the SXA storefront.

To replicate similar behaviour in an OrderCloud storefront implementation, figure 9 provides an approach to the delivery checkout step, which intentionally deviates from the SXA storefront functionality to demonstrate a more granular approach to building up the order using the OrderCloud API.

  1. The /shippingoptions middleware endpoint retrieves the available shipping options for the order, such as delivery or click and collect, with order-level or line-level shipping options, which will then drive storefront functionality for the various data collection methods for the respective shipping option selected.
  2. Fleshing out the approach for physical delivery shipping options, shipping addresses can be input manually or by referencing an existing address assigned to the user.
  3. The ../estimateshipping integration event can leverage the ShippingOption set on the order xp as well as the shipping details, such as shipping addresses, can be used for calculating shipping estimates by our middleware and third party integrations.
  4. The ../shipmethods sets the shipping methods for the order or line items.
Figure 9: An example approach for the delivery checkout step in an OrderCloud storefront.

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