feat(order): checkout orders keep the cart's discounts and taxes (Q7-W1a)

Merged
EV
11d ago
+1594-16

Item: Q7-W1a (commerce flow wiring, wave 1a) — handover HANDOVER-2026-10-17-gauzy-px-improvements.md §5.4.

What was wrong

OrderService.createFromCart copied a cart's lines, shipping methods and addresses and none of its adjustment / tax_line rows. Every order placed through checkout was re-totalled from empty ORDER_* ledgers: discountTotal = taxTotal = 0 and the grand total was not the amount the buyer was shown. OrderTotalsService also never read ORDER-level (document) adjustments.

What this changes (additive)

  • OrderCartLedgerCarryover (new, order/src/lib/checkout/): copies CART_LINE / CART_SHIPPING / CART adjustments and CART_LINE / CART_SHIPPING tax lines onto ORDER_LINE / ORDER_SHIPPING / ORDER through the kernel ledger services (append, so the sign / reason / one-basis-per-rate rules apply to the copy), each stamped metadata.carriedFrom.
  • createFromCart(cart, { carryCartLedgers: true }): builds the cart-row → order-row map while it creates the rows, carries the ledgers before placement (the placement totals and order.placed include them), stamps orderLine.metadata.cartLineId. Only OrderCheckoutHandler asks — the subscription path writes its own cycle discount/tax and must not get the cart's twice.
  • A refused copy cancels the unplaced draft (no event; not cancel(), whose shipped-check refuses a no-shipping draft) and fails the checkout, rather than placing an order without its tax.
  • OrderTotalsService.readTotalsContext passes ORDER-level adjustments as documentAdjustments (nothing wrote one before → no existing order moves).
  • New optional constructor param on OrderService is last + @Optional() (suites constructing by position unchanged).

Tests

  • order-cart-ledger-carryover.service.spec.ts (5) and 3 new cases in order.service.spec.ts (carried totals 54.10 = 50 − 6 + 4.6 + 5 + 0.5; no carry without the option; draft cancelled on a refused copy). Ran locally: order.service + carry-over + order-totals specs 85/85 green. tsc -p packages/plugins/order/tsconfig.lib.json --noEmit: no error in this change (7 pre-existing in core/bootstrap/tracer.ts, dependency typings).
  • E2E (written, not run as a suite): tools/scripts/commerce-checkout-ledgers-e2e.mjs — REST complete → GraphQL order read, and GraphQL completeCheckout → REST read; asserts discount / tax / grand total equal the cart's. Shared harness tools/scripts/commerce-wiring-harness.mjs for the Q7 runs (needs E2E_EMAIL/E2E_PASSWORD; names no Ever host).

Not in this PR

Payment → order ledger and completion (W1b), promotion usage (W1c), stock (W2) — see §5 of the handover. Cart claim/atomicity is R3-10 (px-cart, fix/cart-completion-claim); this PR does not touch its files.

Self-reviewed the diff (CodeRabbit is often rate-limited).

🤖 Generated with Claude Code


Summary by cubic

Orders placed through checkout now carry the cart's discounts and taxes over onto the order before it is placed. Previously OrderService.createFromCart copied lines, shipping methods and addresses but none of the cart's adjustment or tax_line rows, so every checkout order re-totalled from empty ledgers: discount and tax were zero and the grand total was not the amount the buyer was shown.

Details

  • New OrderCartLedgerCarryover copies the cart's adjustment and tax rows onto their order counterparts through the kernel's ledger services, so the sign, reason and one-basis-per-rate rules still apply; each copy records metadata.carriedFrom. A cart-level (CART) row is spread exactly over the order's lines by their value, so the per-line breakdown the invoice is built from still adds up to the totals.
  • The checkout handler opts in with carryCartLedgers: true; the subscription path stays unchanged so it doesn't charge the cart's rows twice.
  • If a copy is refused, the unplaced draft is cancelled (no event) and the checkout fails instead of placing an order without its tax.
  • OrderTotalsService now reads a netAmount stated as decimal text the same way it reads a number, matching the cart's reader for carried rows.
  • The new OrderService constructor parameter is last and @Optional(), so suites that construct the service by position are unchanged.
  • An end-to-end script (commerce-checkout-ledgers-e2e.mjs) completes a cart over REST and over GraphQL and reads the order back over the other surface, asserting the money matches; the discount is a seeded promotion the engine applies (stating an amount is refused since R3-07), and a refused discount fails the run rather than skipping the check.