Sitecore Experience Commerce: Promotion Evaluation and Application Logic

In this article, we will review the default business logic that the Commerce Engine utilises to evaluate and apply promotions.

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

Introduction

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

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

Evaluating Promotions

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

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

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

Promotion Priorisation Rules

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

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

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

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

References

Configuring and Customising SEO Friendly URLs in Sitecore Commerce SXA Storefront

In this article, we will look at the configuration and customisation options available for manipulating URLs in the Sitecore Commerce Storefront.

The goal will be to determine how to manipulate the URLs so that they are more SEO-friendly as represented by the following URL structures.

  • https://{domain}/category/{1st level category name}
  • https://{domain}/category/{1st level category name}/{2nd level category name}
  • https://{domain}/category/{1st level category name}/{2nd level category name}/{3rd level category name}
  • https://{domain}/product/{product id}

This would translate to the following examples

  • https://sxa.storefront.com/category/computers-and-tablets
  • https://sxa.storefront.com/category/computers-and-tablets/kids-tablets
  • https://sxa.storefront.com/category/computers-and-tablets/kids-tablets/boys-tablets
  • https://sxa.storefront.com/product/6042221

Throughout this article we will utilise the category Computers and Tablets > Kid’s Tablets and product Minnow Kid’s Tablet—7”, 8GB to review our progress. These examples also contain some non-alphanumeric characters to ensure we take these special characters into consideration.

How Storefront URLs are Generated

Storefront URLs are constructed using the configuration of the site’ s linkManager. The configuration is located at sitecore/linkManager/providers/add[@name=’commerce‘].

Configuration Properties

An important note about the provider configuration properties is that only 3 properties actually affect the generated URLs – includeFriendlyName, useShopLinks, and encodeNames.

  • includeFriendlyName: Includes the DisplayName of the category or product in the URL segment. i.e. {category DisplayName}={category FriendlyId} and {product DisplayName}={ProductId/FriendlyId}.
  • useShopLinks: Constructs URL with shop/{category}/{product} if enabled, otherwise as category/{category} and product/{product} for category and product URLs respectively.
  • includeCatalog: Not currently supported
  • addAspxExtension: N/A
  • alwaysIncludeServerUrl: N/A
  • encodeNames: Encodes the DisplayName portion of the category and product segments. Only supported when useShopLinks is true.
  • languageEmbedding: N/A
  • languageLocation: N/A
  • lowercaseUrls: Not currently supported
  • shortenUrls: Not currently supported
  • useDisplayName: Not currently supported

URLs Generated from Various Configurations

The following decision table shows the available configurations

 Rules
Conditions12345
useShopLinksYYYNN
includeFriendlyNameYYNYN
encodeNamesYN Y 
Actions12345
ShopXXX  
Product/Category   XX
Display Name prefixXX X 
Display Name encodingX  X 

The following table shows the URLs generated from the rules in the above table.

#PageURL
1Categoryhttps://sxa.storefront.com/shop/Kid%E2%80%99sTablets%3dhabitat_master-kid%20s%20tablets
 Producthttps://sxa.storefront.com/shop/Kid%E2%80%99sTablets%3dhabitat_master-kid%20s%20tablets/MinnowKid%E2%80%99sTablet%E2%80%947%E2%80%9D%2C8GB%3d6042221
2Categoryhttps://sxa.storefront.com/shop/Kid’sTablets%3dhabitat_master-kid%20s%20tablets
 Producthttps://sxa.storefront.com/shop/Kid’sTablets%3dhabitat_master-kid%20s%20tablets/MinnowKid’sTablet—7”%2C8GB%3d6042221
3Categoryhttps://sxa.storefront.com/shop/habitat_master-kid%20s%20tablets
 Producthttps://sxa.storefront.com/shop/habitat_master-kid%20s%20tablets/6042221
4Categoryhttps://sxa.storefront.com/category/Kid’sTablets%3dhabitat_master-kid%20s%20tablets
 Producthttps://sxa.storefront.com/product/MinnowKid’sTablet—7”%2C8GB%3d6042221
5Categoryhttps://sxa.storefront.com/category/habitat_master-kid%20s%20tablets
 Producthttps://sxa.storefront.com/product/6042221

In reviewing the results of the configurations above, rule set 5 creates the desired product URL structure we set out to accomplish.

Now we will focus on customising the website solution to generate our catalog URL structure.

Customising the Solution to Complete the SEO-Friendly URLs

Updating the CatalogUrlManager

We will need to override the BuildCategoryLink method of the CatalogUrlManager class to apply the following customisations:-

  • Generate the category hierarchy (or breadcrumb)
  • Remove the catalog name from the category’s Friendly Id.
public override string BuildCategoryLink(Item item, bool includeCatalog, bool includeFriendlyName)
{
    return BuildBreadcrumbCategoryUrl(item, includeCatalog, includeFriendlyName, CatalogFoundationConstants.Routes.CategoryUrlRoute);
}

protected virtual string BuildBreadcrumbCategoryUrl(Item item, bool includeCatalog, bool includeFriendlyName, string root)
{
    Assert.ArgumentNotNull(item, nameof(item));

    string catalogName = ExtractCatalogName(item, includeCatalog);
    var categoryBreadcrumbList = GetCategoryBreadcrumbList(item);

    return BuildBreadcrumbCategoryUrl(categoryBreadcrumbList, includeFriendlyName, catalogName, root);
}

