Repository navigation
Pass an explicit context to the checkout services - #932
Merged
KrzysztofPajak merged 39 commits intoOct 16, 2026
Merged
Conversation
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.
…or/business-explicit-context-pricing
…or/business-explicit-context-checkout
…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.
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
…explicit-context-pricing
KrzysztofPajak
changed the base branch from
refactor/business-explicit-context-pricing
to
develop
October 16, 2026 19:36
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.
KrzysztofPajak
deleted the
refactor/business-explicit-context-checkout
branch
October 16, 2026 19:59
KrzysztofPajak
added a commit
that referenced
this pull request
Oct 17, 2026
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type: feature
Issue
The checkout services in
Grand.Business.Checkoutread the customer, store, currency, tax display, language and request host fromWorkContext/StoreContext:PlaceOrderCommandHandler,ShoppingCartService,ShoppingCartValidator,CheckoutAttributeService,CheckoutAttributeFormatter, and the cart-mergingCustomerLoggedInEventHandler. 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.CheckoutreadsIContextAccessorany more. Callers pass the values; Web takes them from its request.PlaceOrderCommandcarries aCalculationContext, the fallbackLanguage, the storeHostandOriginalCustomerIfImpersonated. The checkout page fills them from its request, andPlaceOrderContainercarries the context. This removes PR 2's temporaryAmbientCalculationContext.OrderNotificationCommandcarriesCustomerandOriginalCustomerIfImpersonatedinstead of the wholeIWorkContext.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. ThestoreId,customerandignoreAclparameters are gone.IShoppingCartValidator.GetShoppingCartWarnings(cart, checkoutAttributes, CalculationContext, …).CheckCommonWarningsandGetRequiredProductWarningsnow use the customer and store id they already received.IShoppingCartService.GetShoppingCart(customer, storeId, …)andMigrateShoppingCart(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.GetShoppingCartHandler,GetMiniShoppingCartHandler,GetAddToCartHandler, and the cart warnings inGetWishlistHandler) 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:
Not in this PR:
ProductAttributeFormatter(follow-up PR), andExternalAuthenticationService, which still reads the request context itself (PR 4).Breaking changes
CustomerLoggedInEventor sendPlaceOrderCommandmust pass the new values and be rebuilt.PlaceOrderCommand.Context,LanguageandHostare required.OrderNotificationCommand.WorkContextis removed.ShoppingCartRequiredProductValidatorRecord.StorebecameStoreId, andShoppingCartCommonWarningsValidatorRecordlost its unusedStore.PlaceOrderCommandHandler,ShoppingCartService,ShoppingCartValidator,CheckoutAttributeService,CheckoutAttributeFormatterandCustomerLoggedInEventHandlerno longer takeIContextAccessor.Testing
dotnet build GrandNode.slnxdotnet test GrandNode.slnx: green.CheckoutAttributeServiceTests:GetAllCheckoutAttributes_FiltersByTheGroupsOfTheGivenCustomerGetAllCheckoutAttributes_CacheKeyDependsOnTheCustomerGroupsGetAllCheckoutAttributes_IncludingHidden_ReturnsGroupLimitedAttributesCheckoutAttributeViewModelServiceTests.PrepareConditionAttributes_OffersAttributesLimitedToCustomerGroups: pins theCatalogAccess.IncludingHidden()call, which the real service's ACL tests coverCheckoutAttributeFormatterTests: context currency and tax display, translated names, file link hostShoppingCartValidatorTestsShoppingCartServiceTests:GetShoppingCart_ReturnsTheItemsOfTheGivenCustomerMigrateShoppingCart_MovesCheckoutAttributesOfTheGivenStoreMigrateShoppingCart_DoesNotRequireRequiredProductsInTheTargetCartYetMigrateShoppingCart_ChecksRequiredProductsAgainstTheSourceCartCustomerLoggedInEventHandlerTests.Handle_MigratesFromThePreviousCustomerInTheEventStorePlaceOrderCommandHandlerTests,OrderNotificationCommandHandlerTests