using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Sitecore.Commerce.Core; using Sitecore.Commerce.Plugin.Availability; using Sitecore.Commerce.Plugin.Carts; using Sitecore.Commerce.Plugin.Catalog; using Sitecore.Commerce.Plugin.Inventory; using Sitecore.Framework.Conditions; using Sitecore.Framework.Pipelines; namespace Sitecore.Support.Sample { public class UpdateItemAvailabilityBlock : PipelineBlock { private readonly InventoryCommander _commander; public UpdateItemAvailabilityBlock(InventoryCommander commander) : base() { _commander = commander; } public override async Task Run(UpdateAvailabilityArgument arg, CommercePipelineExecutionContext context) { Condition.Requires(arg.ProductId).IsNotNull($"{Name}: The product id can not be null"); Condition.Requires(arg.CatalogName).IsNotNull($"{Name}: The catalog name can not be null"); var sellableItem = context.CommerceContext.GetEntity(x => x.ProductId.Equals(arg.ProductId, StringComparison.OrdinalIgnoreCase) && (!string.IsNullOrEmpty(arg.VariantId) && x.GetComponent().Variations.Any(v => v.Id.Equals(arg.VariantId, StringComparison.OrdinalIgnoreCase)))) ?? await _commander.Pipeline().Run(new ProductArgument(arg.CatalogName, arg.ProductId), context).ConfigureAwait(false); if (sellableItem == null) { context.Logger.LogError($"{Name}-SellableItemNotFound: ProductId={arg.ProductId}, VariantId={arg.VariantId}, CatalogId={arg.CatalogName}"); return false; } if (string.IsNullOrEmpty(arg.VariantId)) { // No Variants if (sellableItem.HasPolicy()) { // Product is always available and has no inventory return true; } } else { // Variants if (!sellableItem.HasComponent()) { context.Logger.LogError($"{Name}-ItemVariationsComponent Not Found: ProductId={arg.ProductId}, VariantId={arg.VariantId}, CatalogId={arg.CatalogName}"); return false; } var itemVariant = sellableItem.GetVariation(arg.VariantId); if (itemVariant == null) { context.Logger.LogError($"{Name}-ItemVariation Not Found: ProductId={arg.ProductId}, VariantId={arg.VariantId}, CatalogId={arg.CatalogName}"); } else { if (itemVariant.HasPolicy()) { // Product is always available and has no inventory return true; } } } var catalog = context.CommerceContext.GetEntity(x => x.Name.Equals(arg.CatalogName, StringComparison.OrdinalIgnoreCase)) ?? await _commander.Pipeline() .Run(new FindEntityArgument(typeof(Catalog), $"{CommerceEntity.IdPrefix()}{arg.CatalogName}"), context) .ConfigureAwait(false) as Catalog; if (catalog == null) { context.Logger.LogError($"{Name}-Could not find a valid catalog name for '{arg.CatalogName}{arg.ProductId}'"); return false; } var semaphoreKey = string.Concat(arg.CatalogName, "|", arg.ProductId, "|", arg.VariantId); var semaphore = _commander.CurrentNodeContext(context.CommerceContext).GetOrAddSemaphore(semaphoreKey, InventoryConstants.UpdateItemAvailabilityBlock); try { await semaphore.WaitAsync().ConfigureAwait(false); // Wait at this line. if (sellableItem.HasComponent()) { if (arg.Line == null) { context.Abort( await context.CommerceContext.AddMessage( context.GetPolicy().Error, "CartLineNotFound", new object[] { arg.AsItemId() }, $"Cart line {arg.AsItemId()} was not found.").ConfigureAwait(false), context); return false; } var result = true; var bundleComponent = sellableItem.GetComponent(); foreach (var subLine in arg.Line.CartSubLineComponents) { var productArgument = ProductArgument.FromItemId(subLine.ItemId); var bundleItem = bundleComponent.BundleItems.FirstOrDefault(x => { var parts = x.SellableItemId.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries); var productId = parts[0].SimplifyEntityName(); var variantId = string.Empty; if (parts.Length > 1) { variantId = parts[1]; } var productMatch = productArgument.ProductId.Equals(productId, StringComparison.OrdinalIgnoreCase); var variantMatch = string.IsNullOrEmpty(variantId) || !string.IsNullOrEmpty(variantId) && productArgument.VariantId.Equals(variantId, StringComparison.OrdinalIgnoreCase); return productMatch && variantMatch; }); if (bundleItem == null) { return false; } var bundleSellableItem = context.CommerceContext.GetEntity( x => x.ProductId.Equals(productArgument.ProductId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(productArgument.VariantId) && x.GetComponent().Variations.Any(v => v.Id.Equals(productArgument.VariantId, StringComparison.OrdinalIgnoreCase))) ?? await _commander.Pipeline().Run(productArgument, context.CommerceContext.PipelineContextOptions).ConfigureAwait(false); if (bundleSellableItem == null) { context.Abort( await context.CommerceContext.AddMessage( context.GetPolicy().Error, "EntityNotFound", new object[] { productArgument.AsItemId() }, $"Entity '{productArgument.AsItemId()}' was not found.").ConfigureAwait(false), context); return false; } if (bundleSellableItem.HasPolicy(productArgument.VariantId)) { // Product is always available and has no inventory continue; } // Run a retrier to avoid concurrency issue var updateInventoryResult = await UpdateInventoryInformationWithRetrier( arg.CatalogName, catalog.DefaultInventorySetName, bundleSellableItem.Id, productArgument.VariantId, Convert.ToInt32(arg.DeltaQuantity) * bundleItem.Quantity, context).ConfigureAwait(false); if (result) { result = updateInventoryResult; } } return result; } else { // Run a retrier to avoid concurrency issue return await UpdateInventoryInformationWithRetrier( arg.CatalogName, catalog.DefaultInventorySetName, sellableItem.Id, arg.VariantId, Convert.ToInt32(arg.DeltaQuantity), context).ConfigureAwait(false); } } finally { semaphore.Release(); } } private async Task UpdateInventoryInformationWithRetrier(string catalogName, string defaultInventorySetName, string sellableItemId, string variationId, int deltaQuantity, CommercePipelineExecutionContext context) { bool readyInventory = false; bool result = false; while (!readyInventory) { var currentContext = new CommerceContext(context.Logger, context.CommerceContext.TelemetryClient) { GlobalEnvironment = context.CommerceContext.GlobalEnvironment, Environment = context.CommerceContext.GlobalEnvironment, Headers = context.CommerceContext.Headers, PipelineTraceLoggingEnabled = context.CommerceContext.PipelineTraceLoggingEnabled }; result = await this.UpdateInventoryInformation(catalogName, defaultInventorySetName, sellableItemId, variationId, deltaQuantity, currentContext.PipelineContext).ConfigureAwait(false); var failureMessage = currentContext.GetMessages(m => (m.Text.Contains(sellableItemId) && (variationId != null ? m.Text.Contains(variationId) : true)) && m.Text.Contains("Concurrency")).FirstOrDefault(); if (failureMessage != null) { currentContext.ClearMessages(); continue; } readyInventory = true; } return result; } private async Task UpdateInventoryInformation(string catalogName, string defaultInventorySetName, string sellableItemId, string variationId, int deltaQuantity, CommercePipelineExecutionContext context) { string productId = sellableItemId.SimplifyEntityName(); InventoryInformation inventoryInformation = await _commander.GetInventoryInformation(context.CommerceContext, new SellableItemInventorySetArgument(sellableItemId, CommerceEntity.IdPrefix() + defaultInventorySetName) { VariationId = variationId }).ConfigureAwait(continueOnCapturedContext: false); CommercePipelineExecutionContext commercePipelineExecutionContext; if (inventoryInformation != null) { int num = inventoryInformation.Quantity - deltaQuantity; bool flag = inventoryInformation.HasComponent() && inventoryInformation.GetComponent().Backorderable; PreorderableComponent preorderableComponent = (inventoryInformation.HasComponent() ? inventoryInformation.GetComponent() : null); bool flag2 = false; if (preorderableComponent != null && preorderableComponent.Preorderable && preorderableComponent.PreorderAvailabilityDate.HasValue) { flag2 = DateTimeOffset.Compare(context.CommerceContext.CurrentEffectiveDate(), preorderableComponent.PreorderAvailabilityDate.Value) < 0; } if (num < 0 && !flag && !flag2) { commercePipelineExecutionContext = context; commercePipelineExecutionContext.Abort(await context.CommerceContext.AddMessage(context.GetPolicy().Error, "ItemOversell", new object[3] { deltaQuantity, inventoryInformation.Quantity, catalogName + "|" + productId + "|" + variationId }, $"{deltaQuantity} exceeds the available quantity of {inventoryInformation.Quantity} for {catalogName}|{productId}|{variationId}.").ConfigureAwait(continueOnCapturedContext: false), context); return false; } inventoryInformation.Quantity = num; await _commander.Pipeline().Run(new PersistEntityArgument(inventoryInformation), context).ConfigureAwait(continueOnCapturedContext: false); return true; } commercePipelineExecutionContext = context; commercePipelineExecutionContext.Abort(await context.CommerceContext.AddMessage(context.GetPolicy().Error, "SellableItemNoInventory", new object[1] { catalogName + "|" + productId + "|" + variationId }, "No inventory associated to " + catalogName + "|" + productId + "|" + variationId + ".").ConfigureAwait(continueOnCapturedContext: false), context); return false; } } }