72 lines
3.0 KiB
Markdown
72 lines
3.0 KiB
Markdown
# ADR-0016: Option values are retained when variants reference them
|
|
|
|
- **Status:** Accepted
|
|
- **Date:** 2026-08-12
|
|
|
|
## Context
|
|
|
|
A merchandiser removes the "Red" colourway from a product. What should happen to
|
|
the three Red variants and to the Red option value itself?
|
|
|
|
Three things reference them and each has a different claim:
|
|
|
|
- **Order history** — an order line points at a variant id, and reading that
|
|
order later needs "Red / M" to still mean something.
|
|
- **The variant matrix** — Red must stop generating combinations.
|
|
- **The storefront** — a shopper must not see a Red swatch that can never be
|
|
selected.
|
|
|
|
The database already takes a position: `ProductVariantOptionValue.optionValue`
|
|
is `onDelete: Restrict`, precisely so that deleting a value cannot silently
|
|
orphan or cascade into history.
|
|
|
|
## Decision
|
|
|
|
**Variants are archived, never deleted.** A removed combination sets
|
|
`status = ARCHIVED`. The row, its SKU and its option links all survive, so an
|
|
order placed last month still resolves.
|
|
|
|
**Option values are deleted only when nothing references them.** If any variant
|
|
— active or archived — still links to a value, the value is _retained_. It stops
|
|
appearing in the matrix and stops being offered, but it continues to exist so the
|
|
archived variants remain readable.
|
|
|
|
**The matrix is built from the operator's submitted option set, not from the
|
|
database.** Retained values would otherwise regenerate the very variants that
|
|
were just archived.
|
|
|
|
**The storefront filters option values to those offered by at least one active
|
|
variant.** Without this last step, a retained value renders as a permanently
|
|
disabled swatch with no explanation.
|
|
|
|
## Consequences
|
|
|
|
Order history stays intact and readable, which is the constraint that drove
|
|
everything else. Re-adding a removed colourway later reuses the retained value
|
|
and its translations rather than creating a duplicate.
|
|
|
|
The cost is that `product_option_values` accumulates rows that are invisible to
|
|
shoppers, and "delete" is not always literally a delete — a merchandiser who
|
|
inspects the database will find values they thought they removed. The schema
|
|
comment and this ADR are the mitigation; a future admin screen could surface
|
|
retained values explicitly.
|
|
|
|
This was found the hard way: the first implementation deleted values before
|
|
archiving variants and hit the `Restrict` constraint as a 500. The failure was
|
|
the schema doing its job.
|
|
|
|
## Alternatives considered
|
|
|
|
**Cascade the delete.** Removes the option value and its variant links, which
|
|
either breaks order lines or cascades into them. Rejected — this is the outcome
|
|
`Restrict` exists to prevent.
|
|
|
|
**Soft-delete option values with an `isActive` column.** Functionally similar to
|
|
retention, but adds a column and a filter to every catalog query for a case that
|
|
is already handled by "is any active variant offering this?". Rejected as
|
|
redundant state.
|
|
|
|
**Refuse to remove a value that has variants.** Simple and safe, but forces the
|
|
merchandiser to archive six variants by hand before they can drop a colourway —
|
|
pushing bookkeeping onto the person the tool exists to help.
|