Skip to content

Commit

Permalink
OCC-181: Inventory warnings when listing content items in admin dashb…
Browse files Browse the repository at this point in the history
…oard (#338)

* Create extension methods for error message checking.

* Add extension documentation.

* Add PriceEstimationWithMinimumOrderQuantityShouldNotShowWarning test (TDD).

* Create an abstraction to update the context used for product price estimation.

* Remove copy-paste error.

* Add Feature ID for "Orchard Core Commerce - Inventory".

* Add inventory product estimation context updater.

* Typo
  • Loading branch information
sarahelsaig authored Sep 14, 2023
1 parent 4e6beaa commit 75102d3
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OrchardCore.Commerce.Inventory.Constants;

public static class FeatureIds
{
public const string Area = "OrchardCore.Commerce.Inventory";
public const string Inventory = Area;
}
8 changes: 8 additions & 0 deletions src/Modules/OrchardCore.Commerce.Inventory/Manifest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using OrchardCore.Commerce.Inventory.Constants;
using OrchardCore.Modules.Manifest;

[assembly: Module(
Expand All @@ -8,3 +9,10 @@
Description = "Inventory management for Orchard Core Commerce.",
Category = "Commerce"
)]

[assembly: Feature(
Id = FeatureIds.Inventory,
Name = "Orchard Core Commerce - Inventory",
Category = "Commerce",
Description = "Inventory management for Orchard Core Commerce."
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using OrchardCore.Commerce.Models;

namespace OrchardCore.Commerce.Abstractions;

/// <summary>
/// A service that updates the <see cref="ProductEstimationContext"/> used by the <see
/// cref="IShoppingCartHelpers.EstimateProductAsync"/> method.
/// </summary>
public interface IProductEstimationContextUpdater : ISortableUpdaterProvider<ProductEstimationContext>
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ Task<ShoppingCartItem> AddToCartAsync(
bool storeIfOk = false);

/// <summary>
/// Adds the given <paramref name="item"/> to the shopping cart without saving, validates the cart and calculates
/// the display information for the added item.
/// Adds the product with the given <paramref name="sku"/> to the shopping cart without saving, validates the cart
/// and calculates the display information for the added item.
/// </summary>
Task<ShoppingCartLineViewModel> EstimateProductAsync(
string shoppingCartId,
ShoppingCartItem item,
string sku,
Address shipping = null,
Address billing = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,7 @@ public override async Task<IDisplayResult> DisplayAsync(ProductPart part, BuildP

try
{
var model = await _shoppingCartHelpers.EstimateProductAsync(
shoppingCartId: null,
new ShoppingCartItem(
quantity: 1,
part.Sku));
var model = await _shoppingCartHelpers.EstimateProductAsync(shoppingCartId: null, part.Sku);
var data = model.AdditionalData;

var discounts = data.GetDiscounts().ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ public override async Task<IDisplayResult> DisplayAsync(TaxPart part, BuildPartD
var addresses = await _hca.HttpContext.GetUserAddressAsync();
var model = await _shoppingCartHelpers.EstimateProductAsync(
shoppingCartId: null,
new ShoppingCartItem(
quantity: 1,
product.Sku),
product.Sku,
addresses?.ShippingAddress.Address,
addresses?.BillingAddress.Address);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using OrchardCore.Commerce.AddressDataType;

namespace OrchardCore.Commerce.Models;

public record ProductEstimationContext(
string ShoppingCartId,
ShoppingCartItem ShoppingCartItem,
Address ShippingAddress,
Address BillingAddress);
36 changes: 29 additions & 7 deletions src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using OrchardCore.Commerce.ProductAttributeValues;
using OrchardCore.Commerce.ViewModels;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;

Expand All @@ -19,15 +20,21 @@ public class ShoppingCartHelpers : IShoppingCartHelpers
{
private readonly IHttpContextAccessor _hca;
private readonly IPriceService _priceService;
private readonly IEnumerable<IProductEstimationContextUpdater> _productEstimationContextUpdaters;
private readonly IProductService _productService;
private readonly IEnumerable<IShoppingCartEvents> _shoppingCartEvents;
private readonly IShoppingCartPersistence _shoppingCartPersistence;
private readonly IShoppingCartSerializer _shoppingCartSerializer;
private readonly IHtmlLocalizer<ShoppingCartHelpers> H;

[SuppressMessage(
"Major Code Smell",
"S107:Methods should not have too many parameters",
Justification = "This service ties together many cart-related features.")]
public ShoppingCartHelpers(
IHttpContextAccessor hca,
IPriceService priceService,
IEnumerable<IProductEstimationContextUpdater> productEstimationContextUpdaters,
IProductService productService,
IEnumerable<IShoppingCartEvents> shoppingCartEvents,
IShoppingCartPersistence shoppingCartPersistence,
Expand All @@ -36,6 +43,7 @@ public ShoppingCartHelpers(
{
_hca = hca;
_priceService = priceService;
_productEstimationContextUpdaters = productEstimationContextUpdaters;
_productService = productService;
_shoppingCartEvents = shoppingCartEvents;
_shoppingCartPersistence = shoppingCartPersistence;
Expand Down Expand Up @@ -168,17 +176,31 @@ public async Task<ShoppingCartItem> AddToCartAsync(
return (cart, item);
}

public async Task<ShoppingCartLineViewModel> EstimateProductAsync(
public Task<ShoppingCartLineViewModel> EstimateProductAsync(
string shoppingCartId,
ShoppingCartItem item,
string sku,
Address shipping = null,
Address billing = null)
Address billing = null) =>
EstimateProductAsync(new ProductEstimationContext(
shoppingCartId,
new ShoppingCartItem(quantity: 1, productSku: sku),
shipping,
billing));

private async Task<ShoppingCartLineViewModel> EstimateProductAsync(ProductEstimationContext context)
{
var sku = item.ProductSku;
var (cart, _) = await AddItemAndGetCartAsync(shoppingCartId, item);
foreach (var updater in _productEstimationContextUpdaters)
{
if (await updater.IsApplicableAsync(context))
{
context = await updater.UpdateAsync(context) ?? context;
}
}

var (cart, _) = await AddItemAndGetCartAsync(context.ShoppingCartId, context.ShoppingCartItem);

return (await CreateShoppingCartViewModelAsync(cart, shipping, billing))
return (await CreateShoppingCartViewModelAsync(cart, context.ShippingAddress, context.BillingAddress))
.Lines
.FirstOrDefault(line => line.ProductSku == sku);
.FirstOrDefault(line => line.ProductSku == context.ShoppingCartItem.ProductSku);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using OrchardCore.Commerce.Abstractions;
using OrchardCore.Commerce.Inventory.Models;
using OrchardCore.Commerce.Models;
using OrchardCore.ContentManagement;
using System.Threading.Tasks;

namespace OrchardCore.Commerce.Settings;

public class InventoryProductEstimationContextUpdater : IProductEstimationContextUpdater
{
private readonly IProductService _productService;
private readonly IShoppingCartPersistence _shoppingCartPersistence;
public int Order => 0;

public InventoryProductEstimationContextUpdater(
IProductService productService,
IShoppingCartPersistence shoppingCartPersistence)
{
_productService = productService;
_shoppingCartPersistence = shoppingCartPersistence;
}

public async Task<ProductEstimationContext> UpdateAsync(ProductEstimationContext model)
{
var product = await _productService.GetProductAsync(model.ShoppingCartItem.ProductSku);
if (product.As<InventoryPart>() is not { } inventory) return model;

var cart = await _shoppingCartPersistence.RetrieveAsync(model.ShoppingCartId);
var item = cart.AddItem(model.ShoppingCartItem.WithQuantity(0));
var newQuantity = item.Quantity + model.ShoppingCartItem.Quantity;

var minimum = inventory.MinimumOrderQuantity.Value is { } minimumDecimal ? (int)minimumDecimal : int.MinValue;
if (newQuantity < minimum)
{
model = model with { ShoppingCartItem = model.ShoppingCartItem.WithQuantity(minimum - item.Quantity) };
}

return model;
}

public Task<bool> IsApplicableAsync(ProductEstimationContext model) => Task.FromResult(true);
}
7 changes: 7 additions & 0 deletions src/Modules/OrchardCore.Commerce/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro
}
}

[RequireFeatures(Inventory.Constants.FeatureIds.Inventory)]
public class InventoryStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services) =>
services.AddScoped<IProductEstimationContextUpdater, InventoryProductEstimationContextUpdater>();
}

[RequireFeatures("OrchardCore.ContentLocalization")]
public class ContentLocalizationStartup : StartupBase
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Atata;
using Lombiq.Tests.UI.Extensions;
using OpenQA.Selenium;
using Shouldly;

namespace Lombiq.Tests.UI.Services;

public static class NotificationUITestContextExtensions
{
/// <summary>
/// Returns the text of the element with the <c>message-error</c> class if one exists.
/// </summary>
/// <param name="safely">
/// If the element is found then this doesn't matter. Otherwise if it's <see langword="true"/> then <see
/// langword="null"/> is returned and if it's <see langword="false"/> an exception is thrown.
/// </param>
public static string GetErrorMessage(this UITestContext context, bool safely = false)
{
var by = By.ClassName("message-error").Safely(safely);
return context.Get(by)?.Text?.Trim();
}

/// <summary>
/// Looks for the element with the <c>message-error</c> class, it shouldn't exist or its content should be empty. If
/// that's not true an exception will be thrown containing the element text.
/// </summary>
public static void ErrorMessageShouldNotExist(this UITestContext context) =>
context.GetErrorMessage(safely: true).ShouldBeNullOrEmpty();
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ await context.SelectFromBootstrapDropdownReliablyAsync(
await context.ClickReliablyOnAsync(By.LinkText("View"));
context.SwitchToLastWindow();
context.Missing(By.ClassName("message-error"));
context.ErrorMessageShouldNotExist();
context.Get(By.CssSelector("header.masthead h1")).Text.Trim().ShouldBe(LocalizedTitle);
await context.ClickReliablyOnAsync(By.CssSelector("form[action='/shoppingcart/AddItem'] button.btn-primary"));
context.Missing(By.ClassName("message-error"));
context.ErrorMessageShouldNotExist();
context.Get(By.ClassName("cart-product-name")).Text.Trim().ShouldBe(LocalizedTitle);
context.Get(By.ClassName("shopping-cart-table-unit-price")).Text.Trim().ShouldBe("3 500,00 Ft");
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ public Task PriceVariantsProductCanBeAddedToShoppingCart(Browser browser) =>
},
browser);

[Theory, Chrome]
public Task PriceEstimationWithMinimumOrderQuantityShouldNotShowWarning(Browser browser) =>
ExecuteTestAfterSetupAsync(
async context =>
{
await context.SignInDirectlyAsync();
await context.GoToContentItemEditorByIdAsync(TestProduct);
await context.ClickAndFillInWithRetriesAsync(By.Id("InventoryPart_MinimumOrderQuantity_Value"), "2");
await context.ClickAndFillInWithRetriesAsync(By.Id("InventoryPart_MaximumOrderQuantity_Value"), "5");
await context.ClickReliablyOnSubmitAsync();
context.ShouldBeSuccess();
await context.GoToContentItemByIdAsync(TestProduct);
context.ErrorMessageShouldNotExist();
},
browser);

private static void ShoppingCartItemCountShouldBe(UITestContext context, int count) =>
context.Get(By.ClassName("shopping-cart-item-count")).Text.ShouldBeAsString(count);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ public Task CartVerifyingItemEventShouldDisplayError(Browser browser) =>
// Due to the expected verification failure, the cart should still be empty and the error message shown.
context.Driver.Url.ShouldEndWith("/cart-empty");
context
.Get(By.ClassName("message-error"))
.Text
.Trim()
.GetErrorMessage()
.ShouldBe("The \"Item Verification Sample\" workflow has intentionally failed this product.");
},
browser);
Expand Down

0 comments on commit 75102d3

Please sign in to comment.