---
name: Escrow + proof-review order flow invariants
description: Authorization and money-movement rules for the model-directory escrow booking flow; how to keep status transitions safe.
---

Order flow: pending → accepted → paid → proof_uploaded → completed
(+ rejected/cancelled/refunded). Orders are created FREE. Funds are captured
only on model accept (auto best-effort if customer has balance), held in the
platform admin wallet (lowest-id admin) as escrow_in, and released on approve
(90% model earning / 10% commission, escrow_out) or refund.

**Invariants (each must be enforced server-side, not just in the UI):**
- `proof_uploaded` is reachable ONLY via `POST /bookings/:id/proof` (which writes
  the order_proofs row). It must NOT be settable through the generic
  `PATCH /bookings/:id/status` endpoint — otherwise the proof-photo gate is
  bypassed and admins have nothing to review.
- `completed` is reachable ONLY via admin proof review (`PATCH /admin/proofs/:id`),
  never through the generic status endpoint.
- Refunds are admin-only and pre-completion only. Enforce by keeping `cancelled`
  out of the transition list for captured states (`paid`, `proof_uploaded`); the
  only money-returning path out of a captured state is the admin-only `refunded`
  transition. A customer DELETE/cancel must 409 on a captured order.
- The generic status endpoint must gate admins through the same per-status actor
  table — do NOT short-circuit with a blanket `isAdmin ||`, or admins can drive
  proof_uploaded/completed and skip the dedicated endpoints.

**Why:** A blanket admin bypass plus `cancelled` allowed from `paid` let customers
self-refund and let providers/admins skip the proof gate. Caught in review.

**How to apply:** When touching the booking state machine, change the TRANSITIONS
graph and the per-status actor table together, and re-run the negative tests
(self-refund → 409, proof bypass → 403). Also: in multi-step DB transactions
(e.g. proof review updating proof row + booking row + wallets), a guarded update
that finds no row must THROW to roll back the whole tx — returning early from the
transaction callback COMMITS the partial work.
