Sitecore Experience Commerce: Implementing Multi-Step Actions in the Business Tools

In this article, we will look at multi-step actions and how we can implement them for customising the Sitecore Commerce Business Tools.

What is a Multi-Step Action?

The multi-step action is the approach to building out entity view modals to act as a kind of wizard, where inputs from each step can affect the subsequent steps.

Adding a qualification/condition to a promotion - step 1.
Adding a qualification/condition to a promotion - step 2.

A Dive into the Multi-Step Action Implementation

We will take a dive into the pieces that make up the multi-step action implementation to see just how we should be building our custom multi-step modal, using the Add Qualification modal for promotions as our working example.

This section is focused on explaining what will be found in the current platform implementation, which can be useful if looking to extend the BizFx and Commerce Engine functionality, but can be skipped if you are only after Implementing Custom Multi-Step Actions as quick as possible.

MultiStepActionPolicy

The MultiStepActionPolicy houses a single property, FirstStep, which will contain the EntityActionView that will be populated in the modal entity view.

public MultiStepActionPolicy()
{
    this.FirstStep = new EntityActionView();
}

public EntityActionView FirstStep { get; set; }

In the following code snippet, we see that the MultiStepActionPolicy is added to the EntityActionView, in which the QualificationsDetails that is assigned to the EntityView will use to populate the first step of the add qualification modal.

Line 18 highlights the Name of the entity view action, which will be resolved to its localised value from the .../Commerce Terms/BusinessTools/ViewActionNames/AddQualification, and populates modal's title.

var actionPolicy = arg.GetPolicy<ActionsPolicy>();
actionPolicy.Actions.Add(
        new EntityActionView(new List<Policy>
        {
            new MultiStepActionPolicy
            {
                FirstStep = new EntityActionView
                {
                    Name = context.GetPolicy<KnownPromotionsActionsPolicy>().SelectQualification,
                    DisplayName = "Select Qualification",
                    Description = "Selects a Qualification",
                    IsEnabled = isEnabled,
                    EntityView = context.GetPolicy<KnownPromotionsViewsPolicy>().QualificationDetails
                }
            }
        })
        {
            Name = context.GetPolicy<KnownPromotionsActionsPolicy>().AddQualification,
            DisplayName = "Add Qualification",
            Description = "Adds a Qualification",
            IsEnabled = isEnabled,
            EntityView = string.Empty,
            Icon = "add"
        });
The entity view populated in the first step, of the Add Qualification modal, is resolved from the entity view action name of the EntityActionView, assigned to the MultiStepActionPolicy's FirstStep, which in this case is the SelectQualification action.

LocalizeEntityViewBlock

In the LocalizeEntityViewBlock, we find that the SelectQualification action name is being populated with the localised term, however as mentioned above, the modal utilises the AddQualification localised term instead, therefore we don't need to be concerned with this.

if (!action.HasPolicy<MultiStepActionPolicy>())
{
    continue;
}

var firstStepAction = action.GetPolicy<MultiStepActionPolicy>().FirstStep;
await SetActionLocalizedTerms(firstStepAction, context).ConfigureAwait(false);

MultiStepActionModel

This model is quite simple. The NextStep represents the name of the action to be executed when the modal form is submitted.

public MultiStepActionModel(string nextStep)
{
    this.NextStep = nextStep;
}

public string NextStep { get; set; }

In the following extract of DoActionSelectQualificationBlock, I have substituted out most of the code with comments, explaining the original code logic, to reduce the noise and keep focus on the multi-step implementation functionality.

In line 11, we see that the original condition property has been made readonly as we don't want the user to change their mind at this stage, however we don't want to hide the property as the user still needs to have context of what was previously selected.

Updating the readonly status of submitted view properties is considered a recommended practice.

Line 15 we have a comment that tells us that this is where the next step's view properties are added to the current entity view, building out the modal form.

This also means that the Do Action Blocks acts a pseudo Get Entity View Block as the initial modal's entity view is populated via the GetEntityView() API, utilising the IGetEntityViewPipeline under the hood, while subsequent updates in the multi-step implementation are triggered as the DoUxAction() API calls the IDoActionPipeline to process requests.

Finally, line 17 has the original entity view action name, 'AddQualification', added to the MultiStepActionModel as the NextStep and applied to the commerce context.

