# ADR-0003 — Multi-unit pricing with base-unit-by-name and conversion-by-FK

> Produced by the Reversa **Detective** (phase: interpretation) · doc_level: `complete`
> Retroactive ADR reconstructed from Git history: MR!8 "Product | Updated to Implement Multi-Unit Pricing System" (`2a41d97`), plus "POS | Fixed product unit error" (`6bac17c`).

- **Status:** Accepted (as-built) 🟢
- **Confidence:** 🟢 CONFIRMED (mechanism) / 🟡 (rationale inferred)

## Context

Grocery products sell in multiple units (e.g. a single can vs a box of cans). The shop needed to sell the same product in different units at a price derived from the base price, without maintaining independent price lists per unit.

## Decision

- Each product has one **base unit** stored as a string on `products.unit`, populated from `Unit::all()->pluck('name','name')`.
- Alternative units are rows in `product_units` referencing `units.id` (`unit_id`) with a `conversion_qty` (how many base units the conversion unit contains) and an `is_default` flag.
- Selling in a conversion unit derives the price: `unitPrice = basePrice / conversion_qty` (guarded `conversion_qty > 0`), where `basePrice` is the customer-type price. See `Product::getPriceByCustomerType`.
- `syncProductUnits` replaces a product's unit set on save; the first `is_default`-flagged valid row wins.

## Consequences

- 🟢 One price definition (base) drives all units, avoiding per-unit price drift.
- 🟡 **Modelling inconsistency (GAP-U2):** base units are keyed by *name* (string) while conversions are keyed by *id* (FK). Renaming a unit desyncs existing base-unit strings on products.
- 🟢 `order_product` snapshots `unit_id` and `conversion_qty` per line, so historical receipts remain correct after catalogue changes.
- 🟢 The follow-up hotfix (`6bac17c`) shows the conversion path was fragile at first; the current guard (`conversion_qty > 0`) reflects that lesson.
