Files
web_sport/docs/adr/0003-product-and-productvariant-as-separate-entities.md
T

40 lines
1.8 KiB
Markdown

# ADR-0003: Product and ProductVariant as separate entities
- **Status:** Accepted
- **Date:** 2026-08-11
## Context
A "Running Shirt" in black, size M is a different physical good from the same shirt in
white, size L: different barcode, different stock, potentially different price. Modelling
`sizes: string[]` and `colors: string[]` on a product makes every one of those facts
unrepresentable.
## Decision
`Product` is the marketing entity — it has a name, a slug and a page, and deliberately
has no SKU, no price and no stock. It owns an ordered list of `ProductOption`s (Colour, Size),
each owning ordered `ProductOptionValue`s. Every purchasable combination is a
`ProductVariant` with its own SKU, price, sale price, barcode, weight and stock.
`ProductVariantOptionValue` resolves a variant to exactly one value per option, with
`@@id([variantId, optionId])` enforcing at the database level that a variant cannot have two
colours. Stock lives in `StockLevel` keyed by (variant, location), never on the variant row.
## Consequences
Cart lines, order lines, stock movements and marketplace listings all reference a
variant id — the same granularity Shopee, Lazada, TikTok Shop, ERP and POS systems use, so
integrations map 1:1 instead of needing a translation layer. Adding a third option (width, fit)
is data, not a migration.
The cost is real: the PDP must resolve option selections to a variant, and the admin needs a
variant-matrix editor rather than two text inputs. That cost is paid once and is the reason the
model survives contact with a warehouse.
## Alternatives considered
Size/colour as columns on Product — rejected: cannot express per-combination stock
or price, which is the entire job. A single flat SKU table with no product grouping — rejected:
there would be nothing to hang a product page, gallery or description on.