feat(order): checkout orders keep the cart's discounts and taxes (Q7-W1a)
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/): copiesCART_LINE/CART_SHIPPING/CARTadjustments andCART_LINE/CART_SHIPPINGtax lines ontoORDER_LINE/ORDER_SHIPPING/ORDERthrough the kernel ledger services (append, so the sign / reason / one-basis-per-rate rules apply to the copy), each stampedmetadata.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 andorder.placedinclude them), stampsorderLine.metadata.cartLineId. OnlyOrderCheckoutHandlerasks — 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.readTotalsContextpassesORDER-level adjustments asdocumentAdjustments(nothing wrote one before → no existing order moves).- New optional constructor param on
OrderServiceis last +@Optional()(suites constructing by position unchanged).
Tests
order-cart-ledger-carryover.service.spec.ts(5) and 3 new cases inorder.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 incore/bootstrap/tracer.ts, dependency typings).- E2E (written, not run as a suite):
tools/scripts/commerce-checkout-ledgers-e2e.mjs— REST complete → GraphQLorderread, and GraphQLcompleteCheckout→ REST read; asserts discount / tax / grand total equal the cart's. Shared harnesstools/scripts/commerce-wiring-harness.mjsfor the Q7 runs (needsE2E_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
OrderCartLedgerCarryovercopies 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 recordsmetadata.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.
OrderTotalsServicenow reads anetAmountstated as decimal text the same way it reads a number, matching the cart's reader for carried rows.- The new
OrderServiceconstructor 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.