protected virtual string BuildBreadcrumbCategoryUrl(List<Item> categories, bool includeFriendlyName, string catalogName, string root)
{
    Assert.ArgumentNotNull(categories, nameof(categories));

    var stringBuilder = new StringBuilder("/");
    if (IncludeLanguage)
    {
        stringBuilder.Append(Context.Language.Name);
        stringBuilder.Append("/");
    }

    if (!string.IsNullOrEmpty(catalogName))
    {
        stringBuilder.Append(EncodeUrlToken(catalogName, true));
        stringBuilder.Append("/");
    }
    stringBuilder.Append(root);

    var itemName = string.Empty;
    var itemFriendlyName = string.Empty;
    foreach (var category in categories)
    {
        stringBuilder.Append("/");
        ExtractCatalogItemInfo(category, includeFriendlyName, out itemName, out itemFriendlyName);
        if (!string.IsNullOrEmpty(itemFriendlyName))
        {
            stringBuilder.Append(EncodeUrlToken(itemFriendlyName, true));
            stringBuilder.Append(UrlTokenDelimiterEncoded);
        }

        itemName = RemoveCatalogFromItemName(root, itemName);
        stringBuilder.Append(EncodeUrlToken(itemName, false));
    }

    return StorefrontContext.StorefrontUri(stringBuilder.ToString()).Path;
}

protected virtual List<Item> GetCategoryBreadcrumbList(Item item)
{
    var categoryBreadcrumbList = new List<Item>();
    var startNavigationCategoryID = StorefrontContext.CurrentStorefront.GetStartNavigationCategory();

    while (item.ID != startNavigationCategoryID)
    {
        categoryBreadcrumbList.Add(item);
        item = item.Parent;
    }
    categoryBreadcrumbList.Reverse();

    return categoryBreadcrumbList;
}

protected virtual string RemoveCatalogFromItemName(string root, string itemName)
{
    if (root == CatalogFoundationConstants.Routes.CategoryUrlRoute)
    {
        var tokens = itemName.Split('-');
        if (tokens.Length > 1)
        {
            itemName = tokens[1];
        }
    }

    return itemName;
}

protected override void ExtractCatalogItemInfo(Item item, bool includeFriendlyName, out string itemName, out string itemFriendlyName)
{
    base.ExtractCatalogItemInfo(item, includeFriendlyName, out itemName, out itemFriendlyName);
    var parentItemName = item.Parent.Name.ToLowerInvariant();
    if (itemName.StartsWith(parentItemName))
    {
        itemName = itemName.Substring(parentItemName.Length);
    }
}

Category URL: https://sxa.storefront.com/category/computers%20and%20tablets/kid%20s%20tablets

The URLs generated are looking good. We do have an issue with the URLs still containing encoded spaces. To control character encoding there is a configuration in the content editor at /sitecore/Commerce/Commerce Control Panel/Storefront Settings/Global Configuration > URL Encoding > Catalog Item Encoding.

Unfortunately there is a quirk in which it doesn’t accept space entries, so we will override the EncodeUrlToken and DecodeUrlToken methods instead. As we aren’t allowed to have hyphens in the category names, we won’t have any character conflicts where encoding or decoding.

protected override string EncodeUrlToken(string urlToken, bool removeInvalidPathCharacters)
{
    if (!string.IsNullOrEmpty(urlToken))
    {
        if (removeInvalidPathCharacters)
        {
            foreach (string invalidPathCharacter in _invalidPathCharacters)
            {
                urlToken = urlToken.Replace(invalidPathCharacter, string.Empty);
            }
        }
        EncodingTokenList.ForEach(t => urlToken = urlToken.Replace(t.Delimiter, t.EncodedDelimiter));
        urlToken = urlToken.Replace(' ', '-');
        urlToken = Uri.EscapeDataString(urlToken).Replace(UrlTokenDelimiter, EncodedDelimiter);
    }

    return urlToken;
}

protected override string DecodeUrlToken(string urlToken)
{
    if (!string.IsNullOrEmpty(urlToken))
    {
        urlToken = Uri.UnescapeDataString(urlToken).Replace(EncodedDelimiter, UrlTokenDelimiter);
        urlToken = urlToken.Replace('-', ' ');
        EncodingTokenList.ForEach(t => urlToken = urlToken.Replace(t.EncodedDelimiter, t.Delimiter));
    }

    return urlToken;
}

Category URL: https://sxa.storefront.com/category/computers-and-tablets/kid-s-tablets

Now that we have our category URL structure that meets our requirements, our last step is to ensure the URLs are resolving back to their correct Sitecore items.

Updating the CatalogPageItemResolver

Now we have covered the URL generation implementation, we now need to resolve these URLs back to their correct Sitecore items.

public override void Process(PipelineArgs args)
{
	if (Context.Item == null || SiteContext.CurrentCatalogItem != null)
	{
		return;
	}

	var contextItemType = GetContextItemType();
	switch (contextItemType)
	{
		case ItemTypes.Category:
		case ItemTypes.Product:
			var isProduct = contextItemType == ItemTypes.Product;
			var catalogItemIdFromUrl = GetCatalogItemIdFromUrl(isProduct);
			if (string.IsNullOrEmpty(catalogItemIdFromUrl))
			{
				break;
			}
			var catalog = StorefrontContext.CurrentStorefront.Catalog;
			var catalogItem = ResolveCatalogItem(catalogItemIdFromUrl, catalog, isProduct);
			if (catalogItem == null && !isProduct)
			{
				catalogItemIdFromUrl = GetCatalogItemIdFromUrl(true);
				if (string.IsNullOrEmpty(catalogItemIdFromUrl))
				{
					break;
				}
				catalogItem = ResolveCatalogItem(catalogItemIdFromUrl, catalog, isProduct);
			}
			if (catalogItem == null)
			{
				WebUtil.Redirect("~/");
			}

			SiteContext.CurrentCatalogItem = catalogItem;
		break;
	}
}

