Managing Stock(inventory) in multiple warehouses

OOTB Sitecore Commerce Engine(CE) allows you to have one inventory set for a catalog(products set). There might be situations when multiple warehouses would have the stock for a single product. In such a scenario you can easily extend CE to look for stock in multiple warehouses.

Availability

When a product is added to the cart, the storefront sends a request to CE to find out if the product is available in stock. CE comes with an API api/GetBulkStockInformation which calls a command GetBulkStockInformationCommand. This command runs IGetSellableItemPipeline to get a sellable item with the availability component.

 

InvenoryPipelines

 

OOTB CE has PopulateItemAvailabilityComponentBlock which I replaced with PopulateItemAvailabilityComponentFromMultipleWarehousesBlock. In ConfigureSitecore.cs, I added

.ConfigurePipeline<IPopulateItemAvailabilityComponentPipeline>(configure => configure
          .Replace<PopulateItemAvailabilityComponentBlock, PopulateItemAvailabilityComponentFromMultipleWarehousesBlock>())

 

While the block PopulateItemAvailabilityComponentFromMultipleWarehousesBlock is inheriting from PopulateItemAvailabilityComponentBlock. In spite of only checking the default inventory set, I am iterating over all the inventory sets to get the stock information.

Note: I am calling the inventory sets as warehouses in the code. While passing the names of warehouses in the context(request header) from storefront. This is not in the scope of this post but just to mention, our warehouses are system entities that are managed in XP.

I am sharing some highlights of the code from PopulateItemAvailabilityComponentFromMultipleWarehousesBlock as the actual implementation could differ from one business to the other.

    public class PopulateItemAvailabilityComponentFromMultipleWarehousesBlock : PipelineBlock<ItemAvailabilityComponent, ItemAvailabilityComponent, CommercePipelineExecutionContext>
    {
        private readonly IGetSellableItemPipeline _getSellableItemPipeline;

        private readonly GetInventoryInformationCommand _getInventoryInformationCommand;

        public PopulateItemAvailabilityComponentFromMultipleWarehousesBlock(IGetSellableItemPipeline getSellableItemPipeline, GetInventoryInformationCommand getInventoryInformationCommand)
            : base((string)null)
        {
            this._getSellableItemPipeline = getSellableItemPipeline;
            this._getInventoryInformationCommand = getInventoryInformationCommand;
        }

        public override async Task<ItemAvailabilityComponent> Run(ItemAvailabilityComponent arg, CommercePipelineExecutionContext context)
        {
            if (arg == null)
            {
                return null;
            }
            ProductArgument productArgument = ProductArgument.FromItemId(arg.ItemId);
            SellableItem sellableItem = context.CommerceContext.GetEntity((SellableItem x) => x.Id.Equals(CommerceEntity.IdPrefix<SellableItem>() + productArgument.ProductId, StringComparison.OrdinalIgnoreCase));
            if (sellableItem == null)
            {
                sellableItem = await this._getSellableItemPipeline.Run(productArgument, context).ConfigureAwait(false);
            }

            if (sellableItem == null)
            {
                context.Logger.LogError(base.Name + ".InventoryItemNotFound." + arg.ItemId, Array.Empty<object>());
                arg.IsAvailable = false;
                arg.GetComponent<MessagesComponent>().AddMessage("InventoryItemNotFound", "Inventory Item Not Found:" + arg.ItemId);
                return arg;
            }

            await this.PopulateAvailabilityComponent(arg, sellableItem, context).ConfigureAwait(false);
            return arg;
        }

        private string[] GetWarehouses(CommercePipelineExecutionContext context)
        {            
            if (context.CommerceContext.Headers.ContainsKey("Warehouses"))
            {
                string warehouses = context.CommerceContext.Headers["Warehouses"];
                return warehouses.Split('|');
            }

            return new string[0];
        }

        private async Task PopulateAvailabilityComponent(ItemAvailabilityComponent availability, SellableItem sellableItem, CommercePipelineExecutionContext context)
        {
            var warehouses = GetWarehouses(context);

            context.Logger.LogInformation($"Checking the stock in {warehouses.Length} warehouses");

            foreach (string warehouse in warehouses)
            {                
                string inventorySet = CommerceEntity.IdPrefix<InventorySet>() + warehouse; // Name of warehouse/inventory set to check the inventory in

                context.Logger.LogDebug($"Looking for stock in wahrehouse:  {inventorySet}");
                InventoryInformation inventoryInformation = await this._getInventoryInformationCommand.Process(context.CommerceContext, inventorySet, sellableItem.Id, null, false).ConfigureAwait(false);

                if (!InStock(inventoryInformation, context))
                {
                    context.Logger.LogWarning($"Product {sellableItem.ProductId} is low-stock in {warehouse} inventory set", this);
                    continue;
                }

                if (inventoryInformation == null)
                {
                    context.Logger.LogInformation(base.Name + ".AllocationNull." + availability.ItemId, Array.Empty<object>());
                }
                else
                {
                    availability.IsAvailable = true;
                    availability.Name = warehouse;
                    availability.AvailableQuantity = inventoryInformation.Quantity;
                    availability.AvailabilityExpires = context.CommerceContext.CurrentEffectiveDate().AddSeconds((double)context.GetPolicy<GlobalAvailabilityPolicy>().AvailabilityExpires);
                    break;
                }
            }
        }

        private bool InStock(InventoryInformation inventoryInformation, CommercePipelineExecutionContext context)
        {
            return inventoryInformation != null && inventoryInformation.Quantity >= quantity;
        }
    }

 

 

Leave a Reply