Skip to content

Pass an explicit language to cart warnings and loyalty point messages - #938

Merged
KrzysztofPajak merged 6 commits into
developfrom
refactor/business-explicit-context-translations
Oct 17, 2026
Merged

KrzysztofPajak merged 6 commits into
developfrom
refactor/business-explicit-context-translations

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Part of the explicit-context work in Business (after #930, #931, #932, #936, #937)
Type: feature

Issue

57 calls in src/Business translated with ITranslationService.GetResource(name), which reads the working language of the current request. These were the 11 cart validators in Grand.Business.Checkout/Validators, the three loyalty point handlers for orders, and the loyalty points for registration. Outside a storefront request the text was wrong or missing. With no request (a scheduled task, a background handler) GetResource(name) returns an empty string. From the admin panel or the API it uses the admin's or caller's language, not the customer's.

Examples:

  • An order marked as paid by a scheduled task or an admin wrote the loyalty points history entry ("Earned for order …") in the admin's language, or left it empty, instead of the customer's language.
  • EndAuctionsTask validated the winning bid with empty warning texts, so the log line said nothing.
  • The cart merge at login validated each item in the request language, which it took from the ambient context.

Solution

  • Cart validation. IShoppingCartValidator methods and the validator records take a Language. The validators call GetResource(name, language.Id). IShoppingCartService.AddToCart, UpdateShoppingCartItem and MigrateShoppingCart, AddRequiredProductsCommand and ReOrderCommand pass the language down. Storefront and admin callers pass WorkContext.WorkingLanguage. PlaceOrderCommandHandler uses the language the order is placed in (PlaceOrderContainer.WorkingLanguage). EndAuctionsTask uses the admin language (LanguageSettings.DefaultAdminLanguageId), because its warnings only reach the log.
  • Login and registration events. CustomerLoggedInEvent and CustomerRegisteredEvent carry the working language of the request. The login event's language goes to the cart merge, the registration event's language to the registration loyalty points. IExternalAuthenticationService.Authenticate and IOidcCustomerResolver.ResolveForLogin take it from the controllers (Facebook, Google, OpenID Connect). An account created from an API access token (OidcCustomerResolver.Resolve) has no request language. The registration handler then uses the store's default language, or the first language of the store.
  • Loyalty points for orders. Award, reduce and return use Order.CustomerLanguageId, the language the customer placed the order in. This is a separate commit.
  • Fallback language. The new extension ILanguageService.GetLanguageOrStoreDefault(languageId, store) returns the given language while it still exists. Otherwise it returns the store's default language, then the store's first language, then any language. The loyalty point handlers use it for an order without CustomerLanguageId or whose language or store was deleted. Before, such an order wrote the raw resource key or threw on a null key. The registration handler uses it when the event has no language, and EndAuctionsTask for the admin language. When no language exists at all, EndAuctionsTask logs a warning and skips the bid.
  • Warning events. ShoppingCartItemWarningsEvent and ShoppingCartWarningsEvent carry the language, so plugins that add cart warnings can write them in the same language as the core warnings.

After this PR, src/Business has no GetResource(name) calls. That overload, and IPermissionService.Authorize(Permission), stay for Web, modules and plugins (the remainder documented in #937).

Breaking changes

  • IShoppingCartValidator: every method takes a Language language. It is the last parameter, except in GetShoppingCartItemAttributeWarnings (before ignoreNonCombinableAttributes) and GetShoppingCartWarnings (after context).
  • The ShoppingCart*ValidatorRecord records have a trailing Language Language.
  • IShoppingCartService.AddToCart(customer, language, productId, shoppingCartType, storeId, warehouseId = null, …), UpdateShoppingCartItem(customer, language, shoppingCartItemId, warehouseId, attributes, …) and MigrateShoppingCart(fromCustomer, toCustomer, storeId, language, includeCouponCodes). The language comes right after the customer, so an old positional call does not compile instead of binding a null to the language.
  • ShoppingCartItemWarningsEvent and ShoppingCartWarningsEvent take a trailing Language language (Language property).
  • AddRequiredProductsCommand.Language and ReOrderCommand.Language (required).
  • CustomerLoggedInEvent(customer, previousCustomer, storeId, language) and CustomerRegisteredEvent(customer, language). A null language means the registration had no request.
  • IExternalAuthenticationService.Authenticate(parameters, customer, store, language, returnUrl = null). The protected ExternalAuthenticationService helpers take the language too.
  • IOidcCustomerResolver.ResolveForLogin(sub, email, emailVerified, storeId, language).
  • Constructors: CustomerRegisteredEventHandler, AwardLoyaltyPointsCommandHandler, ReduceLoyaltyPointsCommandHandler and ReturnBackRedeemedLoyaltyPointsCommandHandler add ILanguageService and IStoreService. EndAuctionsTask adds ILanguageService.

Testing

Automated: the solution builds, and dotnet test GrandNode.slnx passes (22 projects). New or changed tests cover:

  • a validator writing its warning in the language it is given;
  • ShoppingCartValidator passing the language into every record it builds;
  • AddRequiredProductsCommand passing its language to AddToCart;
  • the three loyalty point handlers translating in Order.CustomerLanguageId;
  • the registration handler using the event's language, then the store's default language, then the first store language;
  • the login handler passing the event's store and language to MigrateShoppingCart;
  • the external authentication and OpenID Connect sign-in putting the request language on both events;
  • EndAuctionsTask validating in the admin language.

Manual: run on 2026-10-17 with Playwright against a fresh install with sample data. A second language, "Polski", has every English resource prefixed with PL: , so each message shows which language it was written in. Loyalty points: 10 for registration, 1 point per 1 USD, awarded at Processing.

# Scenario Result
1 Register in Polish Loyalty history: "PL: Registered as customer"
2a Add a gift voucher to the cart without recipient details, in Polish ["PL: Enter valid recipient name","PL: Enter valid recipient email"]
2b Update the cart quantity to 100000, in Polish "PL: The maximum quantity allowed for purchase is 100."
3 Place an order in Polish (cash on delivery), then "Mark as paid" from the admin panel in English "PL: Earned promotion for order #1": the order's language, not the admin's
4 A guest adds a product in Polish, then logs in The product is in the account's cart
5 Control in English: registration and the gift voucher Messages without PL:

The server log showed no errors other than e-mail sending (no SMTP server locally).

support added 2 commits October 17, 2026 15:45
Awarding, reducing and returning loyalty points for an order wrote the
history message in the working language of whatever request triggered
it. From a scheduled task, an admin action or the API that was the
admin's language or nothing at all. The message now uses the language
the customer placed the order in (Order.CustomerLanguageId).
The cart validators and the loyalty points for registration read the
working language of the current request through GetResource(name), so
outside a storefront request (scheduled tasks, the API, background
handlers) their messages came out empty or in the wrong language.

- IShoppingCartValidator methods and the validator records take the
  language the warnings are written in.
- IShoppingCartService.AddToCart, UpdateShoppingCartItem and
  MigrateShoppingCart, AddRequiredProductsCommand and ReOrderCommand
  carry it down to the validators.
- CustomerLoggedInEvent and CustomerRegisteredEvent carry the working
  language of the request; IExternalAuthenticationService.Authenticate
  and IOidcCustomerResolver.ResolveForLogin take it from the controllers.
  A customer registered from an access token has none, and the
  registration loyalty points fall back to the store's default language.
- EndAuctionsTask validates the winning bid in the admin language, since
  its warnings only reach the log.

No GetResource(name) call is left in src/Business.
Copilot AI balanced review requested due to automatic review settings October 17, 2026 13:46

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.

support added 4 commits October 17, 2026 15:47
An order without CustomerLanguageId, a deleted order or store language, or a
store without a published language made GetResource throw on a null key or
write the raw resource key. GetLanguageOrStoreDefault picks the given language
while it exists, then the store default, then any store language.
Plugins that add cart warnings now get the language the core warnings are
written in.
…pingCartItem

A required parameter inserted before the optional ones let an old positional
null bind to the language and fail at runtime; now an old call does not
compile. EndAuctionsTask skips a bid instead of throwing when no language
exists at all.
@KrzysztofPajak
KrzysztofPajak merged commit d7dca3e into develop Oct 17, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the refactor/business-explicit-context-translations branch October 17, 2026 15:00
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