private string GetCatalogItemIdFromUrl(bool isProduct)
{
    var catalogItemId = string.Empty;
    var rawUrl = HttpContext.Current.Request.RawUrl;
    var urlTokens = rawUrl.Split('/');
    if (urlTokens.Any())
    {
        var item = urlTokens.Last();
        var queryStringPosition = item.IndexOf("?", StringComparison.OrdinalIgnoreCase);
        if (queryStringPosition > 0)
        {
            item = item.Substring(0, queryStringPosition);
        }

        if (isProduct && urlTokens.Length >= 4)
        {
            var parentCategoryName = urlTokens[urlTokens.Length - 2];
            item = $"{parentCategoryName}{item}";
        }
        catalogItemId = CatalogUrlManager.ExtractItemId(item);
    }

    return catalogItemId;
}

Summary

We learnt that the construction of the URL can be managed via Sitecore Configuration, the Sitecore Content Editor and via code customisations, depending on the URL requirements.

Source Code: Ajsuth.Foundation.Catalog

Naming Conventions in Sitecore Experience Commerce – Quick Reference Guide

In this article, we will cover the common naming conventions found within Sitecore Experience Commerce to maintain a consistent approach in our custom plugins.

CRUD Operations

Starting with some simple CRUD operations, the following tables documents the naming conventions to utilise for the various Commerce classes

Entities

Create Entity

Naming Convention: Add<entity><context>

Commerce ReferenceExample
Controller ActionAddPriceBook (CommandsController)
CommandAddPriceBookCommand
ModelPriceBookAdded
PipelineAddPriceBookPipeline
Pipeline ArgumentAddPriceBookArgument
Pipeline BlockAddPriceBookBlock

Read Entity

Naming Convention: Get<entity><context>

Commerce ReferenceExample
Controller ActionGet (<Entity>Controller)
CommandN/A. Use FindEntityCommand instead
ModelFoundEntity (handled within FindEntityCommand)
PipelineIFindEntityPipeline (handled within FindEntityCommand)
Pipeline ArgumentFindEntityArgument (handled within FindEntityCommand)
Pipeline BlockSQL.FindEntityBlock (handled within FindEntityCommand)

Update Entity

Naming Convention: Update<entity><context>

Commerce ReferenceExample
Controller ActionEditPriceBook (CommandsController)
CommandEditPriceBookCommand
ModelPriceBookEdited (No current usages)
PipelineEditPriceBookPipeline
Pipeline ArgumentEditPriceBookArgument
Pipeline BlockEditPriceBookBlock

Delete Entity

Naming Convention: Delete<entity><context>

Commerce ReferenceExample
Controller ActionDeletePriceCard. (CommandsController)

 

Note: When deleting entities, it is important to consider how references to these entities need to be handled, as well as any child entity dependencies that may also need to be deleted, which would otherwise be orphaned.

CommandDeletePriceBookCommand
ModelPriceCardDeleted (No current usages)
PipelineIDeleteEntityPipeline
Pipeline ArgumentDeleteEntityArgument (handled within IDeleteEntityPipeline)
Pipeline BlockDeleteEntityBlock (handled within IDeleteEntityPipeline)

Components

Create Component

Naming Convention: Add<component><context>

Commerce ReferenceExample
Controller ActionAddCartLine
CommandAddCartLineCommand
ModelLineAdded
PipelineAddCartLinePipeline
Pipeline ArgumentCartLineArgument
Pipeline BlockAddCartLineBlock

Read Component

Naming Convention: N/A
Components only exist to extend entities and therefore will not live in isolation to be queried. If a component was to be queried it would be to retrieve it in the context of an entity and therefore the entity would be retrieved instead.

Update Component

Naming Convention: Update<component><context>

Commerce ReferenceExample
Controller ActionUpdateCartLine
CommandUpdateCartLineCommand
ModelLineUpdated
PipelineUpdateCartLinePipeline
Pipeline ArgumentCartLineArgument
Pipeline BlockUpdateCartLineBlock

Delete Component

Naming Convention: Update<component><context>

Commerce ReferenceExample
Controller ActionRemoveCartLine
CommandRemoveCartLineCommand
ModelLineRemoved (No current usages)
PipelineRemoveCartLinePipeline
Pipeline ArgumentCartLineArgument
Pipeline BlockRemoveCartLineBlock

Business Tools

Pipeline Block
Naming Convention
ExampleDescription
Get<Navigation Entity View>ViewBlockGetInventoryNavigationViewBlockCreates and constructs a navigation entity view
Get<Dashboard Entity View>ViewBlockGetInventoryDashboardViewBlockCreates and constructs a dashboard entity view
Get<Entities>ViewBlockGetInventorySetsViewBlockCreates and constructs an entity view for managed lists
Get<Entity>DetailsViewBlockGetInventoryDetailsViewBlockCreates and constructs an entity view for a specific entity
Populate<Entity View>ViewActionsBlockPopulateInventorySetsViewActionsBlockPopulates the actions of an entity view
DoAction<Action Name>BlockDoActionAddInventorySetBlockHandles the logic of an entity view action

Miscellaneous

Commerce ReferenceExample
Persist<Entity>BlockPersistCartEntity
Initialize<Entity/Entities>BlockInitializeCatalogBlock
<Entity>Argument (Pipeline Argument)CartArgument
<Entity><Entity>Argument (Pipeline Argument) CartPartyArgument