With the knowledge that adding the MultiStepActionModel to the context effectively creates the next step in the modal, there is no limit to how many steps we can add to a multi-step modal.

public override async Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
    /* validate action */

    /* validate promotion */

    var selectedCondition = entityView.Properties.FirstOrDefault(p => p.Name.Equals("Condition", StringComparison.OrdinalIgnoreCase));

    /* validate condition */

    selectedCondition.IsReadOnly = true;

    /* add and/or conditional operator. hide if no qualifications have been applied to the promotion so far */

    /* add new view properties for selected condition */

    context.CommerceContext.AddModel(new MultiStepActionModel(context.GetPolicy<KnownPromotionsActionsPolicy>().AddQualification));

    return entityView;
}
DoActionSelectQualificationBlock validates the Condition selection and updates the entity view to contain the remaining view properties required for configuring the 'Cart Has [count] Items?' qualification as the second step of this multi-step action modal.

CheckForMultiStepActionBlock

From the following snippet from CheckForMultiStepActionBlock, we see that it updates the current entity views action, removes the MultiStepActionModel from the commerce context, and adds the entity view to the commerce context.

In short, this logic is more of a helper, and we could get the same result by omitting the registration of the MultiStepActionModel, updating the entity view action, and add the entity view directly in the Do Action Block instead.

var multiAction = context.CommerceContext.GetModels<MultiStepActionModel>().FirstOrDefault();
if (string.IsNullOrEmpty(multiAction?.NextStep))
{
    return Task.FromResult(arg);
}

entityView.Action = multiAction.NextStep;
context.CommerceContext.RemoveModel(multiAction);
context.CommerceContext.AddModel(entityView);

The DoUxAction API

The last piece of the puzzle comes with BizFx's handling of the response of the DoUxAction API when we submit the modal dialog.

If an entity view object is returned in the API response object, BizFx will render the modal with the updated view instead of closing the modal and refreshing the current page view.

If we were to change the modal's entity view name during any of the steps it will not be reflected in the modal's title as there is no handling for this implemented by default in BizFx.

Summary

Let's review what we have learnt about multi-step actions.

  1. Adding the MultiStepActionPolicy to an EntityActionView basically swaps out the intended action from being executed in the initial GetEntityView() request, however the modal's title will reflect the initial EntityActionView's localised name.
  2. The LocalizeEntityViewBlock application to the EntityActionView in the MultiStepActionPolicy's FirstStep is superfluous and does not impact the multi-step implementation.
  3. Do Action Blocks act as pseudo Get Entity View Block for updating the modal's entity view.
  4. When updating entity views with multi-step actions, previously input view properties should be set to readonly as a recommended practice.
  5. The usage of the MultiStepActionModel is more of a helper model in CheckForMultiStepActionBlock, rather than a dependent piece of the implementation.
  6. There is no limit to how many steps we can add to a multi-step action modal.
  7. The modal title is not updated with an updated entity view.
  8. When the Commerce Engine's DoUxAction() API returns an entity view in the response model, BizFx will render it in the current modal view.

Implementing Custom Multi-Step Actions

For implementing custom multi-step actions we will skip the details about how to build out the pre-requisites, being the initial Populate View Actions Block to create the entity view action that will trigger the modal, and the Get Entity View Block that will populate the initial entity view that is rendered in the modal, and instead focus on the Do Action Block that will allow us to add the additional steps to the modal.

The multi-step sample show the initial entity view that we will create the second step for.

Essentially, we can implement multi-step actions without using any of the MultiStep classes, simply by adding the new entity view to the commerce context during a Do Action Block, however providing a more complete sample, the following code logic would be applied to our custom Do Action Block.

  1. Validate action
  2. Validate entity (if applicable)
  3. Validate current view properties
  4. Set current view properties to read only
  5. Add new view properties to entity view for next step
  6. Update entity view action with action to perform on the next time the modal is submitted
  7. Add entity view to the commerce context
public override async Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
    Condition.Requires(entityView).IsNotNull($"{Name}: The argument cannot be null");

    /* 1. Validate action */
    if (string.IsNullOrEmpty(entityView?.Action)
        || !entityView.Action.Equals("FirstAction", StringComparison.OrdinalIgnoreCase))
    {
        return await Task.FromResult(entityView).ConfigureAwait(false);
    }

    /* 2. Validate entity (if applicable) */
    // Not applicable

    /* Validate current view properties */
    // Not critical for sample implementation

    /* Set current view properties to read only */
    foreach (var property in entityView.Properties)
    {
        property.IsReadOnly = true;
    }

    /* Add new view properties to entity view for next step */
    entityView.Properties.Add(new ViewProperty
    {
        Name = "Step 2 Field 1"
    });

    entityView.Properties.Add(new ViewProperty
    {
        Name = "Step 2 Field 2",
        IsRequired = false
    });

    /* Update entity view action with action to perform on the next time the modal is submitted */
    entityView.Action = "SecondAction";

    /* Add entity view to the commerce context */
    context.CommerceContext.AddModel(entityView);

    return await Task.FromResult(entityView).ConfigureAwait(false);
}
Our multi-step sample entity view after the modal has been submitted, creating the second step view for completing data input.

Sitecore Identity Server: Increasing the Token Lifetime for Local Development

In this article, we will review how to change the authentication token timeout values that force us to log back in to Sitecore or request a new token from Postman. If you are like me, generally working with Sitecore/Sitecore Commerce 10+ hours per day, 6 days a week, it can seem like you are kicked out every 5 minutes. Personally, I set these timeouts to a week (604800 seconds).

Changing the timeouts are not recommended for production instances.

Changing the Timeouts in Sitecore Identity Server

Sitecore Identity Server was first introduced with Sitecore Commerce 9.0.0 and with the release of Sitecore 9.1, Sitecore Identity Server was added to Sitecore authentication process.

Updating the Token Lifetimes in 9.0.X

  1. Open <Sitecore Identity Server root>\wwwroot\appsettings.json.
  2. Under AppSettings.Clients, update the CommerceBusinessTools and Postman API clients for BizFx and Postman applications respectively :-
    1. Update the AccessTokenLifetimeInSeconds and IdentityTokenLifetimeInSeconds from the default 3600 (seconds) to the desired timespan, in seconds.
    2. Save the configuration.
  3. Restart the Sitecore Identity Server so that the updated configuration is consumed on startup.

Updating the Token Lifetimes in 9.3

  1. Open <Sitecore Identity Server root>\Config\production\Sitecore.Commerce.IdentityServer.Host.xml.
  2. Under /Settings/Sitecore/IdentityServer/Clients, update the CommerceClient and PostmanClient for BizFx and Postman applications respectively:-
    1. Update the AccessTokenLifetimeInSeconds and IdentityTokenLifetimeInSeconds from the default 3600 (seconds) to the desired timespan, in seconds.
    2. Save the configuration.
  3. Restart the Sitecore Identity Server so that the updated configuration is consumed on startup.

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: Preparing a Clean Install of XC 9.1

In this article, we will look at preparing for the installation Sitecore Experience Commerce, by making the minimal amount of necessary changes for a baseline installation and avoiding the pitfalls with the default configuration. From this point, modifying the configurations for project-specific should be simpler.

This article is intended to supplement the Sitecore XC 9.1 Installation Guide for the download package version Sitecore.Commerce.2019.04-3.0.163.zip.

Pre-Installation

Before running the deployment script, we will correct the configuration and apply some workarounds in order to successfully install Sitecore Commerce.

Workarounds

  1. The <Identity Server web root>\Config\production\Sitecore.IdentityServer.Host.xml needs to be updated to include the new commerce site bindings from the $SiteHostHeaderName. ("sxa.storefront.com" from the deployment script).
    <Clients>
      <DefaultClient>
        <AllowedCorsOrigins>
      	  <AllowedCorsOriginsGroup1>http://SC911.sc</AllowedCorsOriginsGroup1>
      	  <AllowedCorsOriginsGroup2>http://sxa.storefront.com</AllowedCorsOriginsGroup2>
      	  <AllowedCorsOriginsGroup3>https://sxa.storefront.com</AllowedCorsOriginsGroup3>
        </AllowedCorsOrigins>
      </DefaultClient>
      ...
    </Clients>
    
  2. Follow Installing Sitecore Experience Commerce 9.1 with Default Storefront Tenant and Site.

Configuration

For the installation step, we assume that Sitecore XP 9.1.1 has been installed already from XP Single packages (XP0). The only change to the configuration that is made is to reset the password $SitecoreAdminPassword to "b".

Now, we need to reset the Deploy-Sitecore-Commerce.ps1 configurations to reasonable defaults.

To simplify the installation process, I place the Sitecore PowerShell Extensions zip file, Sitecore Experience Accelerator zip file, and Microsoft.Web.XmlTransform.dll file directly in the deployment folder.

The following parameters are reset back to XP 9.1.1 Single packages (XP0) configuration:

  • $Sitename
  • $SqlDbPrefix
  • $IdentityServerSiteName
param(
	[string]$SiteName = "XP0",
	[string]$SiteHostHeaderName = "sxa.storefront.com",
	[string]$SqlDbPrefix = "$SiteName",
	[string]$CommerceSearchProvider = "SOLR",
	[string]$IdentityServerSiteName = "$SiteName.IdentityServer"
)

The following parameters are reset back to XP 9.1.1 Single packages (XP0) configuration:

  • SiteName
  • InstallDir
  • XConnectInstallDir

The following parameters are updated so that the evaluations return a single (and correct) string response, as opposed to an array of strings:

  • SitecoreCommerceEnginePath
  • SitecoreBizFxServicesContentPath

The following parameters are configured to point back to the deployment directory:

  • PowerShellExtensionsModuleFullPath
  • SXAModuleFullPath
  • MergeToolFullPath

The following parameters are modified to point to the correct release zip files, provided in the Commerce package:

  • SXACommerceModuleFullPath
  • SXAStorefrontModuleFullPath
$params = @{
		Path                                        = Resolve-Path '.\Configuration\Commerce\Master_SingleServer.json'
		BaseConfigurationFolder                     = Resolve-Path '.\Configuration'
		SiteName                                    = "$SiteName.sc"
		SiteHostHeaderName                          = $SiteHostHeaderName
		InstallDir                                  = "c:\inetpub\wwwroot\$SiteName.sc"
		XConnectInstallDir                          = "c:\inetpub\wwwroot\$SiteName.xconnect"
		CommerceInstallRoot                         = "c:\inetpub\wwwroot\"        
		CommerceServicesDbServer                    = $($Env:COMPUTERNAME)    #OR "SQLServerName\SQLInstanceName"
		CommerceServicesDbName                      = "SitecoreCommerce9_SharedEnvironments"
		CommerceServicesGlobalDbName                = "SitecoreCommerce9_Global"
		SitecoreDbServer                            = $($Env:COMPUTERNAME)            #OR "SQLServerName\SQLInstanceName"
		SitecoreCoreDbName                          = "$($SqlDbPrefix)_Core"
		SitecoreUsername                            = "sitecore\admin"
		SitecoreUserPassword                        = "b"
		CommerceSearchProvider                      = $CommerceSearchProvider
		SolrUrl                                     = "https://solr:8992/solr"
		SolrRoot                                    = "c:\\solr-7.2.1"
		SolrService                                 = "solr-7.2.1"
		SolrSchemas                                 = ( Join-Path -Path $DEPLOYMENT_DIRECTORY -ChildPath "SolrSchemas" )
		CommerceServicesPostfix                     = "Sc9"
		CommerceServicesHostPostfix                 = "Sc9.qa"
		SearchIndexPrefix                           = "sitecore"
		EnvironmentsPrefix                          = "Habitat"
		Environments                                = @('AdventureWorksAuthoring', 'HabitatAuthoring')
		AzureSearchServiceName                      = ""
		AzureSearchAdminKey                         = ""
		AzureSearchQueryKey                         = ""
		CommerceEngineDacPac                        = Resolve-Path -Path "..\Sitecore.Commerce.Engine.SDK.*\Sitecore.Commerce.Engine.DB.dacpac"
		CommerceOpsServicesPort                     = "5015"
		CommerceShopsServicesPort                   = "5005"
		CommerceAuthoringServicesPort               = "5000"
		CommerceMinionsServicesPort                 = "5010"
		SitecoreBizFxPort                           = "4200"
		SitecoreCommerceEnginePath                  = Resolve-Path -Path "..\Sitecore.Commerce.Engine.3.0.163.zip"
		SitecoreBizFxServicesContentPath            = Resolve-Path -Path "..\Sitecore.BizFX.2.0.3"
		CommerceEngineCertificateName               = "storefront.engine"
		SiteUtilitiesSrc                            = ( Join-Path -Path $DEPLOYMENT_DIRECTORY -ChildPath "SiteUtilityPages" )
		HabitatImagesModuleFullPath                 = Resolve-Path -Path "..\Sitecore.Commerce.Habitat.Images-*.zip"
		AdvImagesModuleFullPath                     = Resolve-Path -Path "..\Adventure Works Images.zip"
		CommerceConnectModuleFullPath               = Resolve-Path -Path "..\Sitecore Commerce Connect*.zip"
		CommercexProfilesModuleFullPath             = Resolve-Path -Path "..\Sitecore Commerce ExperienceProfile Core *.zip"
		CommercexAnalyticsModuleFullPath            = Resolve-Path -Path "..\Sitecore Commerce ExperienceAnalytics Core *.zip"
		CommerceMAModuleFullPath                    = Resolve-Path -Path "..\Sitecore Commerce Marketing Automation Core *.zip"
		CommerceMAForAutomationEngineModuleFullPath = Resolve-Path -Path "..\Sitecore Commerce Marketing Automation for AutomationEngine *.zip"
		CEConnectModuleFullPath                     = Resolve-Path -Path "..\Sitecore Commerce Engine Connect*.zip"
		PowerShellExtensionsModuleFullPath          = Resolve-Path -Path "..\Sitecore PowerShell Extensions*.zip"
		SXAModuleFullPath                           = Resolve-Path -Path "..\Sitecore Experience Accelerator*.zip"
		SXACommerceModuleFullPath                   = Resolve-Path -Path "..\Sitecore Commerce Experience Accelerator 2.*.zip"
		SXAStorefrontModuleFullPath                 = Resolve-Path -Path "..\Sitecore Commerce Experience Accelerator Storefront 2.*.zip"
		SXAStorefrontThemeModuleFullPath            = Resolve-Path -Path "..\Sitecore Commerce Experience Accelerator Storefront Themes*.zip"
		SXAStorefrontCatalogModuleFullPath          = Resolve-Path -Path "..\Sitecore Commerce Experience Accelerator Habitat Catalog*.zip"
		MergeToolFullPath                           = Resolve-Path -Path "..\Microsoft.Web.XmlTransform.dll"
		UserDomain                                  = $Env:COMPUTERNAME
		UserName                                    = 'CSFndRuntimeUser'
		UserPassword                                = 'Pu8azaCr'                          

		BraintreeAccount                            = @{
			MerchantId = ''
			PublicKey = ''
			PrivateKey = ''
		}
		SitecoreBizFxServerName                     = "SitecoreBizFx"
		SitecoreIdentityServerApplicationName       = $IdentityServerSiteName
		SitecoreIdentityServerHostName              = $IdentityServerSiteName
	}

