Appearance
Order lifecycle
The state machine
paid ──────► fulfilling ──────► fulfilled (terminal)
│ │
│ └────────────► failed
│ │
├──► queued ──► fulfilling │
│ │
└──────────────────────────────►└──► refunded (terminal)Legal transitions are a table, typed as Record<OrderState, ...> on purpose: if a state is added to the enum and not given a row, the file stops compiling. A new state must have its exits declared deliberately.
Three rules:
- Self-transitions are never legal.
paid → paidis always a bug (a double-processed webhook, a re-entrant worker) and must be loud. fulfilling → refundedis illegal. A stuck order routes throughfailedfirst, which keeps the audit trail honest: we did claim it, we just could not complete it.paid → failedis illegal. A pre-purchase determination that the order is unfulfillable still routes throughfulfilling.
Every write path that changes state goes through the assertion. The database compare-and-swap calls it before it writes.
Atomic vs deferred
| Atomic | Deferred | |
|---|---|---|
| Kinds | ticket, gift_card | credit_line |
| Timescale | Seconds | Days to weeks |
| Settlement | All at once | Staged: delivery, then window close |
| Refund daemon | Serves it | Does not |
The refund daemon implements exactly one model: if the buyer has nothing by the deadline, give the money back. That is right for a ticket and a gift card, and wrong for something expected to be undelivered for a fortnight. Filtering by kind keeps deferred settlement out of the atomic path, rather than teaching one daemon about both.
Deadlines
Every order carries a deadlineAt: the moment by which the buyer must have goods or their money back.
- Ticket → the showtime.
- Gift card → the supply expiry.
- Cash advance (
credit_line) → far out; settlement is staged instead.
There is deliberately no default. A wrong default here means the refund daemon either refunds everything immediately or never refunds at all.
The refund daemon
Sweeps orders in states that still owe the buyer (paid, queued, fulfilling, failed) whose deadline is within REFUND_LEAD_MINUTES (90).
Two properties matter more than they look:
Money moves before the state changes. If the refund succeeds and the state write then fails, the next run retries. The refund call is effectively idempotent on both rails: Stripe rejects a second full refund, and a crypto double-send is caught by the balance precheck and paged. The reverse order could mark an order refunded that never got money back.
A failed refund does not mark the order refunded. It pages [ACTION NEEDED] refund failed and leaves the order owing. Money is never silently kept.
A fulfilling order claimed seconds ago is not stuck. There is a grace window before it is declared dead, unless the deadline has already passed.
The intent lifecycle
Supply has its own machine, running alongside:
offered ──► matched ──► executing ──► completed
│ │ │
│ │ └──► execution_failed ──► refunded
└──► cancelled ◄──┘executing → cancelled is deliberately absent. Once redemption is under way the seller must not be able to pull the card out from under a buyer at a till. That is why balance-watch cancels before a match, never during execution (see Dual claim).
Fake-fulfilment guard
An order marked fulfilled with a confirmation URL the buyer cannot reach is money taken for nothing. It is also terminal: the refund daemon will never revisit it.
So marking fulfilled is refused when the link is unreachable (.invalid, reserved TLDs, RFC 2606 names), and refused when a fake driver would produce a terminal state on a rail carrying real money. queued is not terminal, so it remains refundable. That is why parking is the safe direction.
Liveness is judged per rail, and unknown is treated exactly like live. It means "the configuration does not tell me", which is not a safe place to hand out a dead link from.