Sitecore Experience Commerce – Configuring Currencies for Storefronts

In this article, we will look at all of the configurations around currency so that we don’t run into any currency related errors when working with the Business Tools in Sitecore Experience Commerce and our storefront website.

Currency Configurations in the Sitecore Content Editor – Commerce Control Panel

  1. In the Sitecore Content Editor,
  2. Go to /sitecore/Commerce/Commerce Control Panel/Shared Settings/Currency Settings/Currency Sets/
    1. Create, edit or locate the currency set you wish to utilise
    2. Note the Item ID
  3. Go to /sitecore/Commerce/Commerce Control Panel/Storefront Settings/Storefronts/<Storefront>/Currency Configuration
  4. Set the Currency Set as desired
  5. Publish any changes made

Currency Configurations in the Commerce Engine Connect Config

  1. In the Sitecore configuration file, <Website Root>\App_Config\Include\Y.Commerce.Engine\Sitecore.Commerce.Engine.Connect.config, the property defaultShopCurrency may need to be updated.
    Note: It’s best practice to patch out this value rather than updating this configuration file directly to ensure it’s deployed to each environment and that upgrades can be performed without corrupting the website.
  2. Create or update the patch configuration file in your website’s solution with the desired currency value.
  3. Deploy the project/solution

Currency Configurations in BizFx

  1. In your BizFx solution, open \assets\config.json and review and edit the Currency value as appropriate
  2. Deploy the BizFx site
  3. Clear the Application cache in the browser if the config.json file is still being served from disk cache.

Currency Configurations in the Commerce Engine Environment Policies

  1. In the environment configurations, review and edit the following policy properties as appropriate:-
    • GlobalEnvironmentPolicy -> Default Currency
    • GlobalCurrencyPolicy -> DefaultCurrencySet (This will be the Sitecore Item ID that you noted from the Commerce Control Panel)
    • GlobalPhysicalFulfillmentPolicy ->
      • DefaultCartFulfillmentFees -> CurrencyCode
      • DefaultCartFulfillmentFee -> CurrencyCode
      • DefaultItemFulfillmentFees -> CurrencyCode
      • DefaultItemFulfillmentFee -> CurrencyCode
      • FulfillmentFees -> Fee -> CurrencyCode
  2. Deploy the Commerce Engine Solution
  3. Run Bootstrap

Currency Configurations in Postman Environment

  1. In the environment configurations set up for the project instances, review and update the Currency value as appropriate.

Business Tools UI Hints and UI Types

In this article, we will look at what UI Hints and UI Types are and show samples is each being used within the Business Tools.

Note: This article is a work in progress.

UI Hints

The UI Hints property sets the rendering type for the properties of the entity views and entity action views.

EntityActionView

RelatedList

The RelatedList UI Hint is used to redirect the user to an entity view page instead of opening up a modal for user interaction. The entity view page URL is constructed from the EntityView property in the following format <domain>/entityView/<entity view>.

e.g. https://localhost:4200/entityView/OrdersList-CompletedOrders

var actions = entityView.GetPolicy<ActionsPolicy>().Actions;
var entityActionView = new EntityActionView()
{
	Name = "CompletedOrders",
	IsEnabled = true,
	UiHint = "RelatedList",
	EntityView = "OrdersList-CompletedOrders",
	Icon = "signal_flag_checkered"
}
actions.Add(entityActionView);

EntityView

The following list of UI Hints can be utilised for entity views.

Flat (default)

The default UI Hint for entity views is Flat. The hint is used for rendering a single set of properties as plain text.

Entity View UsageSupportedApplicable UI Types
PageYesN/A (All types will render as raw string values)
ModalYes
  • Empty (string) Text
  • Empty (bool) Checkbox
  • Empty (DateTimeOffset) Date Time picker
  • AutoComplete
  • DownloadCsv
  • Dropdown
  • Multiline
  • RichText
  • Tags

Grid

The Grid UI Hint supports the rendering of child entity views in a table-row layout in the modal window, allowing additional rows to be added and removed.

Notes:

  • The Dropdown UI Type does apply the remove row control, therefore if all view properties of the row are set to Dropdown the row will not be able to be removed.
  • Only a single child entity view is supported as the UI renders a single Add Row button, which applies to the first child entity view.
Entity View UsageSupportedApplicable UI Types
PageNoN/A
ModalYes
  • Empty (string) Text
  • Dropdown

List

The List UI Hint creates a card-like layout, each list item represented by a child entity view containing properties of supported UI Types.

Entity View UsageSupportedApplicable UI Types
PageYes
  • Empty (string) Text
  • Empty (DateTimeOffset) Date Time picker
  • DownloadCsv
  • EntityLink
  • FullDateTime
  • Html
  • ItemLink
  • List
ModalNoN/A

MediaPicker

Details to come.

Entity View UsageSupportedApplicable UI Types
PageNoN/A
ModalYes???

Details to come.

Entity View UsageSupportedApplicable UI Types
Page???N/A
Modal???N/A

Table

Similar to the List UI Hint, the Table UI Hint creates a table-row layout, each list item represented by a child entity view containing properties of supported UI Types.

Entity View UsageSupportedApplicable UI Types
PageYes
  • Empty (string) Text
  • DownloadCSV
  • EntityLink
  • Html
  • ItemLink
  • List
ModalNoN/A

BraintreePayment

