Tuesday, May 22, 2018

Sitecore Experience Commerce - Service Proxy execute compilation error

When attempting to call my custom API in the commerce engine, I was getting a compilation error on the Execute method of a service proxy call.
Cannot convert from 'Microsoft.OData.Client.DataServiceActionQuerySingle<MyModel>' to 'Microsoft.OData.Client.DataServiceActionQuery<MyModel>' 
At first I thought it was a duplicate method across both the shops and ops proxy. But as it turns out, I was expecting a return type of collection and the proxy was expecting a single return object (hence the single at the end of DataServiceActionQuerySingle).

This was resolved by going back into the commerce engine where I configured the API and changing the return type from:
configuration.ReturnsFromEntitySet<ExtendedPricingDetail>("Api");
To:
 configuration.ReturnsCollectionFromEntitySet<ExtendedPricingDetail>("Api");
Then a re-generation of the service proxy and all was working as expected.

Monday, May 21, 2018

Sitecore Experience Commerce - Creating a custom API in the Commerce Engine

Due to a customization in how my Sitecore Experience Commerce implementation structured pricing, there was a requirement for a custom API which returned this additional data. In my post Drilling into storefront rendering code and commerce engine pipeline code, I dug down to the proxy executing the API GetBulkPrices. In this post I am going to create my own custom implementation GetExtendedBulkPrices.

The return model

First I needed to create a model, which I wanted this method to return.
using System;
using System.ComponentModel.DataAnnotations;

namespace Plugin.MyProject.PriceDetail.Model
{
    public class ExtendedPricingDetail
    {
        [Key]
        public string ProductId { get; set; }
        public Decimal OriginalPrice { get; set; }
        public int Flybuys { get; set; }
        public string PromotionCode { get; set; }
        public string PromotionText { get; set; }

        public ExtendedPricingDetail()
        {
            ProductId = string.Empty;
            OriginalPrice = 0.00M;
            Flybuys = 0;
            PromotionCode = string.Empty;
            PromotionText = string.Empty;
        }
    }
}
The key attribute was required as it would error otherwise.

The command

I needed a command for my custom API to call. A command will generally not contain any business logic and will simply be used to call a pipeline. This post won't go as deep as pipelines, however my empty commerce minion code sample post has a pipeline example.
using Plugin.MyProject.PriceDetail.Model;
using Plugin.MyProject.PriceDetail.Pipelines;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Commerce.Plugin.Catalog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Plugin.MyProject.PriceDetail.Commands
{
    public class GetExtendedBulkPricesCommand : CommerceCommand
    {
        private readonly IExtendedPricingDetailPipeline _pipeline;

        public GetExtendedBulkPricesCommand(IExtendedPricingDetailPipeline getSellableItemPipeline, IServiceProvider serviceProvider) : base(serviceProvider)
        {
            _pipeline = getSellableItemPipeline;
        }

        public async Task<IEnumerable<ExtendedPricingDetail>> Process(CommerceContext commerceContext, IEnumerable<string> itemIds)
        {
            using (CommandActivity.Start(commerceContext, this))
            {
                CommercePipelineExecutionContextOptions contextOptions = commerceContext.GetPipelineContextOptions();
                var items = new List<ExtendedPricingDetail>();

                foreach (var current in itemIds)
                {
                    ProductArgument productArgument = ProductArgument.FromItemId(current);
                    if (!productArgument.IsValid())
                    {
                        string str = await contextOptions.CommerceContext.AddMessage(commerceContext.GetPolicy<KnownResultCodes>().Error, "ItemIdIncorrectFormat", new object[1]
                        {
                            (object) current
                        }, string.Format("Expecting a CatalogId and a ProductId in the ItemId: {0}.", (object)current));
                    }
                    else
                    {
                        var priceDetail = await _pipeline.Run(productArgument, contextOptions.CommerceContext.GetPipelineContextOptions());
                       
                        if (priceDetail != null)
                        {
                            items.Add(priceDetail);
                        }
                    }
                }

                return items.AsEnumerable();
            }
        }
    }
}
As you can see, this example calls my custom pipeline IExtendedPricingDetailPipeline.

The API Controller

Now we need a controller with an action which can act as an API method, this code is a standard MVC example.
using Sitecore.Commerce.Core;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web.Http.OData;
using Plugin.MyProject.PriceDetail.Commands;

namespace Plugin.MyProject.PriceDetail.Controller
{
    public class ApiController : CommerceController
    {
        public ApiController(IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment): base(serviceProvider, globalEnvironment)
        {
        }

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

            var bulkCommand = Command<GetExtendedBulkPricesCommand>();
            var res = await bulkCommand.Process(this.CurrentContext, jarray != null ? jarray.ToObject<IEnumerable<string>>() : (IEnumerable<string>)null);

            return (IActionResult)new ObjectResult(res);
        }
    }
}

In this example we are calling this ApiController, which means it follows an example of a Shops API method. Much like the GetBulkPrices method, that means the Postman call URL for this API would be {{ServiceHost}}/{{ShopsApi}}/GetExtendedBulkPrices(). It takes in an input of a list of strings and will output my custom model defined above.

This simply calls my command (as defined above).

Registering Exerything

Inside the Configure Sitecore class for the given plugin which contains these newly added classes, they need to be configured. This is done as follows (and does not show the Pipeline configuration):
public void ConfigureServices(IServiceCollection services)
{
    var assembly = Assembly.GetExecutingAssembly();
    services.RegisterAllCommands(assembly);

    services.Sitecore().Pipelines(config =>
        config
            .ConfigurePipeline<IConfigureServiceApiPipeline>(c => 
            {
                c.Add<RegisterPricingApiBlock>();
            })
    );
}
There is a single method which will register all commands, and added is a reference to RegisterPricingApiBlock. This is where the API and it's added entities are registered/built up for the oData service layer (and therefore commerce proxy used from the front-end). The code for this class is:
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Builder;
using Plugin.MyProject.PriceDetail.Model;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;

namespace Plugin.MyProject.PriceDetail.Pipelines.Blocks
{
    class RegisterPricingApiBlock : PipelineBlock<ODataConventionModelBuilder, ODataConventionModelBuilder, CommercePipelineExecutionContext>
    {
        public RegisterPricingApiBlock() { }

        public override Task<ODataConventionModelBuilder> Run(ODataConventionModelBuilder modelBuilder, CommercePipelineExecutionContext context)
        {
            Condition.Requires(modelBuilder).IsNotNull($"{base.Name}: The argument can not be null");

            modelBuilder.AddEntityType(typeof(ExtendedPricingDetail));
            modelBuilder.EntitySet<ExtendedPricingDetail>("ExtendedPricingDetails");

            var configuration = modelBuilder.Action("GetExtendedBulkPrices");
            configuration.CollectionParameter<string>("itemIds");
            configuration.ReturnsCollectionFromEntitySet<ExtendedPricingDetail>("Api");

            return Task.FromResult(modelBuilder);
        }
    }
}
Here the entity (custom model) and API method have been registered, which now allows them to be called via Postman and accessed via the Service Proxy project.

Calling the API via Postman

The API call can be a copy of one of the existing examples that come with the SDK. In my case, as I was using the same input (string array) as the GetBulkPrices API call, I simply copied this and updated the action name.

Calling the API via Postman


Updating the Service Proxy

Rob Earlam, has a great explanation up about what the Service Proxy is. Basically it's used to be able to share models between the .NET Core commerce Engine and standard .NET Sitecore website. However as I have added a custom model, I will need this to re-generate at the Service Proxy level.

The prerequisite for this is to install the OData v4 Client Code Generator plugin into Visual Studio 2017.

Now you simply add the Sitecore.Commerce.ServiceProxy project to your solution and under connected services are the ops and shops services which have a right click option to update (as long as a commerce engine instance is listening on port 5000). There is a JSON configuration if required.

Regenerating the Service Proxy

Conclusion

This was a simple example of how a custom API endpoint can be added to the commerce engine to call custom code and return custom models. This code could be slightly modified/expanded to actually extend a given model in the commerce engine layer.

Sitecore Experience Commerce - The entity does not have a key defined

After creating a custom command, API and return entity, I was getting the following error when starting the commerce engine in debug mode.
The entity 'MyModel' does not have a key defined
at Microsoft.AspNetCore.OData.Builder.ODataModelBuilder.ValidateModel(IEdmModel model)
The fix was to add the [Key] attribute to one of the attributes of my model. This can be found under the System.ComponentModel.DataAnnotations model.
public class ExtendedPricingDetail
{
 [Key]
 public string ProductId { get; set; }
 public Decimal CurrentPrice { get; set; }
 public Decimal OriginalPrice { get; set; }
 public string PromotionCode { get; set; }
 public string PromotionText { get; set; }
 public bool OnSpecial { get; set; }

 public ExtendedPricingDetail()
 {
  ProductId = string.Empty;
  CurrentPrice = 0.00M;
  OriginalPrice = 0.00M;
  PromotionCode = string.Empty;
  PromotionText = string.Empty;
  OnSpecial = false;
 }
}

Sitecore Experience Commerce - Command won't resolve in commerce engine

I have a custom API in the commerce engine which was able to be called via Postman. However when debugging the API, it was unable to get a copy of my command. The line of code which was failing was:
var myCommand = Command<MyCustomCommand>();
This was returning null. What was missing that the command was not properly being registered, this is done in the  ConfigureSitecore class of the plugin which contains the command and is achieved with the following line of code:
services.RegisterAllCommands(assembly);
After this the command was able to be accessed and run correctly.

Thursday, May 10, 2018

Sitecore Experience Commerce - Brand facet is splitting text by spaces

During an implementation of a product list in Experience Commerce, I found that the brand facet was splitting the brand's text at the occurrence of spaces. So a brand of "My Brand" would appear as two separate facet items: "my" and "brand".

The field was set to "UN_TOKENIZED" which should have not allowed this to happen. Nevertheless, the fix is to change the fields returnType from "test" to "string". Then rebuild the web and master indexes for your Sitecore instance. Below is a patch file to make this change (for SOLR search provider).

    
        
            
                
                    
                        
                            
                        
                    
                
            
        
   

Wednesday, May 9, 2018

Sitecore Experience Commerce - A minion to import/sync catalogs and products

At the time of release, Sitecore Experience Commerce came with an API method which could be used to import and export commerce data. It used a zip containing many JSON files, each of which was a catalog, sellable item, category or relationship of some type. This is handy for once off exports of data which could then be imported into another environment (perhaps developer machines). But for a real world environment, something more robust is required and that's where a custom import minion comes in.

Introduction to minions

In Sitecore Commerce a minion can be thought of as a scheduled task, which runs in the minions commerce engine environment and has access to the full commerce context. 

The habitat minions environment which comes out of the box with commerce (defined in the PlugIn.Habitat.CommerceMinions-1.0.0.json file) has minions defined such as the full index minion and the incremental index minion. These two minions both handle indexing of the commerce data into SQL. The incremental index minion is scheduled to run every 5 minutes and looks for new or updated items to index and the full index minion is not scheduled and can be called via the run minion API call.

Creating a commerce minion and defining it's schedule has been covered in an earlier post. Much like a standard Sitecore pipeline it has a run method which is where things kick off (including initialization). From here you are able to call commerce pipelines (either your own custom or the out of the box ones). 

