ADR-0005. Capture snapshot pricing (v1 + v2) on checkout into order items
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-20 |
| Deciders | sale-team, pricing-team |
| Supersedes | — |
Context
- Pricing rules in
@nx/pricingchange frequently — promotions, price corrections, fare overrides. - A POS-issued receipt must reflect the price at the moment of checkout — not the live price 3 weeks later when accounting reconciles.
- Pricing v1 returns simple price points. v2 returns a snapshot-rich object with rule trace, applied discount levels, currency conversion details.
Decision
At checkout (CheckoutService.checkout), sale calls PricingNetworkService.calculate() (v1) AND calculateV2() (v2) for the order's items. Both results are persisted on each SaleOrderItem:
unitPrice← v1 result (the displayed sale price)priceMetadata(jsonb) ← v2 snapshot with full pricing tracefareId,fareProvider← references to the rule that priced the line
The v2 snapshot is the source-of-truth for refunds, audit, and tax filings. v1 is what the customer sees.
Consequences
| Pros | Cons |
|---|---|
| Receipts are immutable post-checkout | Doubled pricing call latency |
| Full audit trail per line | priceMetadata jsonb grows large |
| Rule changes never affect past orders | Pricing service must be reachable at checkout |
| Refunds know exactly which discount applied | Refund logic must read v2 snapshot, not re-call pricing |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
Store only unitPrice (no snapshot) | Smaller storage | Can't audit which rule applied | Insufficient for refunds and tax |
| Re-call pricing on every read | Always "fresh" | Refunds reflect current price, not paid price | Wrong by design |
| Snapshot only at order completion | Smaller pre-payment storage | Misses pricing if order is cancelled before completion | Audit gap |
References
sale/src/services/checkout.service.tssale/src/services/pricing-network.service.tscore/src/models/schemas/sale/sale-item/schema.ts(priceMetadata,fareId,fareProvider)