The BraintreePayment UI Hint is utilised to handle adding braintree payments. It injects an iframe to manage payment information via the Braintree gateway, but otherwise provides similar support as the default Flat UI Hint for view property rendering.

Entity View UsageSupportedApplicable UI Types
PageNoN/A
ModalYes
  • Empty (string) Text
  • Empty (bool) Checkbox
  • Empty (DateTimeOffset) Date Time picker
  • AutoComplete
  • DownloadCsv
  • Dropdown
  • Multiline
  • RichText
  • Tags

UI Types

The UI Types property sets the control type that will be rendered against entity views.

View Property

The following list of UI Types can be utilised for view properties.

Empty

When no UI Type is provided, the UI control rendered is based off of the data type that is set against the RawValue property of the ViewProperty. The data types that influence the controls rendered are as follows:-

String

The default data type is the String value, which will render a text field. Most data types will fallback to their raw string value when evaluated.

var viewProperty = new ViewProperty()
{
	Name = "Standard Text Title",
	RawValue = "Standard Text"
};

entityView.Add(viewProperty);
Boolean (Checkbox)

Boolean values will be rendered as checkboxes for supported UI Hints, otherwise rendering as a string value.

var viewProperty = new ViewProperty()
{
	Name = "Boolean Field Title",
	RawValue = false
};

entityView.Add(viewProperty);
<!-- /wp:html -->

<!-- wp:heading {"level":5} -->
<h5 id="datetimeoffset">DateTimeOffset (Date Time Picker)</h5>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p><strong>DateTimeOffset</strong> values will be rendered as jQuery Date Time pickers for supported&nbsp;<em>UI Hints</em>, otherwise rendering as a string value.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Note:</strong> <em>ReadOnly</em> mode is not supported for this control. <em>(Confirmed up to XC 9.1)</em></p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":327} -->
<figure class="wp-block-image"><img src="/wp-content/uploads/2018/10/uitype-datetimepicker-1-300x71.png" alt="" class="wp-image-327"/></figure>
<!-- /wp:image -->

<!-- wp:html -->

var viewProperty = new ViewProperty()
{
	Name = "Date Time Offset Field Title",
	RawValue = DateTimeOffset.Now
};

entityView.Add(viewProperty);
Decimal

Decimal values will render an input field with appropriate validation.

var viewProperty = new ViewProperty()
{
	Name = "Decimal Field Title",
	RawValue = (decimal)0.0
};
entityView.Add(viewProperty);
Integer

Integer values will render an input field with appropriate validation.

var viewProperty = new ViewProperty()
{
	Name = "Integer Field Title",
	RawValue = 0
};
entityView.Add(viewProperty);

Autocomplete

Search control that auto-completes after 4 characters. Can be combined with a policy to configure what it searches for.

See Business Tools: The Autocomplete UI Type Control for a more in depth look into this control.

var searchScopePolicy = SearchScopePolicy.GetPolicyByType(context.CommerceContext, context.CommerceContext.Environment, typeof(SellableItem));
var policy = new Policy()
{
	PolicyId = "EntityType",
	Models = new List<Model>()
	{
		new Model() { Name = "SellableItem" }
	}
};
var policyList = new List<Policy>() { searchScopePolicy, policy };

var viewProperty = new ViewProperty()
{
	Name = "Autocomplete Title",
	UiType = UiTypes.Autocomplete,
	Policies = policyList
};

DownloadCsv

Used for the coupon CSV download control.

var items = new List<string>() { "item1", "item2" };
var viewProperty = new ViewProperty()
{
	Name = "DownloadCsv Title",
	UiType = UiTypes.DownloadCsv,
	RawValue = new JArray(items.Select(item => new JObject()
	{
		{
			"Code",
			item
		}
	})).ToString()
};

entityView.Add(viewProperty);

Renders a dropdown control. The list of options must be defined via the AvailableSelectionsPolicy. This behaves the save as Options and SelectList UI Types.

Allows linking directly to an entity. It will render a HTML link and build a URL to the Entity using the entity view’s ItemId property in the following format <domain>/entityView/Master/<entity version>/<item id>.

e.g. https://localhost:4200/entityView/Master/1/Entity-Catalog-Habitat_Master

FullDateTime

Renders date and time in short format.

var viewProperty = new ViewProperty()
{
	Name = "FullDateTime Title",
	UiType = "FullDateTime",
	RawValue = DateTimeOffset.Now
};

entityView.Add(viewProperty);

Html

Renders value as html.

var viewProperty = new ViewProperty()
{
	Name = "Html Title",
	UiType = "Html",
	RawValue = "<i>Sample</i> <b>Html</b>"
};

entityView.Add(viewProperty);

Creates a URL link in the format <domain>/entityView/<entity view name>/<Entity Version>/<entity id>/<item id>.

e.g. https://localhost:4200/entityView/Variant/1/Entity-SellableItem-6042567/56042567

List

Typically used to render a combobox. If it has an AvailableOptionsPolicy it well render as a combo with those as options.

var viewProperty = new ViewProperty()
{
	Name = "List Title",
	UiType = "List",
	RawValue = new String[] { "item 1", "item 2" }
};

entityView.Add(viewProperty);

MultiLine

Multiline text editor.

var viewProperty = new ViewProperty()
{
	Name = "MultiLine Title",
	UiType = "MultiLine",
	RawValue = "First Line\nSecond Line"
};

entityView.Add(viewProperty);

Options

Renders a dropdown control. The list of options must be defined via the AvailableSelectionsPolicy. This behaves the save as Dropdown and SelectList UI Types.

