Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCC-229: Inventory part with non-restricted product attributes can't be added to the cart #421

Merged
merged 14 commits into from
Apr 14, 2024
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization;
using OrchardCore.Commerce.Inventory.Models;
using OrchardCore.Commerce.Inventory.ViewModels;
using OrchardCore.ContentManagement.Display.ContentDisplay;
Expand All @@ -13,9 +14,16 @@ namespace OrchardCore.Commerce.Inventory.Drivers;

public class InventoryPartDisplayDriver : ContentPartDisplayDriver<InventoryPart>
{
public const string NewProductKey = "DEFAULT";

private readonly IHttpContextAccessor _hca;
private readonly IStringLocalizer<InventoryPartDisplayDriver> T;

public InventoryPartDisplayDriver(IHttpContextAccessor hca) => _hca = hca;
public InventoryPartDisplayDriver(IHttpContextAccessor hca, IStringLocalizer<InventoryPartDisplayDriver> localizer)
{
_hca = hca;
T = localizer;
}

public override IDisplayResult Display(InventoryPart part, BuildPartDisplayContext context) =>
Initialize<InventoryPartViewModel>(GetDisplayShapeType(context), viewModel => BuildViewModel(viewModel, part))
Expand All @@ -31,37 +39,38 @@ public override async Task<IDisplayResult> UpdateAsync(
UpdatePartEditorContext context)
{
var viewModel = new InventoryPartViewModel();
if (await updater.TryUpdateModelAsync(viewModel, Prefix))
{
var currentSku = _hca.HttpContext?.Request.Form["ProductPart.Sku"].ToString().ToUpperInvariant();
var skuBefore = viewModel.Inventory.FirstOrDefault().Key != null
? viewModel.Inventory.FirstOrDefault().Key.Split('-')[0]
: "DEFAULT";

part.Inventory.Clear();
part.Inventory.AddRange(viewModel.Inventory);

// If SKU was changed, inventory keys need to be updated.
if (!string.IsNullOrEmpty(currentSku) && currentSku != skuBefore)
if (_hca.HttpContext?.Request.Form["ProductPart.Sku"].ToString().ToUpperInvariant() is not { } currentSku)
{
updater.ModelState.AddModelError("ProductPart.Sku", T["The Product SKU is missing."].Value);
}
else if (await updater.TryUpdateModelAsync(viewModel, Prefix))
{
// Workaround for accepting inventory values during content item creation where the SKU is not yet known.
if (viewModel.Inventory.TryGetValue(NewProductKey, out var defaultCount))
{
part.InventoryKeys.Clear();
viewModel.Inventory.Remove(NewProductKey);
viewModel.Inventory.Add(currentSku, defaultCount);
}

var newInventory = new Dictionary<string, int>();
var oldInventory = part.Inventory.ToDictionary(key => key.Key, value => value.Value);
foreach (var inventoryEntry in oldInventory)
{
var updatedKey = oldInventory.Count > 1
? currentSku + "-" + inventoryEntry.Key.Split('-')[^1]
: currentSku;
var skuBefore = viewModel.Inventory.FirstOrDefault().Key.Split('-')[0];

part.Inventory.Remove(inventoryEntry.Key);
newInventory.Add(updatedKey, inventoryEntry.Value);
part.Inventory.SetItems(viewModel.Inventory);

part.InventoryKeys.Add(updatedKey);
}
var skuChanged = !string.IsNullOrEmpty(currentSku) && (context.IsNew || currentSku != skuBefore);
if (skuChanged && part.Inventory.Count == 1 && !part.Inventory.Keys.Single().Contains('-'))
{
part.Inventory.SetItems([new KeyValuePair<string, int>(currentSku, part.Inventory.Values.Single())]);
part.InventoryKeys.SetItems([currentSku]);
}
else if (skuChanged)
{
var newInventory = part.Inventory.ToDictionary(
item => $"{currentSku}-{item.Key.Split('-', 2)[^1]}",
item => item.Value);

part.Inventory.Clear();
part.Inventory.AddRange(newInventory);
part.Inventory.SetItems(newInventory);
part.InventoryKeys.SetItems(newInventory.Keys);
}

part.ProductSku = currentSku;
Expand All @@ -74,13 +83,9 @@ public override async Task<IDisplayResult> UpdateAsync(
// new ones, hence the filtering below.
private static void BuildViewModel(InventoryPartViewModel model, InventoryPart part)
{
var inventory = part.Inventory ?? new Dictionary<string, int>();
if (inventory.Any())
{
// Workaround for InventoryPart storing the outdated inventory entries along with the updated ones.
var filteredInventory = part.Inventory.FilterOutdatedEntries(part.InventoryKeys);
model.Inventory.SetItems(part.FilterOutdatedEntries());

model.Inventory.AddRange(filteredInventory);
}
var sku = part.ContentItem?.Content.ProductPart?.Sku?.ToString() as string;
if (model.Inventory.Count == 0) model.Inventory.Add(sku ?? NewProductKey, 0);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using OrchardCore.Commerce.Inventory.Models;
using System.Collections.Generic;
using System.Linq;

Expand All @@ -6,8 +7,9 @@ namespace OrchardCore.Commerce.Inventory;
public static class InventoryDictionaryExtensions
{
public static IDictionary<string, int> FilterOutdatedEntries(
this IDictionary<string, int> inventory, IList<string> inventoryKeys) =>
inventory
.Where(inventory => inventoryKeys.Contains(inventory.Key))
this InventoryPart part) =>
part
.Inventory
.Where(inventory => part.InventoryKeys.Contains(inventory.Key))
.ToDictionary(key => key.Key, value => value.Value);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;

namespace OrchardCore.Commerce.Inventory.ViewModels;
Expand All @@ -7,7 +8,7 @@ public class InventoryPartViewModel
public bool AllowsBackOrder { get; set; }
public bool IgnoreInventory { get; set; }

public IDictionary<string, int> Inventory { get; } = new Dictionary<string, int>();
public IDictionary<string, int> Inventory { get; } = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

public int MaximumOrderQuantity { get; set; }
public int MinimumOrderQuantity { get; set; }
Expand Down
DemeSzabolcs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Drivers;
Expand Down Expand Up @@ -57,7 +56,7 @@ public override async Task<IDisplayResult> UpdateAsync(
{
part.CanBeBought.Clear();

var filteredInventory = inventoryPart.Inventory.FilterOutdatedEntries(inventoryPart.InventoryKeys);
var filteredInventory = inventoryPart.FilterOutdatedEntries();

// If an inventory's value is below 1 and back ordering is not allowed, corresponding
// CanBeBought entry needs to be set to false; should be set to true otherwise.
Expand Down Expand Up @@ -100,19 +99,11 @@ private async Task BuildViewModelAsync(ProductPartViewModel viewModel, ProductPa

if (part.ContentItem.As<InventoryPart>() is { } inventoryPart)
{
var filteredInventory = inventoryPart.Inventory.FilterOutdatedEntries(inventoryPart.InventoryKeys);
foreach (var inventory in filteredInventory)
foreach (var (key, value) in inventoryPart.FilterOutdatedEntries())
{
// If an inventory's value is below 1 and back ordering is not allowed, corresponding
// CanBeBought entry needs to be set to false; should be set to true otherwise.
viewModel.CanBeBought[inventory.Key] = inventoryPart.AllowsBackOrder.Value || inventory.Value >= 1;
}

// When creating a new Product item, initialize default inventory.
if (part.ContentItem.As<PriceVariantsPart>() == null && !inventoryPart.Inventory.Any())
{
inventoryPart.Inventory.Add("DEFAULT", 0);
inventoryPart.InventoryKeys.Add("DEFAULT");
viewModel.CanBeBought[key] = inventoryPart.AllowsBackOrder.Value || value >= 1;
}
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ public override async Task<LocalizedHtmlString> VerifyingItemAsync(ShoppingCartI
}

var title = productPart.ContentItem.DisplayText;
var fullSku = await _productService.GetOrderFullSkuAsync(item, productPart);
var fullSku = (await _productService.GetOrderFullSkuAsync(item, productPart))?.TrimEnd('-');

var inventoryIdentifier = string.IsNullOrEmpty(fullSku) ? productPart.Sku : fullSku;
var relevantInventory = inventoryPart.Inventory.FirstOrDefault(entry => entry.Key == inventoryIdentifier);
var relevantInventory = inventoryPart.Inventory.Count == 1
? inventoryPart.Inventory.Single()
: inventoryPart.Inventory.FirstOrDefault(entry => entry.Key == inventoryIdentifier);

// Item verification should fail if back ordering is not allowed and quantity exceeds available inventory.
if (!inventoryPart.AllowsBackOrder.Value && item.Quantity > relevantInventory.Value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ public ContentLocalizationProductService(
ISession session,
IContentManager contentManager,
IContentDefinitionManager contentDefinitionManager,
IPredefinedValuesProductAttributeService predefinedValuesService)
: base(session, contentManager, contentDefinitionManager, predefinedValuesService) =>
IPredefinedValuesProductAttributeService predefinedValuesService,
Lazy<IShoppingCartSerializer> shoppingCartSerializer)
: base(session, contentManager, contentDefinitionManager, predefinedValuesService, shoppingCartSerializer) =>
_siteService = siteService;

public override async Task<IEnumerable<ProductPart>> GetProductsAsync(IEnumerable<string> skus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<IList<ShoppingCartItem>> UpdateAsync(IList<ShoppingCartItem> m
foreach (var item in model)
{
var productPart = await _productService.GetProductAsync(item.ProductSku);
var fullSku = await _productService.GetOrderFullSkuAsync(item, productPart);
var fullSku = (await _productService.GetOrderFullSkuAsync(item, productPart)).TrimEnd('-');

await UpdateInventoryAsync(
await _productService.GetProductAsync(item.ProductSku),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ public class PriceVariantProvider : IPriceProvider
{
private readonly IProductService _productService;
private readonly IPredefinedValuesProductAttributeService _predefinedValuesService;
private readonly IShoppingCartSerializer _shoppingCartSerializer;

public int Order => 1;

public PriceVariantProvider(IProductService productService, IPredefinedValuesProductAttributeService predefinedValuesService)
public PriceVariantProvider(
IProductService productService,
IPredefinedValuesProductAttributeService predefinedValuesService,
IShoppingCartSerializer shoppingCartSerializer)
{
_productService = productService;
_predefinedValuesService = predefinedValuesService;
_shoppingCartSerializer = shoppingCartSerializer;
}

public async Task<IList<ShoppingCartItem>> UpdateAsync(IList<ShoppingCartItem> model)
Expand Down Expand Up @@ -59,6 +64,11 @@ private async Task<ShoppingCartItem> AddPriceToShoppingCartItemAsync(ShoppingCar
.Select(attr => attr.PartName + "." + attr.Name)
.ToHashSet();

if (item.HasRawAttributes())
{
item.Attributes.SetItems(await _shoppingCartSerializer.PostProcessAttributesAsync(item.Attributes, productPart));
}

var key = item.GetVariantKeyFromAttributes(attributesRestrictedToPredefinedValues);

if (string.IsNullOrEmpty(key)) return item.WithPrice(new PrioritizedPrice(1, variants.First().Value));
Expand Down
12 changes: 11 additions & 1 deletion src/Modules/OrchardCore.Commerce/Services/ProductService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ public class ProductService : IProductService
private readonly IContentManager _contentManager;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IPredefinedValuesProductAttributeService _predefinedValuesService;
private readonly Lazy<IShoppingCartSerializer> _shoppingCartSerializer;

public ProductService(
ISession session,
IContentManager contentManager,
IContentDefinitionManager contentDefinitionManager,
IPredefinedValuesProductAttributeService predefinedValuesService)
IPredefinedValuesProductAttributeService predefinedValuesService,
Lazy<IShoppingCartSerializer> shoppingCartSerializer)
{
_session = session;
_contentManager = contentManager;
_contentDefinitionManager = contentDefinitionManager;
_predefinedValuesService = predefinedValuesService;
_shoppingCartSerializer = shoppingCartSerializer;
}

public virtual async Task<IEnumerable<ProductPart>> GetProductsAsync(IEnumerable<string> skus)
Expand All @@ -57,6 +60,13 @@ public async Task<string> GetOrderFullSkuAsync(ShoppingCartItem item, ProductPar
.Select(attr => attr.PartName + "." + attr.Name)
.ToHashSet();

if (attributesRestrictedToPredefinedValues.Count == 0) return item.ProductSku;

if (item.HasRawAttributes())
{
item.Attributes.SetItems(await _shoppingCartSerializer.Value.PostProcessAttributesAsync(item.Attributes, productPart));
}

var variantKey = item.GetVariantKeyFromAttributes(attributesRestrictedToPredefinedValues);
var fullSku = item.Attributes.Any()
? item.ProductSku + "-" + variantKey
Expand Down
2 changes: 1 addition & 1 deletion src/Modules/OrchardCore.Commerce/Views/ProductPart.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
minOrderQuantity = (int)(inventoryPart.MinimumOrderQuantity.Value > 0 && !inventoryPart.IgnoreInventory.Value
? inventoryPart.MinimumOrderQuantity.Value
: 0);
: 1);
maxOrderQuantity = (int)(inventoryPart.MaximumOrderQuantity.Value > 0 && !inventoryPart.IgnoreInventory.Value
? inventoryPart.MaximumOrderQuantity.Value
: int.MaxValue);
Expand Down
Loading