Overview / Orders
ADR 0034
Orders are immutable commercial records; money movement lives in separate ledger transactions
AcceptedOrders
Context
An order records what was agreed — items, prices, addresses, the discount and tax that applied. The money for it moves over time and often more than once: an authorization/capture, platform and provider fees, later a partial refund or a chargeback. If the order row also carried mutable money state, or if its details could drift after creation, the commercial record would be neither a stable audit artifact nor tamper-resistant.
Decision
Treat an order as an immutable commercial record and keep all money movement in separate ledger transactions keyed to the order.
- Frozen snapshot. Order line items and core order fields are captured as immutable records: order lines reject changes outright ("order lines cannot be changed"), and the order's core fields (currency, customer, kind, reference, total, …) are marked immutable in the changeset. The order-detail
*_frozenfields (addresses, merchant name, discount/tax totals) hold the rest of the snapshot by convention — their changeset guard is currently commented out, so they are not hard-enforced. The order is the record of what was agreed, not a live document. - Money is a separate series. An order
has_many :transactions(ledger transactions); each money event — capture, fee, refund — is its own transaction, and reversing entries (refunds, fee adjustments) carry areference_transaction_idback to the original. "What was actually paid" is read from the transactions, not from the order row.
Consequences
- Positive: the order stays a stable, auditable statement of the deal, while the money history is an append-only series that can hold captures, multiple fees and partial refunds without ever mutating the order.
- Positive: it composes cleanly with settlement (ADR 0011), the fee model (ADR 0021/0022) and the ledger (ADR 0029) — each posts transactions against the order.
- Neutral / to revisit: immutability is hard-enforced on order lines and core order fields, but not on the order-detail
*_frozenfields (that changeset guard is commented out) — so the detail snapshot is convention-only today; re-enabling the guard would close the gap. None of it is enforced by database constraints. - Neutral / to revisit: because money lives in transactions, any consumer reporting "order paid / refunded amount" must aggregate transactions rather than read a column on the order.
Alternatives considered
- A mutable order row carrying running money fields: rejected — it loses history, invites drift and tampering, and cannot represent multiple partial money events; a frozen order + separate transactions preserves the record.
- One transaction per order: rejected — a real sale has a capture, fees, and possibly partial refunds or a chargeback;
has_many :transactionsmodels what actually happens. - Refund/adjust by editing the original transaction: rejected — money records are append-only; a refund is a new reversing transaction referencing the original (ADR 0011/0029), never an edit.