var viewProperty = new ViewProperty()
{
	Name = "Options Title",
	UiType = "Options"
};

var availableSelectionsPolicy = new AvailableSelectionsPolicy();
availableSelectionsPolicy.List.Add(new Selection() { DisplayName = "Sample Option 1", Name = "Option 1" });
var selection = new Selection() { DisplayName = "Sample Option 2", Name = "Option 2", IsDefault = true };
availableSelectionsPolicy.List.Add(selection);
viewProperty.Policies = new List<Policy>() { availableSelectionsPolicy };
viewProperty.RawValue = viewProperty.GetPolicy<AvailableSelectionsPolicy>().List.Where(s => s.IsDefault).FirstOrDefault()?.Name ?? string.Empty

entityView.Add(viewProperty);

Product

Details to come.

RichText

WYSISWYG Rich text editor.

var viewProperty = new ViewProperty()
{
	Name = "RichText Title",
	UiType = "RichText"
};

entityView.Add(viewProperty);

SelectList

Renders a dropdown control. The list of options must be defined via the AvailableSelectionsPolicy. This behaves the save as Dropdown and Options UI Types.

var viewProperty = new ViewProperty()
{
	Name = "SelectList Title",
	UiType = "SelectList"
};

var availableSelectionsPolicy = new AvailableSelectionsPolicy();
availableSelectionsPolicy.List.Add(new Selection() { DisplayName = "Sample Option 1", Name = "Option 1" });
var selection = new Selection() { DisplayName = "Sample Option 2", Name = "Option 2", IsDefault = true };
availableSelectionsPolicy.List.Add(selection);
viewProperty.Policies = new List<Policy>() { availableSelectionsPolicy };
viewProperty.RawValue = viewProperty.GetPolicy<AvailableSelectionsPolicy>().List.Where(s => s.IsDefault).FirstOrDefault()?.Name ?? string.Empty

entityView.Add(viewProperty);

Sortable

Used when rendering table headers to signify that they’re sortable.

Similar to the ItemLink, the SubItemLink creates a URL link in the format <domain>/entityView/<entity view name>/<entity version>/<item id [0]>/<item id [1]>, splitting the ItemId by the pipe separator. This UI Type is used when referencing entities differing from the current entity view.

e.g. https://localhost:4200/entityView/Variant/1/Entity-SellableItem-6042567/56042567

Tags

Renders jQuery tag control.

var viewProperty = new ViewProperty()
{
	Name = "Tags Title",
	UiType = UiTypes.Tags,
	RawValue = new String[] { "item1", "item2" },
	OriginalType = UiTypes.List
};

entityView.Add(viewProperty);

Summary

We have reviewed the various UI Hints and UI Types that are available to us for customising the Business Tools and the instances where they are applicable.

Commerce Controller Types in Sitecore Commerce

In this article, we review the Commerce Controller types to identify the purpose behind each controller type and assist with solution design in order to implement Commerce Controllers correctly in your Commerce Engine Plugin projects.

Entity Controllers

Entity Controllers, e.g. CartsController, implement Get methods and should only retrieve data, returning specific entitieslists of entities, or managed lists of entities. They should not manipulate data in the commerce databases or trigger behaviour within the Commerce Engine.

The following code snippet shows the Get Carts endpoint in the CartsController from the Carts Plugin.

public async Task<IEnumerable<Cart>> Get()
{
	CommerceList<Cart> commerceList = await Command<FindEntitiesInListCommand>()?.Process<Cart>(CurrentContext, CommerceEntity.ListName<Cart>(), 0, int.MaxValue);

	return (commerceList?.Items.ToList()) ?? new List<Cart>();
}

Api and CommerceOps Controllers

Api and CommerceOps Controllers implement methods that return non-OData entities only. The only difference between the two controllers is the Api and CommerceOps controller methods are routed via the ‘/api’ and /commerceops’ URL segments respectively.

The following code snippet shows the GetBulkPrices endpoint in the ApiController from the Catalog plugin.

public async Task<IActionResult> GetBulkPrices([FromBody] ODataActionParameters value)
{
	if (!ModelState.IsValid)
		return new BadRequestObjectResult(ModelState);
	if (!value.ContainsKey("itemIds") || !(value["itemIds"] is JArray))
		return new BadRequestObjectResult(value);

	var jarray = (JArray)value["itemIds"];
	IEnumerable<SellableItemPricing> bulkPrices = await Command<GetBulkPricesCommand>().Process(CurrentContext, jarray?.ToObject<IEnumerable<string>>());

	return new ObjectResult(bulkPrices);
}

Commands Controllers

Commands Controllers implement OData actions for manipulating data through Commands. They should only return Command OData entities, not commerce entities like carts. 

The command object contains information about the execution of the command, i.e. ResponseCodeStatusisCancelledisCompleted, etc.

Some APIs do not wait for the command to complete and will return the command with the Status property as WaitingForActivation. These long running commands can be followed up on using the CheckCommandStatus() API with taskId parameter. See the Check Long Running Command Status API in the Sitecore DevOps postman collection.

The following code snippet shows the AddFederatedPayment endpoint in the CommandsController from the Payments plugin.

public async Task<IActionResult> AddFederatedPayment([FromBody] ODataActionParameters value)
{
	if (!ModelState.IsValid || value == null)
		return new BadRequestObjectResult(ModelState);
	if (!value.ContainsKey("cartId") || (string.IsNullOrEmpty(value["cartId"]?.ToString()) || !value.ContainsKey("payment")) || string.IsNullOrEmpty(value["payment"]?.ToString()))
		return new BadRequestObjectResult(value);
	
	var cartId = value["cartId"].ToString();
	var paymentComponent = JsonConvert.DeserializeObject<FederatedPaymentComponent>;(value["payment"].ToString());
	var command = Command<AddPaymentsCommand>();
	
	await command.Process(CurrentContext, cartId, new List<PaymentComponent>() { paymentComponent });
	
	return new ObjectResult(command);
}

