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.

1 comment:

  1. Very good post around debugging pipelines. Thanks Ryan

    ReplyDelete