Inkress ADRsarchitecture decisions
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 *_frozen fields (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 a reference_transaction_id back 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 *_frozen fields (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 :transactions models 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.
← 0033 Merchant payouts are reviewable, balance-validated financial requests materialized by a worker 0035 Disputes: a thin hold projection and internal triad in commerce-api; the chargeback books in the worker →