Generated from: “Show how an order moves from cart to delivered and what can go wrong at each stage.”
View the Markdown
```meta
title: Order lifecycle
subtitle: How an order moves from cart to delivered, and what fails at each stage.
tag: DRAFT
```
An order is one row in the `orders` table with a `status` column. Every stage
below changes that column exactly once, and every failure has a named status
so support can see where the order stopped. The happy path takes six steps;
each step has one owner and one way to fail.
```callout
tone: note
title: Assumptions
body: "Single warehouse, card payment through a hosted payment gateway, one parcel carrier. Payment is captured at checkout, not at shipment. A cancelled or failed order is always refunded in full; partial refunds are out of scope."
```
## Order states
Each order holds one status at a time. The active states on the top row are
the happy path. The wait states hold the order while an outside party
(customer, warehouse, carrier) acts. The terminal states never change again,
and `REFUNDED` is the only exit for money already taken.
```state
id: order-states
dir: LR
states:
- { id: s0, col: 1, row: 1, kind: start }
- { id: cart, col: 2, row: 1, kind: wait, name: CART }
- { id: pending, col: 3, row: 1, kind: wait, name: PAYMENT_PENDING }
- { id: paid, col: 4, row: 1, kind: active, name: PAID }
- { id: reserved, col: 5, row: 1, kind: active, name: RESERVED }
- { id: packed, col: 6, row: 1, kind: active, name: PACKED }
- { id: shipped, col: 7, row: 1, kind: wait, name: SHIPPED }
- { id: delivered, col: 8, row: 1, kind: terminal, name: DELIVERED }
- { id: abandoned, col: 2, row: 2, kind: terminal, name: ABANDONED }
- { id: payfailed, col: 3, row: 2, kind: wait, name: PAYMENT_FAILED }
- { id: backordered, col: 5, row: 2, kind: wait, name: BACKORDERED }
- { id: returned, col: 7, row: 2, kind: active, name: RETURNED_TO_SENDER }
- { id: cancelled, col: 4, row: 3, kind: active, name: CANCELLED }
- { id: refunded, col: 6, row: 3, kind: terminal, name: REFUNDED }
transitions:
- s0 -> cart: add item
- cart -> pending: checkout
- cart -> abandoned: 7 days idle
- pending -> paid: payment captured
- pending -> payfailed: card declined
- payfailed -> pending: retry card
- payfailed -> abandoned: 3 declines
- paid -> reserved: stock reserved
- paid -> backordered: stock short
- backordered -> reserved: restock
- backordered -> cancelled: customer cancels
- reserved -> packed: pick complete
- reserved -> cancelled: customer cancels
- packed -> shipped: carrier scan
- shipped -> delivered: proof of delivery
- shipped -> returned: 3 failed attempts
- returned -> cancelled: received at warehouse
- cancelled -> refunded: refund settled
```
## Checkout
Checkout is the only stage where two systems must agree before the order
moves. Stock is reserved before the card is charged, so a decline never
leaves a paid order with no stock. The reservation is released on decline,
which is why a customer who retries can see "out of stock" on the second
attempt.
```sequence
id: checkout-sequence
actors:
- { id: Storefront, name: Storefront }
- { id: Orders, name: Orders API }
- { id: Inventory, name: Inventory }
- { id: Gateway, name: Payment Gateway, external: true }
messages:
- Storefront -> +Orders: POST /orders/{id}/checkout
- Orders -> +Inventory: reserve(sku, qty)
- alt: all lines in stock
- Inventory --> -Orders: reservation_id
- Orders -> +Gateway: capture(amount, card_token)
- alt: approved
- Gateway --> -Orders: charge_id
- Orders --> -Storefront: 200 PAID
- else: declined
- Gateway -x-> -Orders: decline_code
- Orders -> Inventory: release(reservation_id)
- Orders --> -Storefront: 402 PAYMENT_FAILED
- end
- else: a line is short
- Inventory -x-> -Orders: short(sku, available)
- Orders --> -Storefront: 409 BACKORDERED
- end
```
## Fulfillment and delivery failures
After payment the order is out of the customer's hands and every failure
needs an automatic decision. Retry when the next attempt is free. Ask the
customer when it costs them time. Refund when nothing else can move the
order.
```flow
id: fulfillment-failures
dir: TB
nodes:
- { id: start, col: 2, row: 1, kind: start, label: Order PAID }
- { id: stock, col: 2, row: 2, kind: decision, label: All lines in stock? }
- { id: eta, col: 3, row: 3, kind: decision, label: Restock ETA under 14 days? }
- { id: hold, col: 4, row: 3, kind: process, label: "Hold as BACKORDERED, email ETA" }
- { id: pick, col: 2, row: 4, kind: process, label: Pick and pack }
- { id: scan, col: 2, row: 5, kind: decision, label: Carrier scan within 24h? }
- { id: rebook, col: 3, row: 5, kind: process, label: "Rebook pickup, alert warehouse" }
- { id: attempt, col: 2, row: 6, kind: decision, label: Delivery attempt succeeded? }
- { id: retry, col: 3, row: 6, kind: decision, label: Fewer than 3 attempts? }
- { id: rts, col: 4, row: 6, kind: process, label: Return to sender }
- { id: done, col: 2, row: 7, kind: end, label: DELIVERED }
- { id: cancel, col: 4, row: 7, kind: process, label: Cancel and refund }
- { id: refunded, col: 4, row: 8, kind: end, label: REFUNDED }
edges:
- start -> stock
- stock -> pick: "yes"
- stock -x-> eta: "no"
- eta -> hold: "yes"
- eta -x-> cancel: "no"
- hold --> pick: restocked
- hold --> cancel: customer cancels
- pick -> scan
- scan -> attempt: "yes"
- scan -x-> rebook: "no"
- rebook --> scan
- attempt -> done: "yes"
- attempt -x-> retry: "no"
- retry --> attempt: "yes, next day"
- retry -x-> rts: "no"
- rts -> cancel: received at warehouse
- cancel -> refunded
```
## Failure catalog by stage
Each failure maps to one detection signal and one owner. Anything not in
this table is a bug in the state machine, not a support case.
```table
id: failure-catalog
columns: [Stage, What goes wrong, How we detect it, Recovery, Owner]
rows:
- [CART, Customer leaves without checkout, "No activity for 7 days", "Mark ABANDONED, send one reminder at 24h", Marketing]
- [CART, Price changes before checkout, "Line price differs from catalog at checkout", "Reprice cart, show a diff, ask to confirm", Storefront]
- [PAYMENT_PENDING, Card declined, "Gateway returns decline_code", "Release stock, allow 3 retries, then ABANDONED", Orders API]
- [PAYMENT_PENDING, Gateway timeout, "No response in 30s", "Query charge by idempotency key; never capture twice", Orders API]
- [PAYMENT_PENDING, Fraud hold, "Gateway risk score above threshold", "Hold 4h for manual review, then capture or cancel", Risk]
- [PAID, Stock short after payment, "Inventory returns short(sku)", "BACKORDERED if ETA under 14 days, else cancel and refund", Inventory]
- [RESERVED, Pick error or damaged item, "Picker rejects line in WMS", "Re-pick from another bin; if none, treat as stock short", Warehouse]
- [PACKED, Carrier never scans parcel, "No scan within 24h of label print", "Rebook pickup, alert warehouse lead", Warehouse]
- [SHIPPED, Delivery attempt fails, "Carrier event 'attempted'", "Retry next business day, up to 3 attempts", Carrier]
- [SHIPPED, Parcel lost in transit, "No scan for 5 days", "Open carrier claim, reship if stock, else refund", Support]
- [SHIPPED, Return to sender, "Carrier event 'returned'", "Cancel on receipt, refund in full", Warehouse]
- [DELIVERED, Customer reports not received, "Support ticket within 14 days", "Check proof of delivery; reship or refund at support's call", Support]
note: "Retry counts and day limits are the current production values; change them in config, not here."
```