Design Rationale — Orders

Non-obvious design choices for the orders module — the WHY behind each decision.

Orders (locked 2026-06-10)

Own order_payment table, not POS's sale_payment

Decision: The orders schema has its own order_payment table for deposits and installments. It does not reuse pos.sale_payment.

Why: Orders take payment over time — a quote may receive a deposit, the order may receive partial payments, and fulfillment closes the balance. pos.sale_payment is a point-of-sale tender record tied to a single register session. Different lifecycle, different FK targets, different semantics.

Rejected: Reusing sale_payment — the POS tender model (register, shift, split-tender) doesn't apply to order deposits; forcing orders into it would require nullable columns that defeat the intent of both tables.


Decision: When an order is fulfilled at the POS, the resulting pos.sale is linked back via order_header.fulfilled_sale_id (text forward-ref). The order is not transformed into or replaced by a sale.

Why: An order and its fulfilling sale are different documents with different histories. The order may have a multi-week lifecycle with deposits, changes, and approvals; the sale is the moment of tender and tax finalization. Preserving both records keeps the complete history.

Guard: Do not "convert" orders to sales or merge their records at fulfillment. Link them.


order_type discriminator: quote / order / special_order / preorder

Decision: One order_header table with an order_type discriminator, not a table per order kind.

Why: Discriminator-over-sprawl — all are orders at different stages or kinds, sharing the same FK targets (customer, site, line items, payments, fulfillment). Variant behavior is captured by order_type-aware service logic.


Reserve-on-confirm, not reserve-on-quote or reserve-on-fulfillment

Decision: InventoryService reserves stock when an order status moves to confirmed. Quotes do not reserve; confirmed orders do.

Why: A quote is exploratory — reserving on every quote would lock inventory against hypothetical sales. Waiting until fulfillment risks overselling in the window between confirmation and pick. Confirmation is the right commitment point.


Decision: order_header.estimated_tax_cents and order_line.estimated_line_tax_cents are display figures only. The legally charged tax is finalized by Stripe Tax at POS fulfillment.

Why: Tax is determined at the point and time of sale (jurisdiction, exemptions, and item codes are verified then). An earlier estimate on a quote or order may be wrong — the customer's address could differ, they may present a tax cert at checkout, or rates may change. Only the POS sale_line_tax rows are the authoritative tax record.

Guard: Do not treat estimated_tax_cents as the charged amount — it is a UI convenience figure. pos.sale_line_tax is authoritative.


Last modified: Jun 17, 2026, 8:37 PM PT
On this page
Esc