Skip to content

ADR-0001. Single SaleOrder entity for cart and committed order

FieldValue
StatusAccepted
Date2026-01-10
Deciderssale-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

ProsCons
Single entity = single source of truthStatus enum encodes state-machine logic — easy to mismatch
Merge / split work uniformly across statusesDB queries that want "only orders" must filter status != DRAFT
WebSocket subscribers see one entity lifecycleAudit reports must be careful with cancelled drafts
Mobile/POS UI doesn't need to copy stateConcurrent add-item requires explicit locking (ADR-0002)

Alternatives Considered

OptionProsConsWhy rejected
Separate Cart + Order entitiesEach entity has cleaner schemaCopy at checkout; double WS rooms; merge/split logic doublesImplementation cost outweighs schema cleanliness
SaleOrder + Cart viewMaterialized DRAFT viewView complexity; same locking issuesMarginal benefit

References

  • core/src/models/schemas/sale/sale-order/schema.ts
  • sale/src/services/sale.service.ts:createDraftOrder
  • core/src/models/schemas/sale/sale-order/constants.ts:SaleOrderStatuses

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.