Skip to content

Pass an explicit context to the checkout services - #932

Merged
KrzysztofPajak merged 39 commits into
developfrom
refactor/business-explicit-context-checkout
Oct 16, 2026
Merged

KrzysztofPajak merged 39 commits into
developfrom
refactor/business-explicit-context-checkout

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Type: feature

Issue

The checkout services in Grand.Business.Checkout read the customer, store, currency, tax display, language and request host from WorkContext / StoreContext: PlaceOrderCommandHandler, ShoppingCartService, ShoppingCartValidator, CheckoutAttributeService, CheckoutAttributeFormatter, and the cart-merging CustomerLoggedInEventHandler. Their results therefore depended on who called them, and outside a request they had no values to read. Follows #930 (catalog, CatalogAccess) and #931 (pricing, tax and checkout calculation), both merged into develop.

Solution

Nothing in Grand.Business.Checkout reads IContextAccessor any more. Callers pass the values; Web takes them from its request.

  • PlaceOrderCommand carries a CalculationContext, the fallback Language, the store Host and OriginalCustomerIfImpersonated. The checkout page fills them from its request, and PlaceOrderContainer carries the context. This removes PR 2's temporary AmbientCalculationContext.
  • OrderNotificationCommand carries Customer and OriginalCustomerIfImpersonated instead of the whole IWorkContext.
  • ICheckoutAttributeFormatter.FormatAttributes(attributes, CalculationContext, Language, DomainHost, …).
  • ICheckoutAttributeService.GetAllCheckoutAttributes(CatalogAccess, excludeShippableAttributes), the same access object the catalog read services take since Pass customer and store explicitly to catalog read services #930: CatalogAccess.For(customer, storeId) on the storefront, CatalogAccess.IncludingHidden(storeId) in the admin panel. The storeId, customer and ignoreAcl parameters are gone.
  • IShoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, CalculationContext, …). CheckCommonWarnings and GetRequiredProductWarnings now use the customer and store id they already received.
  • IShoppingCartService.GetShoppingCart(customer, storeId, …) and MigrateShoppingCart(from, to, storeId, includeCouponCodes).
  • CustomerLoggedInEvent(customer, previousCustomer, storeId). Each of the four publishers reads the signed-in customer and the store on the line where it publishes, which is the same moment the handler used to read them.
  • The storefront cart handlers (GetShoppingCartHandler, GetMiniShoppingCartHandler, GetAddToCartHandler, and the cart warnings in GetWishlistHandler) now use their own request values instead of the request context. They are the same values, because every sender fills them from the current request.

Deliberate behavior changes:

  • Checkout attribute cache: the all-attributes cache key held the store, the shippable flag and the ACL flag, but not the customer groups the ACL filter used. The first customer's filtered list was then served to every customer of the store, so attributes limited to a group were shown to, or hidden from, the wrong customers. The key now includes the group ids, as the Catalog keys do (separate commit).
  • Admin condition picker: on the admin checkout attribute page, the condition dropdown filtered attributes by the staff member's own groups, so attributes limited to other groups could not be picked as a condition. It now lists all of them, like the attribute list on the same page. This change lands in the explicit-customer commit: AdminShared has no request customer to pass, so the new signature forces it.
  • Required-products check: the check for products that require other products now looks at the cart of the customer it is given, not the cart of whoever made the request. For a shared wishlist it checks the owner's wishlist, not the visitor's. For an admin editing a customer's cart, it checks the customer's cart, not the admin's own.
    • The login cart merge checks required products against the guest cart, which holds the whole set being moved, and adds each item to the account without the check (separate commits). Checking the account's cart would fail before the required product arrived, and the item would be dropped with the guest cart. Before this branch the check saw the guest's full cart, so the outcome is unchanged.

Not in this PR: ProductAttributeFormatter (follow-up PR), and ExternalAuthenticationService, which still reads the request context itself (PR 4).