Summary

While there’s nothing preventing developers from incorrectly using the Commerce Controllers, these coding standards will contribute to the long term maintainability of your Sitecore Commerce solutions.

How to Create a Commerce Engine Plugin – Part 1 – Creating a Dashboard Item in the Business Tools

In this series of articles, we will go through the process of creating a new Sitecore Commerce Engine plugin from scratch that will eventuate into a complete end-to-end solution. We will touch most of the areas of Sitecore Experience Commerce, demonstrating the flexibility and extensibility of the platform. Our plugin will be the Sitecore Commerce Stores Plugin.

Note: This is intended to be a walk-through guide rather than production-ready, deployable code.

Each article in this series will try to isolate implementation aspects to avoid information overload. We will cover:-

Creating a New Dashboard Item in the Business Tools

Creating the Navigation View

The Business Tools navigation is created from navigation blocks registered to the IBizFxNavigationPipeline. The naming convention used for navigation blocks is Get<Concept>NavigationViewBlock.

So first up, we create our GetStoresNavigationViewBlock. The structure of the Run method for these blocks is as follows:-

  1. Ensure mandatory parameters have been provided
  2. Create the navigation entity view to navigate to the concept dashboard
  3. Add the navigation view as a child view of the Business Tools Navigation view
  4. Return the updated Business Tools Navigation view
public override Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
	// 1. Ensure mandatory parameters have been provided
	Condition.Requires(entityView).IsNotNull($"{Name}: The argument cannot be null.");
	
	// 2. Create the navigation entity view to navigate to the concept dashboard
	var dashboardName = context.GetPolicy<KnownStoresViewsPolicy>().StoresDashboard;
	var storesDashboardView = new EntityView()
	{
		Name = dashboardName,
		ItemId = dashboardName,
		Icon = Views.Constants.Icons.MarketStand,
		DisplayRank = 6
	};

	// 3. Add the navigation view as a child view of the Business Tools Navigation view
	entityView.ChildViews.Add(storesDashboardView);

	// 4. Return the updated Business Tools Navigation view
	return Task.FromResult(entityView);
}

Referencing the code snippet above, the EntityView‘s Icon property can be set to any of the values for the icon font utilised. See my previous post, EntityView Icons in Sitecore Commerce Business Tools, for reviewing available icons.

The DisplayRank is also a self-explanatory property setting its position amongst the other ChildViews of the Business Tools navigation view.

Next, we register the navigation block in ConfigureSitecore.cs, in the BizFx Navigation Pipeline.

.ConfigurePipeline<IBizFxNavigationPipeline>(pipeline => pipeline
	.Add<Pipelines.Blocks.GetStoresNavigationViewBlock>().After<GetNavigationViewBlock>()
)

Commerce Dashboard

Navigation with Stores

Stores Dashboard

Stores Dashboard

Adding Commerce Terms

Now the Stores navigation item will actually read “StoresDashboard” at this point. We will need to add a commerce term to override the text.

In the Sitecore Content Editor, navigate to /sitecore/Commerce/Commerce Control Panel/Commerce Engine Settings/Commerce Terms/BusinessTools/ViewNames and create a new item StoresDashboard.

Commerce Terms - Stores Dashboard

In order to propagate this new data through to the Business Tools we need to run the Ensure/Sync default content paths request from Postman. This will sync the Sitecore content data into the Commerce Engine’s Shared Environments ContentEntities table.

Summary

So we added a new empty dashboard into the Business Tools. There’s nothing really to show but an icon and an empty dashboard view. Not to worry. Continue on to Part 2 – Creating EntityViews, Actions, and Entities (coming soon)

Source Code: Sitecore Commerce Stores Plugin release tag for this article

EntityView Icons in Sitecore Commerce Business Tools

There are 1311 icons available in the custom font used by the Business Tools.

Rather than listing out all of the icons here, I have created a simple plugin that creates a new dashboard item per item for reference purposes only.

When you are creating your entity view, you can simply assign the Icon property to any of the icons provided. For example:-

var myEntityView = new EntityView()
{
    Name = "MyEntityView",
    ItemId = "MyEntityView",
    Icon = "antenna",
    DisplayRank = 6
};

Source Code: The Business Tools plugin project repository

Valid Certificate Thumbprint not Matching in Sitecore Experience Commerce 9

The certificate thumbprint is configured in the website’s configuration file, located at <Website Root>/App_Config/Y.Commerce.Engine/Sitecore.Commerce.Engine.Connect.config (or in a custom patch file created for your solution) and in the Commerce Engine environments under <Commerce Engine Root>/wwwroot/config.json.

If you have configured a valid thumbprint that contains lowercase letters, for example 2700da6ab17c56a01f6d0762b76b3ca77933a68a, this will trigger the following errors in the Commerce Engine logs.

[20:13:57 ERR] ClientCertificateValidationMiddleware: Certificate with thumbprint 2700DA6AB17C56A01F6D0762B76B3CA77933A68A does not have a matching Thumbprint.
[20:13:57 INF] ClientCertificateValidationMiddleware: Certificate with thumbprint 2700DA6AB17C56A01F6D0762B76B3CA77933A68A is not valid.

