Order lifecycle
An order moves through pending, confirmed, in_progress, ready and delivered, or ends in cancelled. Payment status (pending, paid, failed, refunded) is orthogonal: marking an order paid never reserves stock, and cancelling never claims a refund.
Transition matrix
Section titled “Transition matrix”Authorization decides who may act. The state machine decides whether the action is valid. Both must pass, and the full edit endpoint uses the same rules as the quick-status endpoint.
| From | Action and target | Who | Stock effect (Stage 2) |
|---|---|---|---|
| pending | Confirm to confirmed | staff and above | Reserve the complete eligible composition, or fail with a shortage and change nothing |
| confirmed | Start assembly to in_progress | staff and above | Recheck reservations; consume them exactly once, recording actual batch costs; refuse if any needs reallocation |
| in_progress | Mark ready | staff and above | None |
| ready | Complete delivery to delivered | staff and above | None |
| pending | Cancel | manager and above | None |
| confirmed | Cancel | manager and above | Release reservations |
| in_progress or ready | Cancel | manager and above | Retain consumed cost; no automatic restock |
| delivered or cancelled | Any ordinary change | nobody | Rejected; historical corrections use dedicated actions |
Since BRV-07 (14 September 2026) the matrix above is enforced in the code by OrderStatusChanger for both the quick-status route and the full-update route:
- Who is still decided by the policy: staff may only move forward, cancelling needs
orders.edit, viewers cannot change status. - Whether is decided by
OrderTransitions: only the adjacent moves above are legal. A manager asking for a backward move, a skipped step, or any change to a delivered or cancelled order gets the same refusal as staff, with the next available actions named, and nothing changes. - Effects happen in the same transaction as the status change and the
order_transitionsrecord: confirm reserves; start assembly rechecks every reservation and consumes it once, recording the batch unit cost at that moment; cancel releases reservations and leaves consumed stems consumed (ADR-009); ready and delivered touch nothing. If any part fails, everything rolls back. - Assembly recheck (rule B14): if a reserved batch has since expired, been quarantined, been removed, or has fewer stems on hand than reserved, assembly is refused with “N reservations need reallocation before assembly” and nothing is consumed. The order page and the Attention page show the same problems; the resolution is to amend the order, press “Reserve again from eligible batches” (which releases and re-reserves FEFO from batches that are eligible now, or refuses with the shortage leaving the old reservations in place), or cancel it (BRV-08, BRV-11).
- Confirmed orders without reservations cannot start assembly. A legacy order (confirmed before reservations existed: its migrated revision 1 was recorded at
confirmedand it has no confirm transition) is reserved explicitly with “Reserve again” on the order or Attention page, under the same eligibility rules. Any other confirmed order without allocations has lost them (Order::hasLostReservations()): assembly, re-reserving and edits are refused with a message saying the reservations are missing, the order page and the Attention page say so and offer no “Reserve again”, and reconciliation reports it as corruption. Cancelling remains possible. Until 4 October 2026 assembly reserved any such order on the spot, which would have hidden lost reservations (S2-F02). - Repeats with the same
Idempotency-Keyapply once; repeating the last action without a key is a no-op. Payment status is orthogonal: marking paid reserves nothing, cancelling refunds nothing.
The order page shows the status history (who, when, why) below the reservations.
Amendments (since BRV-08)
Section titled “Amendments (since BRV-08)”- A pending order is edited freely; each accepted change is a new priced revision and nothing is reserved.
- A confirmed order is edited in one transaction: the row is locked, the version is re-checked under the lock, the new revision is recorded, the previous reservations are released and the whole new requirement is reserved again FEFO for the (possibly new) delivery cutoff. Amending down releases the difference; amending up beyond available stock, or moving the date past a reserved batch’s expiry with no eligible replacement, is refused with the shortage and the previous revision and reservations stand exactly as they were.
- From in progress on, items, delivery date, time, slot and fee are frozen; notes and payment fields still change.
- Orders with reservation or status history are never deleted; they are cancelled. Cancelling twice is harmless. There is no stem recovery after cancelling an assembled order in this release (ADR-009).
- Races are settled by the row lock: an edit and an assembly submitted together end in exactly one of two consistent states (edit applied and then consumed, or assembly first and the edit refused as stale). The race suite proves both this and cancellation versus assembly. Amendments follow ADR-010: pending and confirmed orders can change, from in_progress they are frozen.
stateDiagram-v2 [*] --> pending pending --> confirmed: confirm (reserve) confirmed --> in_progress: start assembly (consume) in_progress --> ready ready --> delivered pending --> cancelled confirmed --> cancelled: release reservations in_progress --> cancelled: keep consumed cost ready --> cancelled: keep consumed cost delivered --> [*] cancelled --> [*]Golden scenario
Section titled “Golden scenario”The Stage 2 acceptance scenario, frozen on 7 September 2026 in Europe/Kyiv, all prices in UAH:
- Opening stock: 10 roses in batch R1 (expires 8 September, cost 20), 10 roses in R2 (expires 14 September, cost 30), 5 tulips in T1 (expires 10 September, cost 15). Rose sale price 50.
- A catalogue bouquet is 3 roses, 1 tulip and packaging at 200 including packaging; known packaging cost 10.
- Order A is two bouquets: subtotal 400 plus delivery 100 equals 500. Order B is 15 standalone roses: 750, no delivery.
| Step | Action | Roses on hand | Reserved | Free | Required result |
|---|---|---|---|---|---|
| 1 | Receive opening stock | 20 | 0 | 20 | Receipt movements match batches |
| 2 | Create A pending | 20 | 0 | 20 | Total 500, no reservation |
| 3 | Confirm A | 20 | 6 | 14 | 6 roses from R1 and 2 tulips reserved |
| 4 | Try to confirm B | 20 | 6 | 14 | Shortage of 1 rose; B stays pending, nothing allocated |
| 5 | Amend A to one bouquet | 20 | 3 | 17 | Total 300; 3 roses and 1 tulip released |
| 6 | Confirm B | 20 | 18 | 2 | 7 from R1 and 8 from R2 reserved |
| 7 | Cancel A before assembly | 20 | 15 | 5 | A’s 3 roses and 1 tulip released; physical stock unchanged |
| 8 | Start assembly for B | 5 | 0 | 5 | B’s 7 R1 and 8 R2 consumed once; allocations not reshuffled |
| 9 | Mark B ready, then delivered, then retry | 5 | 0 | 5 | No additional deduction or duplicate movement |
| 10 | Reconcile | 5 | 0 | 5 | R1 has 3, R2 has 2, all 5 tulips remain; projections match the ledger |
B’s actual rose cost is 7 × 20 + 8 × 30 = 380. Material profit is 750 − 380 = 370, material margin 49.33 %, markup 97.37 %. Allocations are not reoptimised after A’s cancellation; the freed R1 stems wait for the next allocation. Writing off the 2 remaining R1 roses afterwards leaves 3 roses and books a loss of 40 without touching B’s cost. Values accepted in BRV-02 (13 September 2026).
G1, storefront packaging (Stage 3A). The same bouquet sold at 200 without packaging, ordered from the storefront twice with packaging at 30 and delivery at 100: each line snapshots the packaging and charges it per item, so the total is 2 × (200 + 30) + 100 = 560. Resaving the order after the bouquet or packaging price changes keeps 560; changing the quantity to one gives 330. Proven in tests/Feature/OrderPackagingTest.php.