Breaking changes

  • Every signature listed above changed. Plugins that call these methods, publish CustomerLoggedInEvent or send PlaceOrderCommand must pass the new values and be rebuilt. PlaceOrderCommand.Context, Language and Host are required.
  • OrderNotificationCommand.WorkContext is removed.
  • Validator records: ShoppingCartRequiredProductValidatorRecord.Store became StoreId, and ShoppingCartCommonWarningsValidatorRecord lost its unused Store.
  • Constructors: PlaceOrderCommandHandler, ShoppingCartService, ShoppingCartValidator, CheckoutAttributeService, CheckoutAttributeFormatter and CustomerLoggedInEventHandler no longer take IContextAccessor.

Testing

  1. dotnet build GrandNode.slnx
  2. dotnet test GrandNode.slnx: green.
  3. New tests:
    • CheckoutAttributeServiceTests:
      • GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomer
      • GetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroups
      • GetAllCheckoutAttributes_IncludingHidden_ReturnsGroupLimitedAttributes
    • CheckoutAttributeViewModelServiceTests.PrepareConditionAttributes_OffersAttributesLimitedToCustomerGroups: pins the CatalogAccess.IncludingHidden() call, which the real service's ACL tests cover
    • CheckoutAttributeFormatterTests: context currency and tax display, translated names, file link host
    • ShoppingCartValidatorTests
    • ShoppingCartServiceTests:
      • GetShoppingCart_ReturnsTheItemsOfTheGivenCustomer
      • MigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStore
      • MigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetCartYet
      • MigrateShoppingCart_ChecksRequiredProductsAgainstTheSourceCart
    • CustomerLoggedInEventHandlerTests.Handle_MigratesFromThePreviousCustomerInTheEventStore
    • PlaceOrderCommandHandlerTests, OrderNotificationCommandHandlerTests
  4. Manual test on a local store:
    • a guest added an item and chose gift wrapping, then logged in, and the cart and the choice moved to the account
    • orders placed in EUR and in USD with 23% tax and gift wrapping
    • the storefront order details, the admin order page and both order emails agree on every amount
    • the admin condition picker lists the other attributes

support added 30 commits October 16, 2026 16:00
The handlers cache the ACL-filtered result under keys shared by many customers. A query without a customer would authorize nothing and cache the empty list for all of them. Also pins DeleteCategory detaching its children and restores a negative assertion to any customer and store.
Every other amount in the order email is formatted in the currency the order was placed in; the tax rates used whatever currency the request that sent the email was working in, which for background sending is not the customer's.
CheckoutAttributeService no longer reads the current customer for its group filter; callers pass the customer whose groups apply. The admin condition picker has no request customer, so it now loads attributes without the group filter, like the attribute list on the same screen. Before, it hid attributes limited to groups the staff member was not in.
The all-attributes cache key held the store, the shippable flag and the ACL flag, but not the groups the ACL filter used. The first customer's filtered list was then served to every customer of the store, so group-limited attributes leaked to or hid from the wrong customers. The key now includes the group ids, like the catalog keys.
CheckoutAttributeFormatter no longer reads the working language, currency, tax display or request host. Callers pass a CalculationContext, the language and the store host; the place-order handler and the cart page still hand over their current request values until their own steps.
ShoppingCartValidator no longer reads the current customer, store or currency. GetShoppingCartWarnings takes a CalculationContext; CheckCommonWarnings and GetRequiredProductWarnings use the customer and store id they already received. The required-product record carries the store id and the common-warnings record drops its unused store.
ShoppingCartService.GetShoppingCart takes the customer whose cart it reads, and MigrateShoppingCart the store whose selected checkout attributes move along. CustomerLoggedInEvent carries the customer the request was signed in as before the login and the request's store; every publisher reads them where it publishes, so the cart merge sees the same values as before.
PlaceOrderCommand carries the calculation context, the fallback language, the store host and the impersonating staff member; the checkout page fills them from its request. PlaceOrderCommandHandler no longer reads WorkContext or StoreContext, and OrderNotificationCommand carries the customer and the impersonating staff member instead of the whole work context.
GetShoppingCartHandler priced, taxed and validated the cart with the current request's values although its request carries the same customer, store, currency, tax display and language. It now uses the request; every sender fills it from the current request, so the page is unchanged. Only the store host for file links still comes from the request context.
The required-product check now looks at the cart of the customer it is given instead of the request's customer. During the login merge that is the account being filled, which does not hold the guest's remaining items yet, so a product whose required product came later in the guest cart was skipped and then deleted with the guest cart. The merge moves the whole cart together and skips that check, which matches the old outcome where the check saw the guest's full cart.
…stomer for hidden records