Post Installation

  • During the installation the original bindings for the Sitecore website, e.g. 'XP0.sc', are removed. Add these bindings back in IIS.
  • If you add the Postman collections/environments to Postman, the
    SitecoreIdServerHost will need to be updated to point to 'XP0.identityserver'.

Installing Sitecore Experience Commerce 9.1 with Default Storefront Tenant and Site

In this article, we will look at applying a workaround in order to install the default storefront tenant and site during the Sitecore Commerce 9.1 installation as unfortunately there's a bug that prevents these from being created during the installation.

Workaround Steps

Note: This has been tested and verified against the On Premise packages.

 

  1. Following on from the installation guide's step, 2.2. Download the Sitecore XC release package and prerequisites, navigate and extract the file named xml from Sitecore Commerce Experience Accelerator Storefront 2.0.181.zip\package.zip\items\master\sitecore\system\Modules\PowerShell\Script Library\CXA - Internal\Web API\CreateDefaultStorefrontTenantAndSite\{6FEC77C8-00DC-4B7B-9597-82588616A1F2}\en\1.
  2. Open the file and add the following snippet before Function CreateCXATenant.
    #Override Write-Progress to avoid errors which happen because of impossibility to write-progress in non-interactive sessions
    Function Write-Progress {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory = $false)]
            $Activity,
            [Parameter(Mandatory = $false)]
            $CurrentOperation,
            [Parameter(Mandatory = $false)]
            $Status,
            [Parameter(Mandatory = $false)]
            $PercentComplete,
            [Parameter(Mandatory = $false)]
            [switch]$Completed
        )
        process {
            # do nothing
        }
    }
    
  3. Replace the file in the archive with the updated file.
  4. Continue to follow the installation guide.

When the deployment script run in step 3.2. Run the deployment script, the default tenant and site will now be created.