Skip to content

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.

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_transitions record: 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 confirmed and 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-Key apply 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.

  • 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 --> [*]

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.