This is caused by the logic used to compare the thumbprint values. The thumbprint in the Sitecore configuration file is transformed to uppercase while the thumbprint from the Commerce Engine configuration is not, so when the case-sensitive comparison is performed the result is a mismatch.

As the thumbprint is not case-sensitive, you can safely update the thumbprint values to be uppercase to resolve these errors.

Managing Commerce Configuration to Align with Helix Principles

In this article, we will review a sample configuration of the layers.config file to better manage your solution’s configuration files when working with Sitecore Experience Commerce 9 (XC9).

If you take a look at an XC9 website’s /App_Config/Include folder, you would have noticed that there’s still some chaos happening with folder and file prefixes of ‘Y’ and ‘Z’ to assign priority order to the patch configuration files. On top of this, you may have even previous worked on projects where there have been crazy levels of prefixing ‘z’, ‘zz’, ‘zzzzz’, etc. to folders and files (I know I have seen some crazy levels of this), leading to developers pulling their hair out trying to locate the file’s origin. As chaotic as this is, we will look at how we can bring back order for our solution.

Configuring layers.config

Our intention is to ensure that our solution’s patch config files are always in a position to override the platform’s patch files, while adhering to the Helix layers principle.

With the following approach, the only additional consideration introduced is to review the platform’s custom config folders and files during any future upgrades to update the layers.config file to ensure the intended load order remains in tact.

Note: The following way is just one of many that can be implemented to achieve the same outcome. There is no one right way, so if you manage your solution’s configuration files differently there is no need to align to this.

  1. Backup the layers.config. Always important when you plan on modifying any files that are not part of your project solution (as infrequent as this should be).
  2. Copy the layers.config file out of the App_Config folder into a core project in the same location. This is because the layers.config file itself cannot be patched and we would want this file version controlled so that the file can be deployed to all environments, and modified (if required) by our colleagues.
  3. The layers.config file is then updated to specify the load order for all current folders. While this step is a bit of a pain, as any omissions will mean that any folder/files unspecified will be applied after the loadOrder , it allows us to ensure that all of our solution configuration files will be applied last, having the highest level of priority.
    <layer name="Custom" includeFolder="/App_Config/Include/">
      <loadOrder>
        <add path="Cognifide.PowerShell.config" type="File" />
        <add path="Sitecore.Commerce.Carts.config" type="File" />
        <add path="Sitecore.Commerce.Catalogs.config" type="File" />
        <add path="Sitecore.Commerce.config" type="File" />
        <add path="Sitecore.Commerce.Customers.config" type="File" />
        <add path="Sitecore.Commerce.GiftCards.config" type="File" />
        <add path="Sitecore.Commerce.Globalization.config" type="File" />
        <add path="Sitecore.Commerce.Inventory.config" type="File" />
        <add path="Sitecore.Commerce.LoyaltyPrograms.config" type="File" />
        <add path="Sitecore.Commerce.Orders.config" type="File" />
        <add path="Sitecore.Commerce.Payments.config" type="File" />
        <add path="Sitecore.Commerce.Prices.config" type="File" />
        <add path="Sitecore.Commerce.Shipping.config" type="File" />
        <add path="Sitecore.Commerce.WishLists.config" type="File" />
        <add path="z.Cognifide.PowerShell.config" type="File" />
        <add path="ContentTesting" type="Folder" />
        <add path="Examples" type="Folder" />
        <add path="Feature" type="Folder" />
        <add path="Foundation" type="Folder" />
        <add path="Foundation.Overrides" type="Folder" />
        <add path="Project" type="Folder" />
        <add path="Y.Commerce.Engine" type="Folder" />
        <add path="Z.Commerce.Engine" type="Folder" />
        <add path="z.Feature.Overrides" type="Folder" />
        <add path="Z.Foundation" type="Folder" />
        <add path="Z.Foundation.Overrides" type="Folder" />
        <add path="Z.LayoutService" type="Folder" />
      </loadOrder>
    </layer>
  4. We then create three folders, following the Helix principles for our project solution. I just chose to use ‘Helix’ as a prefix to drill the point in.
    1. Helix.Feature
    2. Helix.Foundation
    3. Helix.Project
  5. Add the new folders to the <loadOrder>.
    <layer name="Custom" includeFolder="/App_Config/Include/">
      <loadOrder>
        ...
        <add path="Z.LayoutService" type="Folder" />
        <add path="Helix.Foundation" type="Folder" />
        <add path="Helix.Feature" type="Folder" />
        <add path="Helix.Project" type="Folder" />
      </loadOrder>
    </layer>
  6. As you are developing your solution you may find that you need to set the load order for the individual configuration files within a certain layer. In this case, you can list out the file order required. In the sample below, we have Feature A, Feature B, and Feature C. Feature A has a configuration conflict with Feature B and needs higher priority. Feature C has no conflicts. We only need to specify the config for Feature A after the folder to ensure the correct patch order.
  7. <layer name="Custom" includeFolder="/App_Config/Include/">
      <loadOrder>
        ...
        <add path="Helix.Feature" type="Folder" />
        <add path="Helix.Feature/Feature.A.config" type="File" />
        <add path="Helix.Project" type="Folder" />
      </loadOrder>
    </layer>
  8. Deploy your solution.

Summary

While you could continue down the path of having project with ‘z’, ‘zz’, ‘zzzzz’, etc. prefixes, by taking the initial steps to isolate and manage your solution’s patch files under managed folder and file load order, we bring the chaos back to order with meaningfully named patch files that can be found exactly where you expect them to be located.