GetPersonalizedProductsQuery and GetSuggestedProductsQuery read the customer id and tags from Customer instead of separate fields that could disagree with it. The product mapping and child category lookups no longer read the customer or store when showHidden is set, so admin callers pass null. Methods that authorize through IAclService throw on a missing customer instead of returning nothing.
The admin cart and wishlist details, the customer edit page's cart tab and
the attribute price adjustments priced another customer's items with the
logged-in admin's groups, discounts and tax. They now use the customer
whose cart it is and the store the item was added in.
ConvertLoyaltyPointsToAmount returns an amount in the primary store
currency, but rounded it in the working currency, so a zero-decimal
working currency rounded 4.95 USD to 5 before conversion. The currency
parameter added for that rounding is dropped again; the amount is rounded
in the primary store currency.
The cart, mini cart, order totals and payment method handlers priced their
lines from the request but their totals from the ambient context. They now
build one context from the request fields, which no longer needs the
context accessor. GetPaymentMethod carries the tax display type like the
other cart requests.

ShoppingCartTotalAmountValidatorRecord and
ValidateMinShoppingCartSubtotalAmountCommand carried a customer and a
currency next to a context holding the same; they now read both from the
context.
Both are dereferenced while the order is prepared: the language when the customer has no published language of their own, the host for file links in the checkout attribute description. A sender that left them out got a NullReferenceException instead of an argument error.
The customer is optional only when ACL is ignored. Without the guard a null customer failed inside the cache key with a NullReferenceException.
support added 6 commits October 16, 2026 18:19
Skipping the check entirely let an item whose required product was missing from the guest cart move to the account. The guest cart holds the whole set being moved, so the check runs there, as it did before the explicit context.
Both handlers read the cart and the calculation context from the request context while the rest of the model used the request's customer and store. Every sender fills them from the current request, so the result is unchanged.
The cart warnings used the visitor's calculation context. They now use the wishlist owner and store from the request, with the visitor's currency and tax display.
The catalog read services took a customer, a store id and a showHidden flag whose meaning depended on each other: the customer had to be null exactly when hidden records were asked for. CatalogAccess.For(customer, storeId) and CatalogAccess.IncludingHidden(storeId) make that choice one argument. The store limitation now applies whenever a store id is set, so the back office passes IncludingHidden() where it ignored the store before. The storefront builds its access from the request with IContextAccessor.CatalogAccess().
…xt-catalog' into refactor/business-explicit-context-pricing

# Conflicts:
#	src/Web/Grand.Web/Features/Handlers/Products/GetProductDetailsPageHandler.cs
@KrzysztofPajak
KrzysztofPajak changed the base branch from refactor/business-explicit-context-pricing to develop October 16, 2026 19:36
support added 3 commits October 16, 2026 21:43
…xt-pricing' into refactor/business-explicit-context-checkout

# Conflicts:
#	src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs
#	src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetAddToCartHandler.cs
#	src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetMiniShoppingCartHandler.cs
#	src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs
…explicit-context-checkout

# Conflicts:
#	src/Business/Grand.Business.Checkout/Commands/Handlers/Orders/PlaceOrderCommandHandler.cs
#	src/Business/Grand.Business.Checkout/Services/CheckoutAttributes/CheckoutAttributeFormatter.cs
#	src/Business/Grand.Business.Checkout/Services/Orders/ShoppingCartValidator.cs
#	src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetShoppingCartHandler.cs
#	src/Web/Grand.Web/Features/Handlers/ShoppingCart/GetWishlistHandler.cs
GetAllCheckoutAttributes took a store id, a nullable customer and an ignoreAcl flag that had to agree with each other. It now takes the CatalogAccess the catalog read services use: CatalogAccess.For(customer, storeId) on the storefront, CatalogAccess.IncludingHidden(storeId) in the admin panel. A missing customer can no longer reach the group filter.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 19:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants