ADR-0001. Single SaleOrder entity for cart and committed order
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-01-10 |
| Deciders | sale-team |
| Supersedes | — |
Context
- POS workflows treat "cart" and "order" as the same entity progressively committed: a barista builds a draft, hits checkout, then payment.
- Splitting into two entities (Cart + Order) requires copying state at checkout, complicates merge/split, and doubles the rooms WebSocket has to manage.
- We need a single source of truth for what the user is working on, with a clean lifecycle.
Decision
Use a single SaleOrder entity with a status field. DRAFT = cart (mutable items). PROCESSING / PARTIAL / COMPLETED = committed. CANCELLED = terminal.
State transitions are gated by canModifyItems() (DRAFT only), canCheckout() (DRAFT only), canRevertToCart() (PROCESSING only), canCancel() (any active).
Consequences
| Pros | Cons |
|---|---|
| Single entity = single source of truth | Status enum encodes state-machine logic — easy to mismatch |
| Merge / split work uniformly across statuses | DB queries that want "only orders" must filter status != DRAFT |
| WebSocket subscribers see one entity lifecycle | Audit reports must be careful with cancelled drafts |
| Mobile/POS UI doesn't need to copy state | Concurrent add-item requires explicit locking (ADR-0002) |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
Separate Cart + Order entities | Each entity has cleaner schema | Copy at checkout; double WS rooms; merge/split logic doubles | Implementation cost outweighs schema cleanliness |
SaleOrder + Cart view | Materialized DRAFT view | View complexity; same locking issues | Marginal benefit |
References
core/src/models/schemas/sale/sale-order/schema.tssale/src/services/sale.service.ts:createDraftOrdercore/src/models/schemas/sale/sale-order/constants.ts:SaleOrderStatuses