Stage M5 and Stage M6
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# ADR-0018: Orders snapshot everything they display
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-08-12
|
||||
|
||||
## Context
|
||||
|
||||
An order line points at a variant. The obvious implementation renders the order
|
||||
by joining through that pointer: read the variant, read its product, show the
|
||||
name and the price.
|
||||
|
||||
That works until the catalog moves, which it does constantly. In this system
|
||||
alone, a merchandiser can rename a product, reprice a variant, archive a
|
||||
colourway, retire an option value or unpublish the whole product — and ADR-0016
|
||||
guarantees the variant row survives precisely so those links do not break. But
|
||||
surviving is not the same as being unchanged. An order rendered by join shows
|
||||
today's name at today's price for something bought last March.
|
||||
|
||||
That is not a display bug. It is a receipt that disagrees with what the customer
|
||||
paid, which is a dispute, a refund calculation and possibly a legal record.
|
||||
|
||||
## Decision
|
||||
|
||||
**An order stores its own copy of everything it displays.** `OrderLine` carries
|
||||
`productName`, `variantTitle`, `sku`, `imageUrl`, `unitAmount`, `quantity` and
|
||||
`lineAmount`. `Order` carries the full shipping address as columns rather than a
|
||||
foreign key into the customer's address book, plus every money component:
|
||||
subtotal, discount, shipping, tax and total.
|
||||
|
||||
**`OrderLine.variantId` is kept, and is `onDelete: SetNull`.** It exists for
|
||||
reporting, returns and restocking — never for rendering. Losing the reporting
|
||||
link is survivable; losing the order line is not.
|
||||
|
||||
**The snapshot is taken in the shopper's language.** The cart composes a variant
|
||||
title from translated option values, and that composed string is what gets
|
||||
frozen. An order is a record of what was agreed, and what was agreed was shown
|
||||
in Vietnamese or English.
|
||||
|
||||
**Money components are stored even when zero.** `discountAmount` and
|
||||
`shippingAmount` are 0 until M7 and M9, but they are columns rather than absent
|
||||
fields, so a total is always the sum of parts someone can name.
|
||||
|
||||
## Consequences
|
||||
|
||||
An order stays readable and correct forever, through any catalog change. Reading
|
||||
one touches two tables and no catalog joins, which also makes the order history
|
||||
cheap.
|
||||
|
||||
Duplication is real: a product name lives once in the catalog and once per order
|
||||
line that ever contained it. That is the point — they are different facts that
|
||||
happen to share a value today.
|
||||
|
||||
Corrections become explicit. Fixing a typo in a product name does not
|
||||
retroactively edit anyone's receipt, and if an order genuinely needs amending,
|
||||
that has to be a deliberate operation with its own audit entry rather than a
|
||||
silent side effect of a catalog edit.
|
||||
|
||||
Analytics that groups by product must group on `variantId`, not on the snapshot
|
||||
name, or a renamed product will appear as two products.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Join to the catalog at read time.** Simplest, no duplication, and wrong for
|
||||
the reason above. Rejected.
|
||||
|
||||
**Snapshot into a JSON blob.** Fewer columns, and gives up every guarantee the
|
||||
database offers — no types, no constraints, no indexing, and a schema that
|
||||
drifts silently. Rejected; the fields are known and stable.
|
||||
|
||||
**Version the catalog and point at a version.** Fully correct and considerably
|
||||
more machinery: every product write creates an immutable revision, and every
|
||||
read has to resolve one. Worth it for a system where catalog history is itself a
|
||||
product. Here it would be a large amount of infrastructure to avoid copying
|
||||
seven columns.
|
||||
|
||||
**Snapshot only price, join the rest.** The hybrid is the worst option: it
|
||||
implies the other fields are safe to join when they are not, and the day someone
|
||||
renames a product the receipts change without anyone noticing.
|
||||
Reference in New Issue
Block a user