A pipeline can be consider a wrapper for a number of blocks which will contain the actual code and logic. As an example, there might be a need to get a Sellable Item which already exists inside Sitecore commerce. This is done using the pipeline IGetSellableItemPipeline. If you were to look inside the log files of one of your commerce engine environments (NodeConfiguration_Deployment01 log files in particular), you would see this pipeline is configured as follows:
IGetSellableItemPipeline (Sitecore.Commerce.Plugin.Catalog.ProductArgument => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Catalog.GetSellableItemInitializeBlock (Sitecore.Commerce.Plugin.Catalog.ProductArgument => Sitecore.Commerce.Plugin.Catalog.ProductArgument)     ------------------------------------------------------------     Plugin.Catalog.GetSellableItemBlock (Sitecore.Commerce.Plugin.Catalog.ProductArgument => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Inventory.ResolveSellableItemInventoryInformationBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Catalog.EnsureSellableItemPoliciesBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Availability.EnsureSellableItemAvailabilityPoliciesBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Catalog.ICalculateSellableItemPricesPipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Availability.IPopulateItemAvailabilityPipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Catalog.AddSellableItemToContextBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
It takes in an argument of ProductArgument and will output a SellableItem. You can see that the pipeline itself calls a number of various blocks, each of which performs it's own piece of logic/code. In fact this particular pipeline actually calls additional pipelines (such as ICalculateSellableItemPricesPipeline) which again call their configured blocks (and perhaps further pipelines).

The import minion structure

If you are looking to build an import/sync minion then you obviously have the data available in some format. In my case, I was building a sync minion and used SQL server to provide delta information of the data for each run of the minion. This was structured into several key areas (each of which could be accessed and actioned in a separate block of my import minions custom pipeline. These are the required actions I identified:
  1. Added category
  2. Added sellable item
  3. Added ranging (a given sellable item appearing in a given catalog)
  4. Removed ranging (a given sellable item no longer appearing in a given catalog)
  5. Deleted sellable item
  6. Deleted category
  7. Updated sellable item
Therefore I structured my minion as follows:
  • Import Minion
    • Import Pipelines
      • ProcessAddedCategories
      • ProcessAddedSellableItems
      • ProcessRanging
      • ProcessDeranging
      • ProcessDeletedSellableItems
      • ProcessDeletedCategories
      • ProcessUpdatedSellableItems
Note that this is a cut down example, from what I actually built as there was a large amount of business requirements which dictated the needs to separate catalogs for distinct stores each of which required their own price books and price cards to handle separate pricing at each.

The import minion code

The example commerce minion code solution I have placed on GitHub is a great start for this project, as it comes with the minion itself along with a pipeline and block. That means that it only would need the extra blocks created as defined above.

I'm not going to provide a full working example of an import or sync minion, but instead will show the commerce (out of the box) pipelines you would call for some of the key interactions (creating a sellable item for example). These pipelines would be called as part of the relevant block section inside a loop (for example the loop of categories to add inside ProcessAddedCategories).

The reason we call these commerce pipeline (as opposed to the service calls to the API) is the fact that those APIs will actually themselves call the pipeline code. This saves going over the wire and dramatically will cut down on processing time.

To see which pipelines are available, head over to the NodeConfiguration_Deployment01 log files in one of your commerce instance engines logs folder (C:\inetpub\wwwroot\CommerceMinions_Sc9\wwwroot\logs in my case). A search through this for keywords such as SellableItem as an example, can show you whats available. From there it's a bit of trial and error to correctly call the pipeline. 

The following code snippets and commerce minions will require access to the Sitecore Commerce nuget repository. You should add this to Visual Studio: https://sitecore.myget.org/F/sc-commerce-packages/api/v3/index.json.

Adding a SellableItem

This example shows how to create a basic sellable item and once completed will return a SellableItem object. 
using Sitecore.Commerce.Core;
using Sitecore.Framework.Pipelines;
using System;
using System.Linq;
using System.Threading.Tasks;
using Sitecore.Commerce.Plugin.Catalog;

namespace Plugin.MyProject.Import
{
    class ImportMinionBlock : PipelineBlock<MinionRunResultsModel, MinionRunResultsModel, CommercePipelineExecutionContext>
    {
        private readonly ICreateSellableItemPipeline _createSellableItemPipeline;

        public ImportMinionBlock(ICreateSellableItemPipeline createSellableItemPipeline)
        {
            _createSellableItemPipeline = createSellableItemPipeline;
        }

        public override async Task<MinionRunResultsModel> Run(MinionRunResultsModel arg, CommercePipelineExecutionContext context)
        {
            try
            {
                var result = await _createSellableItemPipeline.Run(new CreateSellableItemArgument("MyProdID", "TestProduct", "Test Product", "This is a description of my test product"), context);

                if (result != null) // Null here means it failed to add
                {
                    SellableItem addedItem = result.SellableItems.FirstOrDefault(); // This is the item which has just been added
                }
            }
            catch (Exception)
            {
                // Some sort of failure - for example trying to add a duplicate product
                throw;
            }

            return arg;
        }
    }
}
The key pipeline in this case is the ICreateSellableItemPipeline.


Editing/updating a SellableItem

Now lets continue on from the previous example and add some additional data to our SellableItem.
using Sitecore.Commerce.Core;
using Sitecore.Framework.Pipelines;
using System;
using System.Linq;
using System.Threading.Tasks;
using Sitecore.Commerce.Plugin.Catalog;

namespace Plugin.MyProject.Import
{
    class ImportMinionBlock : PipelineBlock<MinionRunResultsModel, MinionRunResultsModel, CommercePipelineExecutionContext>
    {
        private readonly ICreateSellableItemPipeline _createSellableItemPipeline;
        private readonly IPersistEntityPipeline _persistEntityPipeline;

        public ImportMinionBlock(ICreateSellableItemPipeline createSellableItemPipeline, IPersistEntityPipeline persistEntityPipeline) 
        {
            _createSellableItemPipeline = createSellableItemPipeline;
            _persistEntityPipeline = persistEntityPipeline;
        }

        public override async Task<MinionRunResultsModel> Run(MinionRunResultsModel arg, CommercePipelineExecutionContext context)
        {
            try
            {
                var result = await _createSellableItemPipeline.Run(new CreateSellableItemArgument("MyProdID", "TestProduct", "Test Product", "This is a description of my test product"), context);

                if (result != null) // Null here means it failed to add
                {
                    SellableItem addedItem = result.SellableItems.FirstOrDefault(); // This is the item which has just been added

                    addedItem.Brand = "My Brand";
                    addedItem.Manufacturer = "My Manufacturer";
                    addedItem.ListPrice = new Money("USD", 9.99m);

                    var saveResult = _persistEntityPipeline.Run(new PersistEntityArgument(addedItem), context);
                }
            }
            catch (Exception)
            {
                // Some sort of failure - for example trying to add a duplicate product
                throw;
            }

            return arg;
        }
    }
}
The key pipeline used here is the IPersistEntityPipeline, and in fact can be used to persist (or save) any commerce entity of which you make changes to.

Final thoughts

Hopefully this blog post has provided a good base for anyone who is looking to create a Sitecore minion to handle importing and syncing products. As someone who has built a complete sync minion here are a few further tips:
  • Work with subsets of product data instead of testing you minion with larger data sets.
  • Test out how the minion will handle any corrupted data - that commerce server might not like
  • Logging is your best friend, as in many cases an exception on a given pipeline will stop the whole minion run
  • The example Habitat data that comes with commerce is your best friend, and is an example of what your end product should look like.
Any further questions, feel free to comment below or contact me on twitter @RyanBaileyNZ.

Friday, May 4, 2018

Sitecore Experience Commerce - Drilling into storefront rendering code and commerce engine pipeline code

After importing my own catalog containing various categories and sellable items into Sitecore Experience Commerce. I found that pricing was displaying as $0 for all items instead of resolving via price cards.

The problem rendering

Sitecore Experience Commerce - Product list product
The presentation details of the search results page, showed that the rendering which was incorrectly displaying pricing was the 'Product List' rendering.

Search results page renderings
This rendering then traced back to the following controller: Sitecore.Commerce.XA.Feature.Catalog.Controllers.CatalogController found inside the Sitecore.Commerce.XA.Feature.Catalog DLL.

On the load of the page, the product list also populates after page load via a JavaScript call. The network tab of the console in chrome shows this call as GetProductList.

Experience Commerce - API call

Tracing the website code

Using dotPeek we are able to de-compile the DLL and view the code contained inside the controller.

Controller source 
The main controller action of ProductList (and therefore it's model source of ProductListRepository.GetProductListRenderingModel()) is not the real source of this data, as it actually comes via an API call. However that same ProductListRepository has a method called GetProductListJsonResult and this is where the API call gets it's data from. Looking into this method leads down a rabbit hole as follows:
  1. AdjustProductPriceAndStockStatus
  2. CatalogManager.GetProductBulkPrices
  3. PricingManager.GetProductBulkPrices
  4. PricingServiceProvider.GetProductBulkPrices
  5. RunPipeline<GetProductBulkPricesRequest, GetProductBulkPricesResult>("commerce.prices.getProductBulkPrices", request);
    1. Resolves to Sitecore.Commerce.Engine.Connect.Pipelines.Prices.GetProductBulkPrices
    2. Then calls Proxy.Execute<SellableItemPricing> ... GetBulkPrices
  6. Sitecore.Commerce.Engine.Container.GetBulkPrices
    1. References the URI: "/GetBulkPrices"
  7. Sitecore.Commerce.Plugin.Catalog.ApiController.GetBulkPrices
  8. IGetSellableItemPipeline
After all of that digging it finally leads to a commerce engine pipeline being run. This is where the actual logic will query the commerce data and attempt to resolve the price.

Tracing Commerce Engine code

How the commerce engine works is that a given pipeline such as IGetSellableItemPipeline will call a number of blocks (which is where code the actual logic sits) or even another pipeline. To see what code a given pipeline is actually calling, head over to the logs folder of one of your commerce engine instances (C:\inetpub\wwwroot\CommerceAuthoring_Sc9\wwwroot\logs for example) and open up the latest NodeConfiguration log file.

Performing a search in the file for IGetSellableItemPipeline returns the following:
Sitecore.Commerce.Plugin.Catalog
IGetSellableItemPipeline (Sitecore.Commerce.Plugin.Catalog.ProductArgument => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.GetSellableItemInitializeBlock (Sitecore.Commerce.Plugin.Catalog.ProductArgument => Sitecore.Commerce.Plugin.Catalog.ProductArgument)
     ------------------------------------------------------------
     Plugin.Catalog.GetSellableItemBlock (Sitecore.Commerce.Plugin.Catalog.ProductArgument => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Inventory.ResolveSellableItemInventoryInformationBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.EnsureSellableItemPoliciesBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Availability.EnsureSellableItemAvailabilityPoliciesBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.ICalculateSellableItemPricesPipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Availability.IPopulateItemAvailabilityPipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.AddSellableItemToContextBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
What this means is that for each call to IGetSellableItemPipeline, it will run each of these children blocks and pipelines in turn. Because I am interested in how pricing is resolved for a sellable item, the ICalculateSellableItemPricesPipeline is the where this happens. This pipeline is configured as:
Sitecore.Commerce.Plugin.Catalog
ICalculateSellableItemPricesPipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.GetSellableItemCatalogBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.ICalculateSellableItemSellPricePipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.ICalculateSellableItemListPricePipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
     ------------------------------------------------------------
     Plugin.Catalog.ReconcileSellableItemPricesBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
In my case, as I am not looking to debug list price and instead pricing via price cards the ICalculateSellableItemSellPricePipeline is what is relevant to investigate. This pipelines is configured as:
Sitecore.Commerce.Plugin.CatalogICalculateSellableItemSellPricePipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Catalog.CalculateSellableItemSellPriceBlock (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Plugin.Catalog.ICalculateVariationsSellPricePipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)
Which finally leads to a code block CalculateSellableItemSellPriceBlock and this is where the true logic to calculate the price for a given sellable item is.

Replacing and debugging Sitecore Commerce Engine code

The great thing about the Sitecore Commerce Engine, is that it is fully customizable. Any of the pipelines above can be removed, replaced and even reconfigured (in terms of what blocks/pipelines the pipeline run). In my case I would like to replace the CalculateSellableItemSellPriceBlock with my own version (the same code taken via DotPeek) so that it can be debugged and changed if required.

This is easily achieved by using the Commerce Engine source solution (which is what is deployed to the 4 commerce engine roles in IIS). This is the Sitecore.Commerce.Engine project and comes with the SDK when you install Experience Commerce (in my case Sitecore.Commerce.Engine.SDK.2.1.10). The Sitecore.Commerce.Engine project out of the box will reference a number of plugins, such as the braintree payment provider - Plugin.Sample.Payments.Braintree. In my case I have a number of custom plugins to extend commerce entities such as Price Cards and Sellable items. 

The first step is to create the replacement block inside a plugin project. I have called this example: CalculateSellableItemSellPriceBlockCustom
namespace MyPlugin.Pipelines.Blocks
{
    [PipelineDisplayName("Catalog.block.calculatesellableitemsellprice")]
    public class CalculateSellableItemSellPriceBlockCustom : PipelineBlock<SellableItem, SellableItem, CommercePipelineExecutionContext>
    {
        private readonly IResolveActivePriceSnapshotByCardPipeline _resolveSnapshotByCardPipeline;
        private readonly IResolveActivePriceSnapshotByTagsPipeline _resolveSnapshotByTagsPipeline;
        private IGetCatalogPipeline _getCatalogPipeline;

        public CalculateSellableItemSellPriceBlockCustom(IResolveActivePriceSnapshotByCardPipeline resolveSnapshotByCardPipeline, IGetCatalogPipeline getCatalogPipeline, IResolveActivePriceSnapshotByTagsPipeline resolveActivePriceSnapshotByTagsPipeline)
          : base((string)null)
        {
            this._resolveSnapshotByCardPipeline = resolveSnapshotByCardPipeline;
            this._resolveSnapshotByTagsPipeline = resolveActivePriceSnapshotByTagsPipeline;
            _getCatalogPipeline = getCatalogPipeline;
        }

        public override async Task<SellableItem> Run(SellableItem arg, CommercePipelineExecutionContext context)
        {
            // Code removed
            return arg;
        }
    }
}
Inside each plugin is a ConfigureSitecore class, it's in here that we can configure the replacement of the out of the box CalculateSellableItemSellPriceBlock block to my custom CalculateSellableItemSellPriceBlockCustom block.
public void ConfigureServices(IServiceCollection services)
{
 var assembly = Assembly.GetExecutingAssembly();
 services.RegisterAllPipelineBlocks(assembly);

 services.Sitecore().Pipelines(config =>
  config
   .ConfigurePipeline<ICalculateSellableItemSellPricePipeline>(c =>
   {
    c.Replace<CalculateSellableItemSellPriceBlock, CalculateSellableItemSellPriceBlockCustom>();
   })
 );
}
Now the code can be debugged and customized as required by a given commerce instances business rules. If you open up the node configuration after deploying the commerce engine, you can see that the out of the box pipeline is not referenced and the custom one is instead:
Sitecore.Commerce.Plugin.CatalogICalculateSellableItemSellPricePipeline (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)     ------------------------------------------------------------     Myproject.Pipelines.Blocks.CalculateSellableItemSellPriceBlockCustom (Sitecore.Commerce.Plugin.Catalog.SellableItem => Sitecore.Commerce.Plugin.Catalog.SellableItem)

Conclusion

Ultimately I traced my particular error back to a timezone issue (which stopped price card snapshots resolving correctly). However this post serves as an example of the processes required to trace back issues from the Commerce Experience Accelerator components back to the Sitecore Commerce Engine.

Tuesday, April 24, 2018

Sitecore Experience Commerce - No commerce data in Sitecore indexes

After creating a catalog with product/category data and hooking it up to your Storefront site, you might expect the data to start appearing on the front-end. Even after full index rebuilds of the web and master indexes, this was not working as expected for me. The query I was using on the web index was:
commercesearchitemtype_t:"Category"
Which would be expected to return me all categories as defined in the catalog. Likewise the following, should return all products (sellable items):
commercesearchitemtype_t:"SellableItem"
 In my case I had changed the environment name from "HabitatAuthoring", and had updated the defaultEnvironment setting. However there are a number of index related settings which require the updated environment as well. The following patch file if applied should correctly connect the Sitecore indexer up with the correct commerce environment.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore role:require="Standalone or ContentDelivery or ContentManagement">
        <contentSearch>
            <indexConfigurations>
                <indexUpdateStrategies>
                    <sellableItemsIntervalAsynchronousStrategyMaster type="Sitecore.Commerce.Engine.Connect.Search.Strategies.SellableItemsIntervalAsynchronousStrategy, Sitecore.Commerce.Engine.Connect">
                        <Environments hint="list">
                            <environment patch:instead="environment">MyAuthoring</environment>
                        </Environments>
                    </sellableItemsIntervalAsynchronousStrategyMaster>
                    <sellableItemsIntervalAsynchronousStrategyWeb type="Sitecore.Commerce.Engine.Connect.Search.Strategies.SellableItemsIntervalAsynchronousStrategy, Sitecore.Commerce.Engine.Connect">
                        <Environments hint="list">
                            <environment patch:instead="environment">MyAuthoring</environment>
                        </Environments>
                    </sellableItemsIntervalAsynchronousStrategyWeb>
                    <categoriesIntervalAsynchronousStrategyMaster type="Sitecore.Commerce.Engine.Connect.Search.Strategies.CategoriesIntervalAsynchronousStrategy, Sitecore.Commerce.Engine.Connect">
                        <Environments hint="list">
                            <environment patch:instead="environment">MyAuthoring</environment>
                        </Environments>
                    </categoriesIntervalAsynchronousStrategyMaster>
                    <categoriesIntervalAsynchronousStrategyWeb type="Sitecore.Commerce.Engine.Connect.Search.Strategies.CategoriesIntervalAsynchronousStrategy, Sitecore.Commerce.Engine.Connect">
                        <Environments hint="list">
                            <environment patch:instead="environment">MyAuthoring</environment>
                        </Environments>
                    </categoriesIntervalAsynchronousStrategyWeb>
                    <catalogsIntervalAsynchronousStrategyMaster type="Sitecore.Commerce.Engine.Connect.Search.Strategies.CatalogsIntervalAsynchronousStrategy, Sitecore.Commerce.Engine.Connect">
                        <Environments hint="list">
                            <environment patch:instead="environment">MyAuthoring</environment>
                        </Environments>
                    </catalogsIntervalAsynchronousStrategyMaster>
                    <catalogsIntervalAsynchronousStrategyWeb type="Sitecore.Commerce.Engine.Connect.Search.Strategies.CatalogsIntervalAsynchronousStrategy, Sitecore.Commerce.Engine.Connect">
                        <Environments hint="list">
                            <environment patch:instead="environment">MyAuthoring</environment>
                        </Environments>
                    </catalogsIntervalAsynchronousStrategyWeb>
                </indexUpdateStrategies>
            </indexConfigurations>
            <configuration type="Sitecore.ContentSearch.ContentSearchConfiguration, Sitecore.ContentSearch">
                <indexes hint="list:AddIndex">
                    <index id="sitecore_master_index">
                        <locations hint="list:AddCrawler">
                            <crawler type="Sitecore.Commerce.Engine.Connect.Search.Crawlers.SellableItemsCrawler, Sitecore.Commerce.Engine.Connect">
                                <Environments hint="list">
                                    <environment>MyAuthoring</environment>
                                </Environments>
                            </crawler>
                            <crawler type="Sitecore.Commerce.Engine.Connect.Search.Crawlers.CategoriesCrawler, Sitecore.Commerce.Engine.Connect">
                                <Environments hint="list">
                                    <environment>MyAuthoring</environment>
                                </Environments>
                            </crawler>
                            <crawler type="Sitecore.Commerce.Engine.Connect.Search.Crawlers.CatalogsCrawler, Sitecore.Commerce.Engine.Connect">
                                <Environments hint="list">
                                    <environment>MyAuthoring</environment>
                                </Environments>
                            </crawler>
                        </locations>
                    </index>
                    <index id="sitecore_web_index">
                        <locations hint="list:AddCrawler">
                            <crawler type="Sitecore.Commerce.Engine.Connect.Search.Crawlers.SellableItemsCrawler, Sitecore.Commerce.Engine.Connect">
                                <Environments hint="list">
                                    <environment>MyAuthoring</environment>
                                </Environments>
                            </crawler>
                            <crawler type="Sitecore.Commerce.Engine.Connect.Search.Crawlers.CategoriesCrawler, Sitecore.Commerce.Engine.Connect">
                                <Environments hint="list">
                                    <environment>MyAuthoring</environment>
                                </Environments>
                            </crawler>
                            <crawler type="Sitecore.Commerce.Engine.Connect.Search.Crawlers.CatalogsCrawler, Sitecore.Commerce.Engine.Connect">
                                <Environments hint="list">
                                    <environment>MyAuthoring</environment>
                                </Environments>
                            </crawler>
                        </locations>
                    </index>
                </indexes>
            </configuration>
        </contentSearch>
    </sitecore>
</configuration>

Sitecore - Failed to create counter

Looking into the logs of my local Sitecore 9 instance, there were a number of warning related to performance counters:
ManagedPoolThread #0 17:13:08 WARN  Failed to create counter 'Sitecore.System\Events | Events Raised / sec'. Sitecore has no necessary permissions for reading/creating counters. Message: Access to the registry key 'Global' is denied.
ManagedPoolThread #1 17:13:08 WARN  Failed to create counter 'Sitecore.System\IO | File Watcher Events / sec'. Sitecore has no necessary permissions for reading/creating counters.Message: Access to the registry key 'Global' is denied.
To resolve this error, we need to add the application pool identity into two groups on your machine. This can be achieved by:
  1. From the start menu, open run and enter 'lusrmgr.msc'
  2. The two groups we are interested in are:
    1. Performance Log Users
    2. Performance Monitor Users
  3. Right click each group and select 'Add to group'
  4. Add the app pool identity of the Sitecore website and xConnect website. In my case these were: 'IIS APPPOOL\prefix.sc' and 'IIS APPPOOL\prefix.xconnect' where prefix is what was set in the Sitecore install PowerShell script.
  5. Restarting IIS via the start menu command (IISRESET) should now allow Sitecore to work correctly with performance counters.

Thursday, April 12, 2018

Sitecore Experience Commerce - Catalog Item of the Storefront catalog Configuration cannot be found

After performing a deployment to a local Sitecore Experience Commerce instance, the Storefront site was not loading and instead showed the following error:
Catalog Item of the Storefront catalog Configuration cannot be found
Catalog Item of the Storefront catalog Configuration cannot be found
The Sitecore back-end was still loading as expected. What caused this error was that at startup of the Sitecore site, the commerce engine services were not started. This caused Sitecore to not be able to load/cache the commerce data. To resolve this error, ensure the commerce engine services are started and restart the IIS web site for the Sitecore instance

Tuesday, April 10, 2018

Sitecore Experience Commerce - Shop CommerceEngineDefaultStorefront does not exist

On a local instance of Sitecore Experience Commerce, the following error was appearing inside the commerce business tools and the front-end Storefront website.
"@odata.context":"https://localhost:5000/Api/$metadata#Sitecore.Commerce.Core.CommandMessage","MessageDate":"2018-04 10T10:03:59.2415572Z","Code":"Error","Text":"Shop 'CommerceEngineDefaultStorefront' does not exist.","CommerceTermKey":"InvalidShop"
Sitecore Experience Commerce - CommerceEngineDefaultStorefront does not exist
This error traces back to the Sitecore.Commerce.Engine.Connect.config configuration file (available in App_Config\Include\Y.Commerce.Engine). The defaultShopName setting should match that of a valid Storefront setting node (available under /sitecore/Commerce/Commerce Control Panel/Storefront Settings/Storefronts).

Sitecore - XConnect XdbCollectionUnavailableException error

Inside the logs of a Sitecore 9 instance the following error was appearing inside the website logs:
ERROR Exception when executing agent aggregation/pathAnalyzerLiveAgentException: Sitecore.XConnect.XdbCollectionUnavailableExceptionMessage: The HTTP response was not successful: ServiceUnavailableSource: Sitecore.Xdb.Common.Web  at Sitecore.Xdb.Common.Web.Synchronous.SynchronousExtensions.SuspendContextLock[TResult](Func`1 taskFactory) at Sitecore.XConnect.Client.XConnectSynchronousExtensions.SuspendContextLock(Func`1 taskFactory) at Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.Initialize(XmlNode configNode)   at Sitecore.Configuration.DefaultFactory.CreateObject(XmlNode configNode, String[] parameters, Boolean assert, IFactoryHelper helper)   at Sitecore.Configuration.DefaultFactory.CreateObject(XmlNode configNode, String[] parameters, Boolean assert)   at Sitecore.Configuration.DefaultFactory.CreateObject(String configPath, String[] parameters, Boolean assert)   at Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient(String clientConfigPath)   at Sitecore.PathAnalyzer.Processing.Agents.TreeAggregatorAgent.Execute()   at Sitecore.Analytics.Core.BackgroundService.Run()
Inside the ConnectionStrings.config there were 4 settings which contained a thumbprint (FindByThumbprint) for the xConnect site:
  1. xconnect.collection.certificate
  2. xdb.referencedata.client.certificate
  3. xdb.marketingautomation.reporting.client.certificate
  4. xdb.marketingautomation.operations.client.certificate
For some reason this was incorrectly set, this should match the thumbprint of your xconnect client certificate. This is likely to be name.xconnect_client where name is that of your Sitecore site set in the install PowerShell script. This same thumbprint is also set in the configuration of the xconnect site.

Monday, April 9, 2018

Sitecore - Indexing manager shows no indexes

After installing an update package on a local instance of Sitecore 9 with Experience Commerce. The indexing manager on the Sitecore control panel would appear empty with no indexes listed to rebuild (or view). The logs were showing the following message:
[Content Testing]: Failed to find testing index
It came down to two issues with the configuration:

  1. App_Config\Sitecore\ContentSearch\Sitecore.ContentSearch.config needed to have the ContentSearch.Enabled setting set to true.
  2. The App_Config\Sitecore\Marketing.Operations.xMgmt\Sitecore.Marketing.Search.config file needed to be enabled.
The fact that an installation did not complete, might be a cause of this issue and should be investigated. However in my case, it had previously installed correctly and I was only setting some items back to the default.

Sitecore Experience Commerce - Storefront navigation: No catalog data available

After connecting my Sitecore Experience Commerce Storefront to a custom catalog, the navigation menu no longer showed any categories and instead displayed the message:
[No catalog data available]
Sitecore Experience Commerce - no catalog data available
Product detail pages would display the following error:
No catalog data configured and available!
Sitecore Experience Commerce - No catalog data configured and available

The first steps are to ensure the catalog has been configured correctly in Sitecore. The places to check are:
  1. The catalogs item: /sitecore/Commerce/Catalog Management/Catalogs - should correctly have enabled (box checked) the catalog you want available in Sitecore and cached.
  2. Storefront catalog configuration item: /sitecore/Commerce/Commerce Control Panel/Storefront Settings/Storefronts/MyStorefront/Catalog Configuration - This should have the correct catalog configured and in particular Start Navigation Category should also be pointing to a category (such as Departments in the Habitat example).
  3. Site home item: /sitecore/content/Sitecore/NW/Home - should point to the correct Storefront configuration on the Control Panel Configuration field.
  4. Product catalog item for the site: /sitecore/content/Sitecore/MySite/Home/Product Catalog - should correctly point to the same source as Start Navigation Category in point 2.

Once all of these settings appear correct, perform a full publish to ensure all data is present in the web database. In my case by rebuilding the web and master indexes (Sitecore and not commerce), the navigation begun to work as expected. I was able to debug this by checking if my Start Navigation Category item was present in the SOLR index.

This can be achieved by querying the SOLR index with the following query:
sitecoreid_t:5fe593d1-5993-73f9-80d9-97bb2157fb10
Where you replace the GUID with that of the item of your starting navigation category. To check if any categories are actually indexed at all, the following query will suffice:
commercesearchitemtype_t:"Category"
If these queries return no data in the web or master Sitecore indexes, the indexer may not correctly be configured to the right commerce environment.

Tuesday, April 3, 2018

Sitecore 9 install error - You must remove all users with password before setting the containment property to NONE

When attempting to install a Sitecore 9 (update 1) instance the PowerShell installation script failed with the following error:
Install-SitecoreConfiguration : Command C:\Program Files\iis\Microsoft Web Deploy V3\msdeploy.exe returned a non-zero exit code - (-1)At C:\Install\Sitecore9.1\Install.ps1:42 char:1+ Install-SitecoreConfiguration @xconnectParams+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Install-SitecoreConfiguration
Further up in the window it was traced back to the following source error:
Error: .Net SqlClient Data Provider: Msg 12809, Level 16, State 1, Line 5 You must remove all users with password before setting the containment property to NONE.
My deploy script had failed on a previous run (due to invalid SOLR base URL), however databases had been added to SQL. By removing the following 4 databases from SQL, the error above would no longer appear and Sitecore would install as expected.
  1. [prefix]_MarketingAutomation
  2. [prefix]_Messaging
  3. [prefix]_Processing.Pools
  4. [prefix]_ReferenceData
Where [prefix] is as configured in your installation script.

Thursday, March 29, 2018

Sitecore Experience Commerce - Storefront configuration missing.

When loading up the frontend site of a Sitecore Experience Commerce storefront, the following error may appear:
Storefront configuration missing.
Sitecore Experience Commerce - Storefront configuration missing
The logs were showing:
ERROR Application error.Exception: System.InvalidOperationExceptionMessage: Storefront configuration missing.Source: Sitecore.Commerce.XA.Foundation.CommonSitecore.Commerce.XA.Foundation.Common.Models.CommerceStorefront.get_StorefrontConfiguration()Sitecore.Commerce.XA.Foundation.Common.Pipelines.SecuredPageProcessor.IsSslEnforcedForStorefront()Sitecore.Commerce.XA.Foundation.Common.Pipelines.SecuredPageProcessor.Process(PipelineArgs args)
In particular the IsSslEnforced part of the error stood out and looking at my Storefront configuration, this was somehow missing. Comparing with the branch which should be inserted (/sitecore/templates/Branches/Foundation/Commerce Experience Accelerator/Commerce Foundation/Storefront Control Panel) several other configuration items were missing as well.

Inserting a new storefront configuration of this branch time and setting the relevant settings then allowed the frontend web site to load as expected.

Sitecore Experience Commerce - Error on Frontend - Control Panel Storefront

When attempting to load the front end of an Experience Commerce Storefront the following error was appearing:
Server Error in '/' Application.
Control Panel Storefront
Sitecore Experience Commerce - Control Panel Storefront error
It can be fixed by simply going to the Home node of your storefront site (/sitecore/content/Sitecore/MySite/Home) and ensuring the Control Panel Configuration setting is correctly pointing to a commerce control panel storefront settings.

Sitecore Experience Commerce - Update data templates fails

After upgrading from version 9.0 to 9.1 of Sitecore Experience Commerce, the update data templates command was failing.

Sitecore Experience Commerce - Update data templates error
The Sitecore logs were showing:
ERROR GetList(id='Catalogs',type='Sitecore.Commerce.Plugin.Catalog.Catalog, Sitecore.Commerce.Plugin.Catalog',skip=0,take=1)?$expand=Items($select=Id)
System.Exception: An error occured while trying to contact the Commerce Service. Error code BadRequest at Sitecore.Commerce.Engine.Connect.DataProvider.CatalogRepository.LogResponseError(HttpResponseMessage response, Boolean raiseError)
   at Sitecore.Commerce.Engine.Connect.DataProvider.CatalogRepository.<InvokeHttpClientGetAsync>d__24.MoveNext()
Looking into the logs for the Authoring role of commerce engine, there were more detailed logs:
ERROR Management.block.getitembypath: Sitecore Item Service Get item failed, Item /sitecore/Commerce/Commerce Control Panel/Storefront Settings/Storefronts/MyShop not found.54 13:23:40 ERROR CtxMsg.Error.InvalidShop: Text=Shop 'MyShop' does not exist.54 13:23:40 ERROR PipelineAbort:Shop 'MyShop' does not exist.54 13:23:40 ERROR CtxMsg.Error.InvalidShop: Text=Shop 'MyShop' does not exist.49 13:23:40 ERROR CommerceController.OnActionExecuting.BadRequest: Code=Error|TermKey=InvalidShop|Text=Shop 'MyShop' does not exist.
Two changes was able to get this resolved for me:
  • If you have renamed the default instance of the Storefront settings (/sitecore/Commerce/Commerce Control Panel/Storefront Settings/Storefronts\Storefront) this change may have been overwritten during the upgrade.
  • Ensure that the PlugIn.Content.PolicySet-1.0.0.json file inside each commerce engine site has the correct Host URL that points to your Sitecore instance.
These changes should get the data templates updating as expected. Don't forget to bootstrap if you change the PolicySet inside the commerce engine sites.

Sitecore Experience Commerce - One or more local models conflict with the xDB service layer

During an upgrade from the initial release for Sitecore Experience Commerce 9.0 to update 1, the following errors were appearing in the logs:
ERROR Exception when executing agent aggregation/pathAnalyzerLiveAgent
Exception: Sitecore.XConnect.Client.XdbModelConflictException
Message: One or more local models conflict with the xDB service layer.
 'RegisterConnectEventModel, 0.1' does not match the remote version
Source: Sitecore.Xdb.Common.Web
This was appearing when attempting to update the commerce data templates. It was not actually an error related to this action and is resolved when following these steps in the upgrade guide:

  1. In File Explorer, go to: \inetpub\wwwroot\<siteName>\App_Config\Include\Y.Commerce.Engine and Rename/enable:
    1. Sitecore.Commerce.Engine.DataProvider.config
    2. Sitecore.Commerce.Engine.Connectors.Index.Common.config
    3. All Sitecore.Commerce.Engine.Connectors.Index.Solr.*.config
  2. Copy the file: \inetpub\wwwroot\<siteName>\XConnectModels\Sitecore.Commerce.Connect.XConnect.Models.json and Paste it to two locations to overwrite:
    1. C:\inetpub\wwwroot\<siteName>.xconnect\App_data\jobs\continuous\IndexWorker\App_data\Models
    2. C:\inetpub\wwwroot\<siteName>.xconnect\App_data\Models
  3. Perform an IISreset

Tuesday, March 13, 2018

Sitecore Experience Commerce - Error: catalog name X is already in use

During testing of a Sitecore Experience Commerce minion which manipulated catalog and sellable item entities, the following error occurred when attempting to run the ICreateCatalogPipeline pipeline.
ERROR PipelineAbort:Catalog name MyCatalog is already in use.
In between minion runs I had been running the following DevOps calls in postman to ensure a fresh start:

  1.  {{OpsApiHost}}/{{OpsApi}}/CleanEnvironment() - Cleans the environment of all entities
  2. {{OpsApiHost}}/{{OpsApi}}/Bootstrap() - Bootstraps commerce, if this is not run you may not be able to add categories
  3. {{OpsApiHost}}/{{OpsApi}}/InitializeEnvironment(environment='MyEnvironment') - as above
However the error was still appearing, even though the catalog no longer existed. To solve this error one of the following will usually work:
  1. Restart IIS to clear any caches
  2. Run the cache clear using the devops postman call {{ServiceHost}}/{{OpsApi}}/ClearCacheStore()
Doing these, stopped the error and my catalog would successfully be created again.

Sitecore Experience Commerce - Error: PopulateItemAvailabilityBlock.AllocationNull

When creating a minion that handled data sync into Sitecore Experience Commerce, the following error occurred when using the IAssociateSellableItemToCatalog pipeline to associate a sellable item with a given category (inside a catalog).
ERROR PopulateItemAvailabilityBlock.AllocationNull.CatalogName|ProductId|
From what I could see, the cause of this error was attempting to add a sellable item to a catalog when no pricing/inventory information is available for it. Doing one of the following to/for the product before attempting to associate with a catalog should fix the error:

  1. Tag the sellable item and a price card with the same tag - worked for me
  2. Add a list price to the sellable item
Let me know in the comments down below, if either of these don't solve your issue. Also ensure that the entity has been updated correctly after making changes (IPersistEntityPipeline pipeline can help with this).

Sitecore Experience Commerce - Error: PipelineAbort:Invalid currency

During minion development in Sitecore Experience Commerce, the following error occurred when running the IAddPriceCardPipeline pipeline.
2 00:10:13 ERROR Pipeline completed with error
System.Exception: Error processing block: Management.block.GetCurrencySetById ---> System.ArgumentNullException: Management.block.GetCurrencySetById: The currency set Id cannot be null or empty.
42 00:10:13 WARN CtxMsg.ValidationError.InvalidCurrency: Text=Invalid currency 'NZD'. Valid currencies are ''.|Shopper=[None]|Shop=|Correlation=d68174cf-9976-410d-b15b-0ee1de8ba39c
42 00:10:13 ERROR PipelineAbort:Invalid currency 'NZD'. Valid currencies are ''.
This error traces back to the fact that I was incorrectly passing in the PriceBook parameter. There is no pipeline to get you a price book by name, however the following example will get you a PriceBook entity by name (if it exists). This makes use of the IFindEntitiesInListPipeline pipeline.
var books = (await _findEntitiesPipeline.Run(new FindEntitiesInListArgument(typeof(PriceBook), string.Format(context.GetPolicy<KnownPricingViewsPolicy>().PriceBooks, "BookName"), 0, int.MaxValue), context)).List;
var book = (PriceBook)books.Items.FirstOrDefault();
Hopefully this snippet is able to save someone some time!

Friday, March 9, 2018

Sitecore Experience Commerce - Adding a new empty environment

For a detailed explanation of Commerce roles and how they tie in with environments, please see my dedicated post. This gives an overview of environments and I recommend reading before continuing with this post.

This example shows how to add a new environement with empty data, if you want to initialize it with data (like the Habitat example) then see my post: Adding a new environment initialized with data.

Define the environment

The first piece of the puzzle is a JSON configuration file where the environment/plugin is defined. These files are placed it src\Sitecore.Commerce.Engine\wwwroot\data\Environments, and in my case I duplicated the PlugIn.Habitat.CommerceShops-1.0.0.json example. It's too big to post here, but the areas I changed were:
  • ArtifactStoreId - to a new GUID - this would be the same for a given set of environments (authoring, shops and minions)
  • Id - to a new ID (the efe49ff743d34b649fffcof293615cd9one)
  • Id - to a new name (the Entity-CommerceEnvironment-HabitatShops one)
  • Name - a new environment name
  • InitialArtifactSets - as follows
InitialArtifactSets for environment

Add the environment to global

To have the environment appear in the business tools, open up the Global.json file that comes inside the bootstrap folder of the Sitecore.Commerce.Engine project. There will be a GlobalEnvironment section which contains the values of the environments to appear in business tools. Here you can add the new environment name. 

Sitecore Experience Commerce - Environment for business tools

Initialize the environment

Now I just publish the Sitecore.Commerce.Engine project to the four commerce sites (ensuring thumbprints for certificates are setup correctly). Of course don't forget that each of these needs their environment set in the config file.

Inside postman, create a new environment to match the one you just created:

New postman environment
The run the following service calls from Postman:
  1. GetToken - under authentication
  2. Bootstrap Sitecore Commerce - under SitecoreCommerce_DevOps
  3. Initialize Environment - under SitecoreCommerce_DevOps
If any of these calls failed (due to 500 error), you will need to take a look in the Commerce Ops log files. If all went smoothly then your new environment is available in the Commerce Business Tools interface!

new environment in Experience Commerce
For me, the only error I got was around some dodgy JSON in the environment/plugin configuration. I had changed the currency elements to NZD and had missed a single USD instance. 

Sitecore Experience Commerce - Commerce roles and how they tie in with environments

As part of the installation of Sitecore Experience Commerce, 4 commerce websites are added to the IIS manager, each of which performs a unique engine role. Note that in some cases users will scale this down to a single instance locally for development and the full 4 in production. These 4 roles are:
  1. Authoring - handles traffic from the commerce business tools.
  2. Shops - serves traffic from a storefront.
  3. Minions - runs independently and runs your minion(s).
  4. DevOps - internal and will have higher permissions to handle environment bootstrapping etc.
Sitecore Experience Commerce roles
These 4 engine roles actually have the same code deployed - via the Sitecore.Commerce.Engine project which contains references to any plugins you create as well as defined environments. 

Where they differ is in which commerce environment is defined against a given role. To quote the Sitecore documentation:
Each deployed role can have different policies and behaviors, which can be specified using a specific Commerce Environment for that role. When a call is made to the Commerce Engine, the call’s header specifies an environment. This environment is used by the engine to control policies and behavior for that call. Specifying a particular environment allows explicit independent control (per role) of functions, such as caching, data storage, integration connections, and so on. 
Using environment policies, engine roles can either share a common persistent store, or use separate dedicated storage. All roles share the same storage in the out-of-box implementation. This allows artifacts to be visible to each role without a publishing process. For example, this makes orders immediately available to the Authoring role, and makes approved promotions and pricing changes immediately available to the Shops role. 
Each role can have independent caching policies. For example, reduced caching can be configured on the Authoring role so that changes are seen right away, and heavier caching can be configured on the Shops role to increase performance.
So what exactly is an environment, again lets take a look at the documentation:
Commerce environments provide the ability to have separately configurable pools of data and service functionality that run together in a single-service instance. Environments can share the same persistent store as other environments or be separated into their own exclusive persistent store.
Basically an environment can be used to group configurations for a given function and can share as much as needed with other related/unrelated environments.

To give a better illustration, lets take a look at what comes out of the box with Experience Commerce and Habitat. Inside the default Sitecore.Commerce.Engine project (comes as part of the SDK and is what is deployed to these roles) is an Environments folder which contains a number of JSON configuration files. The files that are relevant for this discussions are:
  1. PlugIn.Habitat.CommerceAuthoring-1.0.0.json - Name: HabitatAuthoring
  2. PlugIn.Habitat.CommerceMinions-1.0.0.json - Name: HabitatMinions
  3. PlugIn.Habitat.CommerceShops-1.0.0.json  - Name: HabitatShops
Take note of the name of each of these environment (as defined in the JSON itself). Looking into the contents of these files you may notice that shops and authoring are quite similar and the main differences are a couple of extra policies and plugins on the authoring environment. They actually share the same store/context so this makes sense as we don't want customer migration processing to happen on the instance accessed by the storefront. The minions environment definition is quite different and of course has a heap of minions defined. 

Now if we go into each of the commerce role websites (mentioned at the start of this post) they each will contain a config.json file in the wwwroot folder. The EnvironmentName setting in these configuration files is the only difference at deployment time (remember the same project gets deployed to all 4). 
  1. Authoring - HabitatAuthoring
  2. Shops - HabitatShops
  3. Minions - HabitatMinions
  4. DevOps - AdventureWorksOpsApi
As you may have noticed, these 4 roles each refer back to one of the habitat environments defined above. This is how each role knows what it's purpose is and it ties back to the environment. In this out of the box implemtation they all work together: All roles share the same storage in the out-of-box implementation.

Note: the AdventureWorksOpsApi environment does not appear to have been defined or shipped with the version of the SDK that I used. In this case it's fine to also set the role of this to the HabitatAuthoring or to remove this and point to the Authoring role.

There are two final pieces to this puzzle:

How does an environment appear in Experience Commerce Business Toosl?
Inside the Sitecore.Commerce.Engine project is a Global.Json file, this contains a GlobalEnvironment section which is where the values of environments you wish to appear in the business tools will be listed. Generally this would just be the authoring and shops environments (as is out of the box).

How do we define which environment is used inside Sitecore itself?
The full answer is available in my post: Sitecore Experience Commerce - Catalogs in the Sitecore Content Editor. The short answer is that the Sitecore.Commerce.Engine.Connect.config configuration file has a defaultEnvironment setting where we name the environment.

Hopefully this blog post has provided some clarity about what the commerce roles are in Experience Commerce and how they relate back to environments. 

Sitecore Experience Commerce - Creating a minion

Minions in Sitecore Experience Commerce can be thought of as a scheduled task. Many of the example minions that come with the Habitat commerce solution will access and watch a list of data from commerce. For example the ReleasedOrdersMinion watches the ReleasedOrders list. Other more general minions may not watch a list at all, such as the NodeHeartBeatMinion for example.

In my implementation I needed a process which would run in the background to watch an external system for product changes and then import them into Sitecore Experience Commerce. I decided to create a minion for this task (as it needed to run on a schedule and I wanted to have commerce context). This minion would not be watching a list, as one was not relevant to this case.

The full source code for the Sitecore Experience Commerce minion is available on GitHub. Also included in this example is a block and pipeline example.
The key part of this project is the minion defintion itself, this is what is configured in the minions environment and what is run on the schedule:
namespace Plugin.MyProject.Import
{
    public class ImportMinion : Minion
    {
        protected IImportMinionMinionPipeline MinionPipeline { get; set; }

        public override void Initialize(IServiceProvider serviceProvider, ILogger logger, MinionPolicy policy, CommerceEnvironment environment, CommerceContext globalContext)
        {
            base.Initialize(serviceProvider, logger, policy, environment, globalContext);
            MinionPipeline = serviceProvider.GetService<IImportMinionMinionPipeline>();
        }

        public override async Task<MinionRunResultsModel> Run()
        {
            this.Logger.LogInformation("ImportMinion running");

            var commerceContext = new CommerceContext(this.Logger, this.MinionContext.TelemetryClient, null);
            commerceContext.Environment = this.Environment;

            CommercePipelineExecutionContextOptions executionContextOptions = new CommercePipelineExecutionContextOptions(commerceContext, null, null, null, null, null);

            MinionRunResultsModel res = await this.MinionPipeline.Run(new MinionRunResultsModel(), executionContextOptions);

            return new MinionRunResultsModel();
        }
    }
}
To actually get this minion to run it needs to be added to the environment definition for your minions environment. In the Sitecore.Commerce.Engine project is the JSON file PlugIn.Habitat.CommerceMinions-1.0.0.json which is the definition for the habitat minions environment. This is the active environment on the commerce minions web site (one of 4 in IIS installed as part of commerce).

Inside this file, you can see some example minions have been added:

Sitecore Experience Commerce Minions environment definition

Here we can inject in the definition of our custom minion:
{
    "$type": "Sitecore.Commerce.Core.MinionPolicy, Sitecore.Commerce.Core",
    "WakeupInterval": "00:05:00",
    "ListToWatch": "",
    "FullyQualifiedName": "Plugin.MyProject.Import.ImportMinion, Plugin.MyProject.Import",
    "ItemsPerBatch": 10,
    "SleepBetweenBatches": 500
}
Notice that there is no ListToWatch and that the minion is set to run every 5 minutes. The type of Sitecore.Commerce.Core.MinionPolicy is important as this is what defines this as a minion definition and ensures that it will be started during environment initialization.

Now you simply deploy the Sitecore.Commerce.Engine project to your commerce minions IIS website (ensuring that the correct EnvironmentName is set the the config.json file. If you check the logs for that IIS website, it should show the minion running every 5 minutes:
7 11:33:02 INFO ImportMinion running
If all else fails, try a Bootstrap Sitecore Commerce call in Postman as this tends to fix most commerce errors.

Sitecore Experience Commerce - Plugin to extend a price card

As part of a Sitecore Experience Commerce implementation, I had requirements that involved putting some additional data (text) on price cards. The code for this plugin involves some minor changes to the SellableItem plugin example provided by Sitecore.

The full plugin code to extend a PriceCard in Experience Commerce is available on GitHub. The key file differecnes to target PriceCard instead of SellableItem are as follows:

Configure Sitecore
public void ConfigureServices(IServiceCollection services)
{
    var assembly = Assembly.GetExecutingAssembly();
    services.RegisterAllPipelineBlocks(assembly);

    services.Sitecore().Pipelines(config =>
        config
            .ConfigurePipeline<IGetEntityViewPipeline>(c =>
            {
                c.Add<GetPriceDetailViewBlock>().After<GetPriceCardDetailsViewBlock>();
            })
            .ConfigurePipeline<IPopulateEntityViewActionsPipeline>(c =>
            {
                c.Add<PopulatePriceDetailActionsBlock>().After<InitializeEntityViewActionsBlock>();
            })
            .ConfigurePipeline<IDoActionPipeline>(c =>
            {
                c.Add<DoActionEditPriceDetail>().After<ValidateEntityVersionBlock>();
            })
    );
}
The view block is added after GetPriceCardDetailsViewBlock in this case instead of GetSellableItemDetailsViewBlock with a SellableItem.

Get View Block
[PipelineDisplayName(Constants.Pipelines.Blocks.GetPriceDetailViewBlock)]
public class GetPriceDetailViewBlock : PipelineBlock<EntityView, EntityView, CommercePipelineExecutionContext>
{
private readonly ViewCommander _viewCommander;

public GetPriceDetailViewBlock(ViewCommander viewCommander)
{
    this._viewCommander = viewCommander;
}

public override Task<EntityView> Run(EntityView arg, CommercePipelineExecutionContext context)
{
    Condition.Requires(arg).IsNotNull($"{Name}: The argument cannot be null.");

    var request = this._viewCommander.CurrentEntityViewArgument(context.CommerceContext);

    var pricingViewsPolicy = context.GetPolicy<KnownPricingViewsPolicy>();

    var viewsPolicy = context.GetPolicy<KnownPriceDetailViewsPolicy>();
    var actionsPolicy = context.GetPolicy<KnownPriceDetailActionsPolicy>();

    // Make sure that we target the correct views
    if (string.IsNullOrEmpty(request.ViewName) ||
        !request.ViewName.Equals(pricingViewsPolicy.Master, StringComparison.OrdinalIgnoreCase) &&
        !request.ViewName.Equals(pricingViewsPolicy.Details, StringComparison.OrdinalIgnoreCase) &&
        !request.ViewName.Equals(viewsPolicy.PriceDetail, StringComparison.OrdinalIgnoreCase))
    {
        return Task.FromResult(arg);
    }

    // Only proceed if the current entity is a price card
    if (!(request.Entity is Sitecore.Commerce.Plugin.Pricing.PriceCard))
    {
        return Task.FromResult(arg);
    }

    var priceCard = (Sitecore.Commerce.Plugin.Pricing.PriceCard) request.Entity;

    var targetView = arg;

    // Check if the edit action was requested
    var isEditView = !string.IsNullOrEmpty(arg.Action) && arg.Action.Equals(actionsPolicy.EditPriceDetail, StringComparison.OrdinalIgnoreCase);
    if (!isEditView)
    {
        // Create a new view and add it to the current entity view.
        var view = new EntityView
        {
            Name = context.GetPolicy<KnownPriceDetailViewsPolicy>().PriceDetail,
            DisplayName = "Pricing Details",
            EntityId = arg.EntityId
        };

        arg.ChildViews.Add(view);

        targetView = view;
    }

    if (priceCard != null && (priceCard.HasComponent<PriceDetailComponent>() || isEditView))
    {
        var component = priceCard.GetComponent<PriceDetailComponent>();
        AddPropertiesToView(targetView, component, !isEditView);
    }

    return Task.FromResult(arg);
}
In this case the views policy changes from KnownCatalogViewsPolicy to KnownPricingViewsPolicy and of course we target the PriceCard entity instead of SellableItem.

It's not a lot to change when trying to extend various entities in Experience Commerce 9. It's worth noting that the types of which we do extend all appear to be of type CommerceEntity in the Sitecore DLLs.