Sitecore Experience Commerce: Working with Digital Sellable Items – Part 1

In this article, we will review the difference between physical and digital sellable items, while focusing on the lesser documented digital sellable items. We will also review how to configure digital sellable items in the business manager.

Sellable Item Classifications

Sellable items can either represent physical or digital items. The following high-level overviews detail some of the discerning factors between the two classifications of sellable items.

Physical Sellable Items

Physical sellable items are any tangible product that a customer can purchase and have delivered. This is the default product type when creating sellable items via the Merchandising Manager in Sitecore Commerce.

Inventory

With physical items, inventory is required to track stock levels and prevent overselling.

Fulfillment Option Types

When purchasing physical items, during the checkout delivery step, a customer typically enters a delivery address of where the items will be delivered to, or selects a 'click and collect' style fulfillment option type where they can pick up the items from.

The SXA Storefront provides the common 'deliver to address' (physical) fulfillment option type.

Digital Sellable Items

Digital sellable items represent non-tangible products that a customer can purchase that entitle the customer to a form of product or service. Examples of digital sellable items include services, such as installation, warranties, subscriptions, digital downloads, online access, and digital currency (gift cards).

Inventory

As digital sellable items are non-tangible, they do not require inventory information to be associated to them. This can also be thought of as having perpetual inventory, therefore always being available for purchase.

Fulfillment Option Types

For digital sellable item purchases, the SXA Storefront provides a digital delivery sample implementation intended for digital gift card purchases, consisting of a recipient email and custom message, so the digital gift card can be personalised and delivered via email to the intended recipient.

Alternately, for other digital item fulfillment option types, custom implementations would be required to handle these scenarios, based on the client's requirements. Some ideas of requirements are as follows:

  • Services: A custom form, potentially with a third party integration, to create a booking system.
  • Warranties: A custom form to register the sellable item that the warranty was purchased for.
  • Subscriptions: A custom form to register the subscription's recipient details.
  • Digital Downloads: A custom form containing requesting the email address to send the digital download link to, or perhaps the user account functionality is customised to provide access to digital downloads.
  • Online access: Similar to digital downloads, the user may be given access to content via their logged in account, which may not require additional details to be taken during the Delivery step, however may require the user to make the purchase via a registered account.

Cart Lines

Digital sellable items are also separated into their own line items, e.g. when adding a digital sellable item with quantity of 2 this will create 2 cart lines with a quantity of 1, rather than 1 cart line with a quantity of 2. This provides the customer with the ability to input unique delivery information for each item during the checkout delivery step.

Entitlements

Another differentiating factor of digital sellable items are entitlements, which are registered to the order and customer account (where users are registered at the time of purchase), so that customer service representatives can view the current state of each entitlement.

Entitlements provides a basic implementation and a great starting point for customisation to meet business requirements.

Order Summary with entitlements
Customer Summary with entitements

Configuring Digital Sellable Items

Digital sellable items are determined by applying the appropriate Digital Sellable Item Tags on the sellable item and its variants. Let's review this together.

In the Business Tools,

  1. Navigate to the Merchandising Manager
  2. Locate an existing new sellable item or create a new sellable item and navigate to the Sellable Item page view
  3. Follow either Configuring Standalone Sellable Items or Configuring Sellable Item With Variants
  4. Ensure the sellable item has a valid list price and/or price card with an active price snapshot, so that the sellable item is purchasable
  5. Publish the sellable item

Configuring Standalone Sellable Items

  1. Add the appropriate tag that represents a Digital Item.
  2. Add the appropriate tag that represents the desired type of Digital Item Type.
    This will likely be the same tag as the Digital Item, therefore not required, however it is good to validate that the tags match, especially if custom tags have been assigned to the digital item and digital item type tag policies.
  3. Add the tag "entitlement".
Sellable item tags

Configuring Sellable Items With Variants

  1. On the sellable item, add the appropriate tag that represents a Digital Item.
  2. On each variant:
    1. Add the appropriate tag that represents the desired type of Digital Item Type.
    2. Add the tag "entitlement".

Note: Technically, the variants inherit tags from the parent sellable item only if no tags have been specified on the variant. Configuring any tags on a variant will remove the inherited tags from the variant, therefore these instructions specify the fool-proof solution.

Sellable item tags
Variant tags

Digital Sellable Item Tags

The following tables contain the default tags that are used to classify sellable items as digital and their digital item types.

Note: Tags for digital sellable items are not treated as case-sensitive.

Digital Item Tags

