Skip to content

Return named records from the order calculation services - #964

Merged
KrzysztofPajak merged 2 commits into
developfrom
refactor/order-calculation-results
Oct 21, 2026
Merged

KrzysztofPajak merged 2 commits into
developfrom
refactor/order-calculation-results

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Part of the architecture backlog (item S12: tuple results in the order calculation services)
Type: feature

Issue

IOrderCalculationService and IShoppingCartSubTotalService returned positional tuples: GetShoppingCartSubTotal five fields, GetShoppingCartTotal six, GetShoppingCartShippingTotal three, and AdjustShippingRate and GetTaxTotal two each. The field names are not part of the type, so a caller could swap two doubles and the compiler would not notice. The signatures were also hard to read and hard to mock: a test had to spell out the whole tuple type in Task.FromResult<(...)>.

Solution

  • Five sealed positional records in Grand.Business.Core.Utilities.Checkout, next to CalculationContext:
    • ShoppingCartSubTotal(DiscountAmount, AppliedDiscounts, SubTotalWithoutDiscount, SubTotalWithDiscount, TaxRates), returned by both interfaces
    • ShippingRateAdjustment(ShippingRate, AppliedDiscounts)
    • ShoppingCartShippingTotal(ShippingTotal, TaxRate, AppliedDiscounts)
    • ShoppingCartTaxTotal(TaxTotal, TaxRates)
    • ShoppingCartTotal(Total, DiscountAmount, AppliedDiscounts, AppliedGiftVouchers, RedeemedLoyaltyPoints, RedeemedLoyaltyPointsAmount)
  • Every tuple-returning method on the two interfaces returns one of them, so the interface keeps one style. The field types and their order have not changed.
  • Positional records have Deconstruct, so callers outside this repository that write var (a, b, ...) = await ... still compile. Inside the repository every caller now reads the results by property, so no caller depends on field position any more. Callers that read tuple fields by name now use the properties (.subTotalWithDiscount → .SubTotalWithDiscount, .shoppingCartTotal → .Total, .shoppingCartShippingTotal → .ShippingTotal, .taxtotal → .TaxTotal).
  • Pure refactor: the calculations are unchanged.

Breaking changes

  • The return types of IOrderCalculationService.GetShoppingCartSubTotal, AdjustShippingRate, both GetShoppingCartShippingTotal overloads, GetTaxTotal and GetShoppingCartTotal, and of IShoppingCartSubTotalService.GetShoppingCartSubTotal, are now records instead of tuples.
    • Not binary compatible. A plugin or module compiled against 2.4.x that calls any of these methods fails at runtime with MissingMethodException until it is recompiled, even if its source compiles unchanged. Third-party plugins must be rebuilt against this version.
    • Code outside this repository that deconstructs the result positionally still compiles.
    • Code that reads tuple fields by name must use the new property names.
    • A class that overrides one of the virtual methods of OrderCalculationService / ShoppingCartSubTotalService, or implements one of the interfaces, must change the return type and return new ShoppingCartTotal(...) (etc.) instead of a tuple literal.
    • A Moq mock of either interface with no setup for these methods now returns null instead of a default tuple.
  • In this repository the affected plugins are Payments.BrainTree and Payments.CashOnDelivery, both updated here.

Testing

  1. dotnet build GrandNode.slnx: the whole solution, plugins included, builds with no errors and no new warnings in the files this PR changes.
  2. dotnet test src/Tests/Grand.Business.Checkout.Tests (292), dotnet test src/Tests/Grand.Web.Tests (154), dotnet test src/Tests/Grand.Modules.Tests (397) all pass, with the same counts as before the change. The only test changes are the mocks that built a tuple, which now build the record, and the assertions that read tuple fields by name, which now read the properties. No assertion was removed or weakened.
  3. Optional storefront check: add a product to the cart and walk through checkout with a subtotal discount, a shipping method and a payment method with an additional fee. The mini cart, cart totals, estimated shipping, shipping and payment method fees, and the placed order's totals show the same amounts as on develop.

IOrderCalculationService and IShoppingCartSubTotalService returned positional tuples of up to six fields. The field names were not part of the type, so two doubles could be swapped without a compile error, and every mock had to spell out the whole tuple type. Each method now returns a sealed positional record from Grand.Business.Core.Utilities.Checkout; deconstructing callers compile unchanged.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 04:34

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.

var taxRates = taxtotal.TaxRates;

if (taxtotal.taxtotal == 0 && _taxSettings.HideZeroTax)
if (taxtotal.TaxTotal == 0 && _taxSettings.HideZeroTax)
@KrzysztofPajak
KrzysztofPajak merged commit c0f9915 into develop Oct 21, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the refactor/order-calculation-results branch October 21, 2026 14:30
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