ClassificationDefault Tags
Digital Itementitlement
service
installation
subscription
digitalsubscription
warranty
onlinetraining
onlinelearning
giftcards

Digital Item Type Tags

TypeDefault Tags
Virtual Gift Cardgiftcards
Digital ProductOnlineTraining
OnlineLearning
Subscription
DigitalSubscription
Warranty Warranty
InstallationInstallation
Service

Sitecore Experience Commerce: Accessing the GetRawEntity API

In this article, we will take a look at why the GetRawEntity api returns a 404 Not Found response for the default admin user in Sitecore Commerce.

Note: The GetRawEntity API is intended for troubleshooting and validation purposes and would be utilised by devops and developer users.

Reviewing the commerce logs we find that the QA role in not a role in the current request.

00064 22:41:06 ERROR CtxMsg.Error.QARoleNotFound: Text=QA is not a role in the current request.
00064 22:41:06 ERROR PipelineAbort:QA is not a role in the current request.

We can resolve this by updating the role memberships assigned the admin user, or any desired user.

  1. In Sitecore, go to the User Manager
  2. Select and edit the desired user
  3. In the Edit User modal,
    1. Select the MEMBER OF tab and edit the roles.
    2. Locate the sitecore\QA role and add it to the selected roles.

Note: If the user has already has received a token from Identity Server, a new token will need to be issued to receive the new role.

Sitecore Experience Commerce: Enabling Disassociate, Edit, and Transfer Inventory Actions for Published Sellable Items and Variants

In this article, we will look at how we can enable the Disassociate Sellable Item from Inventory Set, Edit Sellable Item Inventory and Transfer Inventory actions when viewing sellable item and variant entity views in BizFx.

For entities that have been configured to utilise entity versioning (catalogs, categories, and sellable items), via the VersioningPolicy in the VersioningPolicySet, all actions are disabled by default when the entity has been published.

The actions that are enabled are due to those actions being registered in the EntityVersionsActionsPolicy in the VersioningPolicySet, under the AllowedActions property.

There is no need to restrict the Inventory Sets actions in the Sellable Item and Variant entity views as they can be executed from within the Inventory Manager.

Note: Inventory association and disassociation actions apply to all entity versions of the sellable items as inventory records don't have strong ties to specific entity versions as inventory is not content.

To enable the Inventory Sets actions, simply add their action names to the AllowedActions, and deploy and bootstrap.

{
    "$type": "Sitecore.Commerce.Plugin.EntityVersions.EntityVersionsActionsPolicy, Sitecore.Commerce.Plugin.EntityVersions",
    "AllowedActions": {
    "$type": "System.Collections.Generic.List`1[[System.String, mscorlib]], mscorlib",
    "$values": [
            "AddEntityVersion",
            "AddCatalog",
            "DeleteCatalog",
            "AddCategory",
            "DeleteCategory",
            "AddSellableItem",
            "DeleteSellableItem",
            "AddBundle",
            "AssociateCategoryToCategoryOrCatalog",
            "AssociateSellableItemToCatalog",
            "AssociateSellableItemToCategory",
            "DisassociateItem",
            "MakePurchasable",
            "DisassociateSellableItemFromInventorySet",
            "EditSellableItemInventory",
            "TransferInventory"
        ]
    }
}

These actions can now be performed, regardless of whether or not the entity has been published.

Sitecore Experience Commerce: Improved Serilog Configurations

In this article, we will look at a few ways to configure the Serilog Logging to improve the ability to troubleshoot via logs.

Introduction

The LoggerConfiguration is defined in Startup.cs in the Commerce Engine project and will be created if the Logging.SerilogLoggingEnabled setting in the config.json file is true. The configuration is as follows:

Log.Logger = new LoggerConfiguration()
				.ReadFrom.Configuration(this.Configuration)
				.Enrich.FromLogContext()
				.Enrich.With(new ScLogEnricher())
				.WriteTo.Async(
					a => a.File(
						$@"{Path.Combine(this._hostEnv.WebRootPath, "logs")}\SCF.{DateTimeOffset.UtcNow:yyyyMMdd}.log.{this._nodeInstanceId}.txt",
						this.GetSerilogLogLevel(),
						"{ThreadId:D5} {Timestamp:HH:mm:ss} {ScLevel} {Message}{NewLine}{Exception}",
						fileSizeLimitBytes: fileSize,
						rollOnFileSizeLimit: true),
					bufferSize: 500)
				.CreateLogger();

With a little investigation into open source Serilog.Sinks.File repo, and an understanding of the Commerce Engine configuration, we can look to improve the logging configuration.

Log File Enhancements

Updating the Log File Name with Local Timezone

When the Commerce Engine initialises, it creates a log file with the the naming convention of SCF.<date>.log.<node instance id>.txt, e.g. SCF.20190902.log.03d49b837f214f55b815ee7adbba5ec.txt.

By default, Serilog uses the DateTime.Now in the outputTemplate, however the Commerce Engine configures the path property with a UTC timestamp. This means that if the date applied to the filename is not a true indication of the system date.

To resolve this, we can simply update the path property to use DateTimeOffset.Now.

Creating a New Log File Each Day

Creating a new log file each day ensures that all log entries for any given log file will pertain to a single day. No more do we have to review the rolling logs spanning multiple days, with a fine-tooth comb, identifying how many cycles of 24 hour time have passed to determine the date of log entries.

We can achieve this by setting the rollingInterval argument to RollingInterval.Day. We will also need to configure an additional period to the path parameter where the extension is specified {this._nodeInstanceId}..txt.

a => a.File(
	 $@"{Path.Combine(this._hostEnv.WebRootPath, "logs")}\SCF.{DateTimeOffset.UtcNow:yyyyMMdd}.log.{this._nodeInstanceId}..txt",
	 this.GetSerilogLogLevel(),
	 "{ThreadId:D5} {Timestamp:HH:mm:ss} {ScLevel} {Message}{NewLine}{Exception}",
	 fileSizeLimitBytes: fileSize,
	 rollOnFileSizeLimit: true,
	 rollingInterval: RollingInterval.Day)

By changing the rollingInterval, Serilog will append a date/time stamp to the file name, prior to the rolling log index, based on the specificity of the interval. For example, setting the rollingInterval to Day will append the date format yyyyMMdd, whereas setting it to the minute will append the date/time format of yyyyMMddHHmm.

This date/time format appended by Serilog when using rolling intervals is hard-coded and cannot be configured.

The additional period will separate the node instance id from the date, and with the change to the rollingInterval parameter, the date will be listed twice in the filename, e.g. SCF.20190902.log.03d49b837f214f55b815ee7adbba5ec1.20190902.txt.

The difference between the two occurences is the first instance is a constant value while the second is dynamic. When specifying the date format in the filename from the previous step this is a constant value that is set during the initialisation of the Commerce Engine, whereas the second date format instance is part of the Serilog library and during the creation of the daily logs will update this value, e.g.

  • SCF.20190902.log.03d49b837f214f55b815ee7adbba5ec1.20190902.txt
  • SCF.20190902.log.03d49b837f214f55b815ee7adbba5ec1.20190903.txt
  • SCF.20190902.log.03d49b837f214f55b815ee7adbba5ec1.20190904.txt

Removing the date from the beginning of the filename will still allow the files to be sorted by filename in descending order while the Commerce Engine has only been initiliased once, however with deployments and manual resets a new guid will be created to represent the node instance id.

Instead, sorting by date modified will provide an accurate timeline, however this won't separate the NodeConfiguration files from the log files as per sorting by filename.

Adding the Date to the outputTemplate Timestamp

If you are a fan of rolling logs spanning multiple days, then having the date specified in the timestamp will be beneficial.

To achieve this, simply update the outputTemplate parameter's Timestamp to include the date in the desired format. e.g. {Timestamp:dd/MM/yy HH:mm:ss}.

00027 30/08/19 11:28:11 INFO Executing action method "Sitecore.Commerce.Plugin.Carts.CartsController.Get (Sitecore.Commerce.Plugin.Carts)" with arguments (["Default4c29a122-a190-44f3-ab30-baa79629155dStorefront"]) - ModelState is Valid

Summary

We looked at some of the most useful configuration enhancments to improve our ability to troubleshoot the Commerce Engine via log files. While there are potentially other configurations to further improve logging, Creating a New Log File Each Day appears to have the most benefits.

Sitecore Experience Commerce: Methods for Logging and Command Messaging

In this article, we will look at the APIs available for logging to the logging framework and applying command messages to the CommerceContext.

The reason for grouping these two subjects together is due to seeing a lot of confusion around these areas when reviewing developers' code in the field; there is some overlap between them, which is often overlooked.

In Sitecore Commerce, logging is based on Microsoft.Extensions.Logging, and the Sitecore Commerce Engine SDK is setup to utilise the SeriLog diagnostic library for logging.

The CommandMessages are flushed to calling CommerceCommands via the completion of the CommandActivity and are included in the response object of CommandsController APIs.

Logging and command messaging occurs within methods of the CommercePipelineExecutionContext and the CommerceContext.

CommercePipelineExecutionContext

LogInfoIf

public void LogInfoIf(bool conditionResult, string info);

Intuitive enough, the LogInfoIf method will log an Information level entry, info, if the conditionResult is met.

Abort

public override void Abort(string reason, object data);

The Abort method will abort the pipeline and will create an Error level log entry if the reason message doesn't contain the magic string "Ok|".

It's also worth noting that this method is intended to abort the executing pipeline first and foremost, and the log entry is secondary. It is not intended solely for the purpose of logging.

Note: The data object would normally return the current CommercePipelineExecutionContext.

CommerceContext

AddDataMessage

public virtual void AddDataMessage(string messageType, string dataMessage);

The AddDataMessage will add the dataMessage to the command messages. It will also add the dataMessage to the logger at the Information level.

Tip: Avoid setting Debug level messages to avoid spamming your local development logs, which are defaulted to the Information level.

AddMessage

public virtual void AddMessage(CommandMessage message);

AddMessage will add a CommandMessage to the command messages.

The message will not invoke the logger.

AddMessage (Alternate)

public virtual Task<string> AddMessage(string code, string commerceTermKey, object[] args, string defaultMessage = null);

The overloaded AddMessage method's logging behaviour is as follows:

  • Message codes of ValidationError or Warning will add a Warning message to the Logger.
  • All exception types will be logged using the LogException method. See LogException for more details.
  • An Error will also be logged as an Error.

For the CommandMessages, the localised message will attempted to be retrieved from Sitecore, using the commerce term key provided, and further formatted/interpolated with the args provided.

LogException

public virtual void LogException(string caller, Exception ex);

The LogException method will log an exception as an error with the exception message and stack trace details.

LogExceptionAndMessage

public virtual void LogExceptionAndMessage(string caller, Exception ex);

The LogExceptionAndMessage logs the exception as per the LogException method, however the exception message will be added to the CommandMessages at the Error level in addition.

Logger

public ILogger Logger { get; }

The Logger exposes the can be utilised to add the standard log-level entries to it, being:

  • LogDebug
  • LogInformation
  • LogWarning
  • LogError
  • LogCritical

Sitecore Experience Commerce: Conditionally Executing Pipeline Blocks

In this article, we will look at the various approaches to implementing pipeline blocks to conditionally execute.

As the pipeline framework executes pipeline blocks linearly, there may be instances where pipeline blocks registered to the pipeline should not be executed. For example, in the following pipeline both the Braintree and Gift Cards payments are registered in the IProcessPaymentsPipeline to capture payments, however the customer may have only paid with a single payment method.

IProcessPaymentsPipeline
  Sample.Payments.Braintree.CapturePaymentsBlock 
  Sample.Payments.Finance.CapturePaymentsBlock  
  Sample.Payments.GiftCards.CapturePaymentsBlock 

To prevent unnecessary execution of pipeline blocks, the following approaches are discussed generically and not specific to the sample pipeline above.

The Pipeline Block

The PipelineBlock is the common abstract class that the majority of pipeline blocks are based off of. The Run method is where the business logic is placed and will be executed for the pipeline block. There is no mechanism to conditionally execute the business logic within its Run method, however we can simply achieve this by returning the pipeline early if a certain condition is not met.

The following example, simply checks to see if the cart has a FederatedPaymentComponent, otherwise it will return the pipeline argument to continue execution of the pipeline.

public class SamplePipelineBlock : PipelineBlock<SamplePipelineArgument, SamplePipelineArgument, CommercePipelineExecutionContext>
{
	protected CommerceCommander Commander { get; set; }

	public SamplePipelineBlock(CommerceCommander commander)
	: base(null)
	{
		this.Commander = commander;
	}
		
	public override async Task<SamplePipelineArgument> Run(SamplePipelineArgument arg, CommercePipelineExecutionContext context)
	{
		Condition.Requires(arg).IsNotNull($"{this.Name}: The argument can not be null");

		var cart = arg.Cart;
		if (!cart.HasComponent<FederatedPaymentComponent>())
		{
			return await Task.FromResult(arg).ConfigureAwait(false);
		}

		/* Add business logic here */

		return await Task.FromResult(arg).ConfigureAwait(false);
	}
}

The ConditionalPipelineBlock

The ConditionalPipelineBlock is an abstract class that allows a custom pipeline block to be implemented with the BlockCondition Predicate, which will determine whether to execute the Run method, otherwise executing the ContinueTask method.

In Sitecore Commerce Project projects, this pipeline block implements concrete pipeline block by utilising the Run method as the standard housing for the business logic; the BlockCondition is implemented to determine whether a policy is available within the current environment or global environment, dependent on the context, and the ContinueTask to return the pipeline argument to continue the pipeline.

Note: The BlockCondition does not be determined by an environment policy, however consider whether another pipeline block approach may be more beneficial if implementing conditional logic alternate to an environment policy.

Note: It is strongly recommended that the ContinueTask simply returns the pipeline argument, but it is not mandatory. Consider alternative approaches, such as additional confitional pipeline blocks, prior to adding business logic in this method.

public class SampleConditionalPipelineBlock: ConditionalPipelineBlock<SamplePipelineArgument, SamplePipelineArgument, CommercePipelineExecutionContext>
{
	public SampleConditionalPipelineBlock()
	{
		this.BlockCondition = ValidatePolicy;
	}

	private static bool ValidatePolicy(IPipelineExecutionContext context)
	{
		return ((CommercePipelineExecutionContext)context).CommerceContext.HasPolicy<SampleEnvironmentPolicy>();
	}

	public override Task<SamplePipelineArgument> Run(SamplePipelineArgument arg, CommercePipelineExecutionContext context)
	{
		Condition.Requires(arg).IsNotNull($"{this.Name}: argument can not be null.");

		/* business logic here */

		return Task.FromResult(arg);
	}

	public override Task<SamplePipelineArgument> ContinueTask(SamplePipelineArgument arg, CommercePipelineExecutionContext context)
	{
		return Task.FromResult(arg);
	}
}

The PolicyTriggerConditionalPipelineBlock

The PolicyTriggerConditionalPipelineBlock is another abstract class, piggybacking off of the ConditionalPipelineBlock, introducing an abstract string ShouldNotRunPolicyTrigger, implementing the ContinueTask to return the pipeline argument my default, and implementing the BlockCondition to determine if a the pipeline block should run based on the ShouldNotRunPolicyTrigger value being present in the PolicyKeys request header property.

Tip: This approach may come in handy in custom integration import/export APIs.

public class SamplePolicyTriggerConditionalPipelineBlock : PolicyTriggerConditionalPipelineBlock<SamplePipelineArgument, SamplePipelineArgument>
{
	public override string ShouldNotRunPolicyTrigger
	{
		get
		{
			return "IgnoreSample";
		}
	}

	public override Task<SamplePipelineArgument> Run(SamplePipelineArgument arg, SamplePipelineArgument context)
	{
		Condition.Requires(arg).IsNotNull(this.Name + ": argument cannot be null.");

		/* business logic here */

		return Task.FromResult(arg);
	}
}

Business Tools: The Autocomplete UI Type Control

In this article, we will review the Autocomplete UI Type control in detail to understand what capabilities we have available to us with and without further customisation.

What Does the Autocomplete UI Type Control Do?

The Autocomplete control provides the user with a list of potentially search matches to identify the entity with only a partial match. This occurs only when 4 or more characters have been entered into the field.

Upon selection, the entity is converted from its user-friendly display name to the raw entity id value required by the system.

The Implementation Behind the Autocomplete Control?

Commerce Engine Configuration

The autocomplete control is configured by setting a ViewProperty's UiType to "Autocomplete". In addition to this there are two policies that need to be added to the ViewProperty, which are required to complete its configuration.

The first policy is the SearchScopePolicy, which is utilised to retrieve the index name from, which is set in the Plugin.Search.PolicySet-1.0.0.json in the Commerce Engine. Using the GetPolicyByType method, pass in the typeof entity that the is configured to the policy's EntityTypeNames property.

Note: Only the Catalog Items Scope is supported by default.

var searchScopePolicy = SearchScopePolicy.GetPolicyByType(context.CommerceContext, context.CommerceContext.Environment, typeof(SellableItem));

The second policy is a generic policy that will be utilised by BizFx to apply some post-search filtering to the search results. This policy must have the PolicyId of "EntityType" and will contain a list of up to two models.

The first model's name must be set as the name of the entity without the "Entity-" prefix ("SellableItem", "Category", or "Catalog"). This is known as the policy scope in BizFx.

The second model's name is only applicable for sellable items and can only be set as "SearchVariants" if you want the variants to be included in the search results for selection. All other values will be ignored and you cannot set multiple entities to be included in the autocomplete list.

var policy = new Policy(new List<Model>()
{
	new Model() { Name = "SellableItem" },
	new Model() { Name = "SearchVariants" }
})
{
	PolicyId = "EntityType"
};

BizFx Implementation

The sc-bizfx-autocomplete.component.ts file that is shipped with the BizFx SDK is where some of the magic happens. A couple of magic strings and magic indexes are the keys to processing the translating the ViewProperty configuration into search parameters and post-search filtering.

In short, when 4 or more characters are available in the text field, the text is added as the search term parameter, and the search index name, which is extracted from the SearchScopePolicy, is added as the scope parameter, passed into the Commerce Engine's Search API, querying the top 100 results. The results are then processed by the BizFx component by filtering out entities that don't match the entity type, specified in the policy scope model from the EntityType policy. The results are then added to the list of results that will populate the autocomplete dropdown, using the displayname as the display name and the entityid as the value.

Where SearchVariants have been configured for sellable item searches, the BizFx component iterates over the pipe separated variantdisplayname and variantid fields to create variant entries in the autocomplete list.

Note: As BizFx uses the displayname field to render the autocomplete item list, the order and customer indexes, which do not contain a displayname field cannot be configured with autocomplete functionality without customisation. Alternatively, the Search entity views in the Customers Manager and Orders Manager are available.

Search Configuration

In the Solr core's managed-schema, copies fields over to the _text_ field, which is used to construct the search query in the Commerce Engine.

The catalog item scope index contains the Catalog, Category, and Sellable Item data, based on the SearchScopePolicy configuration of the Entity Type Names from the Commerce Engine SDK. Search queries will attempt to match fields copied into the _text_ field in the search provider's index schema.

<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_"/>

Note: I have only looked into the Solr configuration, so for those using Azure Search there may be some investigation work required to identify its search configuration.

What Configurations are Available for the Autocomplete Control?

Catalog Search

The catalog search is not used by the business tools by default. Instead, due to the low catalog entity count, the underlying code logic for associating a catalog to a price book or promotion book utilises the IFindEntitiesInListPipeline to populate a dropdown list control.

Note: Only the displayname and name fields will be present in the catalog entity indexes.

Category Search

The category search will return results for categories, regardless of the catalog it resides in.

Note: Only the displayname and name fields will be present in the category entity indexes.

Sellable Item Search

The sellable item search has two configurations available. One without variants included in the search results, and one with variants.

Sellable Item without Variants Search

Sellable Item with Variants Search

Sitecore Experience Commerce: Pricing Calculation Logic – Part 2

In this article, we highlight the expected pricing outcomes through decision tables.

For a high-level look at the important aspects of the business logic that determine list and sell prices, see Pricing Calculation Logic - Part 1.

Pricing Transparency (Advanced)

The following decision tables can be utilised to help understand and troubleshoot pricing outcomes.

Important notes:

  • The business logic applies to the OOTB Sitecore Commerce solution.
  • Price cards are resolved by the price book associated to the catalog.
  • These findings do not take into consideration promotions, specifically those that affect sellable item/variant sell price.

Sellable Item Price Calculation

These messages will be applied to the MessagesComponent of a  SellableItem entity.

In simplying the table, I took a few shortcuts to note:

  • 'Valid snapshot' conditions assume a price card has been resolved by name or tag. Refer to the previous flow diagrams in Pricing Calculation Logic - Part 1 to understand the more complex conditions.
  • For 'Has variants?', 'Calculate item list price in depth?', and 'Has list price for currency?' conditions, the Rules that state a group 'N'. However, I actually mean that some, but not all, of these conditions can be true.

Variant Price Calculation

These messages will be applied to the MessagesComponent of a  ItemVariationComponent within the SellableItem entity.

In simplying the table, I took a few shortcuts to note:

  • 'Valid snapshot' conditions assume a price card has been resolved by name or tag. Refer to the previous flow diagrams in Pricing Calculation Logic - Part 1 to understand the more complex conditions.

Cart Item Calculation (Sellable Item)

These messages will be applied to the MessagesComponent of a sellable item CartLineComponent.

Messages will be applied in the following order:

  1. Sellable Item Price Calculation
  2. Cart Item Calculation (Sellable Item)

Cart Item Calculation (Variant)

These messages will be applied to the MessagesComponent of a variant CartLineComponent.

Messages will be applied in the following order:

  1. Sellable Item Price Calculation
  2. Variant Price Calculation
  3. Cart Item Calculation (Variant)

References

Sitecore Experience Commerce: Pricing Calculation Logic – Part 1

In this multi-part article, we will review the Sellable Item/Variant price calculation processes that determines and differentiates the list pricing from the sell pricing.

As this is one of the more complex areas of Sitecore Commerce, Part 1 will cover the business logic used to apply the pricing, while Pricing Calculation Logic - Part 2 will highlight the expected pricing outcomes through decision tables.

This article is intended to suplement the official Pricing transparency documentation.

Introduction

The  MessagesComponent on the SellableItem entity, ItemVariationComponent, and CartProductComponent provides the audit trail for how the sellable item/variant pricing was calculated. The '<=' in the Text property is not a logic operator, but instead represents the assignment of the source to the price type.

{
	"@odata.type": "#Sitecore.Commerce.Core.MessagesComponent",
	"Id": "47edb873fe754c89ae4bc41d7c309811",
	"Name": "",
	"Comments": "",
	"Policies": [],
	"Messages": [
		{
			"Code": "Pricing",
			"Text": "SellPrice<=PriceCard.Snapshot: Price=$10.00|Qty=1.0|PriceCard=Habitat_PriceCard"
		},
		{
			"Code": "Pricing",
			"Text": "ListPrice<=PricingPolicy: Price=$1,919.69"
		}
	],
	"ChildComponents": []
}
{
	"@odata.type": "#Sitecore.Commerce.Core.MessagesComponent",
	"Id": "ed77a9de5db941ffbfcf0b217e3affda",
	"Name": "",
	"Comments": "",
	"Policies": [],
	"Messages": [
		{
			"Code": "Pricing",
			"Text": "Variation.SellPrice<=Variation.PriceCard.Snapshot: Price=$9.00|Qty=1.0|Variation=56042567|PriceCard=Habitat_VariantsPriceCard"
		},
		{
			"Code": "Pricing",
			"Text": "Variation.ListPrice<=Variation.PricePolicy: Variation=56042567|Price=$2,429.99"
		}
	],
	"ChildComponents": []
}
{
	"@odata.type": "#Sitecore.Commerce.Core.MessagesComponent",
	"Id": "7e6bc27965ef4f3a954e6fcdadb96fa5",
	"Name": "",
	"Comments": "",
	"Policies": [],
	"Messages": [
		{
			"Code": "Pricing",
			"Text": "SellPrice<=PriceCard.Snapshot: Price=$10.00|Qty=1.0|PriceCard=Habitat_PriceCard"
		},
		{
			"Code": "Pricing",
			"Text": "ListPrice<=PricingPolicy: Price=$1,919.69"
		},
		{
			"Code": "Pricing",
			"Text": "Variation.SellPrice<=Variation.PriceCard.Snapshot: Price=$9.00|Qty=1.0|Variation=56042567|PriceCard=Habitat_VariantsPriceCard"
		},
		{
			"Code": "Pricing",
			"Text": "Variation.ListPrice<=Variation.PricePolicy: Variation=56042567|Price=$2,429.99"
		},
		{
			"Code": "Pricing",
			"Text": "CartItem.SellPrice<=PriceCard.ActiveSnapshot: Price=$6.00|Qty=5.0"
		},
		{
			"Code": "Pricing",
			"Text": "CartItem.ListPrice<=SellableItem.Variation.ListPrice: Price=$2,429.99"
		}
	],
	"ChildComponents": []
}

The 2 primary pricing scenarios we see in Sitecore Commerce are:

  • The pricing of a sellable item/variant as is (per single unit)
  • The pricing of a sellable item/variant once added to the cart. Pricing calculations here may also affected by quantity, via price tiers of associated price cards, and specific promotions that affect the sell price.

Sellable Item/Variant Pricing Calculations

The first scenario, where we are retrieving a sellable item/variant, to display on the Product Listing or Product Details pages for example, calculates the pricing through the following steps:

  1. Calculate Sellable Item Sell Price
  2. Calculate Variations Sell Price
  3. Calculate Sellable Item List Price
  4. Calculate Variations List Price
  5. Reconcile Sellable Item Prices

Important notes:

  • The business logic applies to the OOTB Sitecore Commerce solution.
  • Price cards are resolved by the price book associated to the catalog.
  • These findings do not take into consideration promotions, specifically those that affect sellable item/variant sell price.

Calculate Sellable Item Sell Price

The CalculateSellableItemSellPriceBlock will attempt to determine the sell price for the sellable item, adding it to the PurchaseOptionMoneyPolicy.

Notable flow aspects

  • Only the first attempt at resolving a price card will be utilised. i.e. if the price card name associated to the sellable item cannot be resolved to a price card or valid snapshot, the price card by tags logic is not used as a fall back.

Calculate Variations Sell Price

The CalculateVariationsSellPriceBlock will attempt to determine the sell price for each variant of the sellable item.

Notable flow aspects

  • The flow diagram represents a single variant. The pipeline block will utilise this flow for each variant.
  • Only the first attempt at resolving a price card will be utilised.
  • If the price card is resolved from the sellable item, the message will not reflect that the price card was retrieved from the sellable item.

Calculate Sellable Item List Price

The CalculateSellableItemListPriceBlock will attempt to determine the list price for sellable item.

Notable flow aspects

  • The GlobalPricingPolicy's CalculateItemListPriceInDepth property is set to false by default.
  • When iterating over variants for a valid ListPricingPolicy, the first variant to have a valid ListPricingPolicy will be used. The logic does not look for the lowest or highest price amongst all variants.

Calculate Variations List Price

The CalculateVariationsListPriceBlock will attempt to determine the list price for each variant of the sellable item.

Reconcile Sellable Item Prices

The ReconcileSellableItemPricesBlock will attempt to resolve missing List Prices and Sell Prices from the sellable item and variants. A flow diagram has been created for the Sellable Item and Variants separately.

Notable flow aspects

  • Setting the list price to 0 will ensure a price has been assigned.

Notable flow aspects

  • Setting the list price to 0 will ensure a price has been assigned.

Cart Line Calculations

The second scenario mentioned was that where the pricing calculation of sellable items/variants that have been added to the cart. The following step is performed after the standard pricing calculations to take into consideration the price tiers from price cards:

  1. Calculate Cart Line Price

Calculate Cart Line Price

The CalculateCartLinePricesBlock will determine and apply the appropriate list and sell prices for each cart line.

Notable flow aspects

  • There is an expectation that the list price and sell prices would have been resolved in Reconcile Sellable Item Prices.
  • Where a snapshot exists on a sellable item/variant the cart line's quantity value will determine the price tier that will be utilised to retrieve the sell price from.
  • The messages from the Sellable Item and, if applicable, Variant are copied into the cart line MessagesComponent to document the pricing stacks, which assist in auditing pricing overrides in determining the final pricing.

Glossary

Navigating the pricing logic can be time-consuming due to similar and varied terms being used for similar or varying components, so here is a short glossary for better clarification.

  • Sellable Item/Variant List Pricing (Policy): The list pricing policy represents an array of prices (only one price per currency) that are associated to a sellable item/variant is specified under the Pricing entity view of the sellable item/variant in the Merchanding manager.
  • Price Card (Name): The price card or price card name associated to the sellable item/variant is specified under the Pricing entity view of the sellable item/variant in the Merchanding manager.
  • Sellable Item/Variant list price: This is the ListPrice property of a Sellable Item/Variant, not to be confused with the ListPricingPolicy.
    "Policies": [
    	{
    		"@odata.type": "#Sitecore.Commerce.Plugin.Pricing.ListPricingPolicy",
    		"PolicyId": "0d4cee51dffd44aa8a019394dd5ad1b1",
    		"Prices": [
    			{
    				"CurrencyCode": "USD",
    				"Amount": 1919.69
    			},
    			{
    				"CurrencyCode": "CAD",
    				"Amount": 2078.26
    			}
    		]
    	}
    ],
    "ListPrice": {
    	"CurrencyCode": "USD",
    	"Amount": 1919.69
    }
    
  • Sell Price: The sell price is associated to a Sellable Item/Variant or Cart Line via the PurchaseOptionMoneyPolicy.
    "Policies": [
    	{
    		"@odata.type": "#Sitecore.Commerce.Plugin.Pricing.PurchaseOptionMoneyPolicy",
    		"PolicyId": "12b963fe3b2b47618555c8783c877002",
    		"Models": [],
    		"Expires": "2019-04-22T12:30:46.5380974Z",
    		"SellPrice": {
    			"CurrencyCode": "USD",
    			"Amount": 10
    		},
    		"FixedSellPrice": false
    	}
    ]
    
  • (Cart Line) Unit List Price: This is the determined list price of the cart line.
    "Policies": [
    	{
    		"@odata.type": "#Sitecore.Commerce.Plugin.Pricing.PurchaseOptionMoneyPolicy",
    		"PolicyId": "5e6c1f42c7c94bc4b79696e716b6cda5",
    		"Models": [],
    		"Expires": "2019-04-22T13:13:18.8117816Z",
    		"SellPrice": {
    			"CurrencyCode": "USD",
    			"Amount": 6
    		},
    		"FixedSellPrice": false
    	}
    ],
    "UnitListPrice": {
    	"CurrencyCode": "USD",
    	"Amount": 2429.99
    }
    

References

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