Order lifecycle
The orders row owns business state only: status, payout_status, fulfillment_method. Payment provider truth (Pagar.me) lives on a 1:1 payments row; carrier/courier truth (ME, Lalamove, meetup PIN) lives on a 1:1 fulfillments row. The order machine fires normalized events; webhook handlers stamp payments.provider_status_raw / fulfillments.provider_status_raw so the raw trail is preserved alongside cüte's normalized read.
For the deeper dive on shipping money flow, label-buy timing, and cancellation refunds, see Fulfillment & money flow.
Canonical statuses
| Status | Meaning |
|---|---|
pending_payment | Order created, payment not yet confirmed. Item is reserved. |
active | Payment confirmed. Seller is fulfilling. Splits pre/post-ship by orders.shippedAt. |
awaiting_buyer_confirmation | Delivered or handed off. 2-day buyer acceptance window. |
completed | Funds released. Item moves reserved → sold. |
cancelled | Terminal cancel. Item moves reserved → available. |
disputed | Buyer raised a dispute during the acceptance window. payout_status = blocked; the dispute region is active. |
Fulfillment method (shipping | same_day | meetup) and payment status (pending | analyzing | paid | failed | expired | refunded | chargeback) are independent dimensions — see the diagrams below for how they fan out.
Payment flow (PIX vs credit card)
What happens when a buyer clicks "place order" — the two payment paths diverge here.
| Method | Initial payments.status | Confirmation | Timeout |
|---|---|---|---|
| PIX | pending | Webhook order.paid after buyer scans QR | 30 min, then cron cancels |
| Credit card | analyzing | Webhook order.paid after anti-fraud passes | Pagar.me timeout |
In both cases the item is reserved immediately at order creation (before payment confirms). If payment fails or expires, the cron releases the item back to available.
Happy path — shipping (carrier)
The standard order flow when the seller ships via a carrier (ME).
No diagram code provided
Happy path — same-day (Lalamove / Uber Direct)
On-demand courier dispatched by cüte once the seller marks the parcel ready.
No diagram code provided
Happy path — meetup
In-person handoff. Buyer and seller coordinate via chat; seller enters the buyer's 4-digit PIN to confirm handoff.
No diagram code provided
Dispute path
Buyer raises a dispute during the 2-day acceptance window. The dispute itself is the machine's fifth region, mirroring disputes.status (none until a row exists): triage, party questions and the decision all move that region, and every move is an order event with a dispute from/to pair on order_events. A closure event is fired twice: the deciding side records it on the region, and the saga fires it again (role: system) after the settlement claim to move the order and the payout. The region, its role rules and the money fence are on the dispute region page.
No diagram code provided
Cancellation paths
Orders can be cancelled at two distinct stages — pre-payment, or after payment but before shipping. Once an order is shipped (active + shippedAt set) or delivered, cancellation gives way to the dispute flow.
No diagram code provided
| Stage | Who can cancel | Trigger / reason |
|---|---|---|
pending_payment | buyer, cron (payment_expired), webhook (payment_failed) | Buyer abandons / payment fails / 30-min PIX timeout |
active, shippedAt = null | buyer, seller, admin, cron (ship_deadline_expired) | Manual cancel, or 5 biz-day fulfillment deadline missed |
active, shippedAt set | — | Cannot cancel once in transit; only dispute_opened after delivery |
disputed | the saga, after an admin resolves for the buyer (dispute_resolved_refund) | Dispute resolved in buyer's favor |
Full state diagram
All possible transitions of the order region; the payment, fulfillment, payout and dispute regions move alongside it.
No diagram code provided
Guards
| Guard | Condition | Used on |
|---|---|---|
isShipping | fulfillmentMethod === "shipping" | label_purchased, label_generated, seller_shipped |
isSameDay | fulfillmentMethod === "same_day" | courier_dispatched, courier_in_transit |
isMeetup | fulfillmentMethod === "meetup" | handoff_confirmed, the one-tap buyer_accepted |
notShipped | orders.shippedAt IS NULL | ship_deadline_expired (don't auto-cancel an in-transit order) |
noActiveDispute | the dispute region is none or a closure | auto_accept_timeout |
canOpenDispute | the dispute region is none and the event is fired by a buyer, seller or admin | dispute_opened |
settlesResolvedRefund / settlesResolvedSeller / settlesResolvedPartial / settlesWithdrawn / settlesDismissed | the dispute region already holds the matching closure and the event is fired by system (the saga's second fire) | the five dispute closures on the order and payout regions |
byAdmin, byBuyerOrAdmin, bySellerOrAdmin, byBuyer, bySeller, asksBuyer, asksSeller | the event's role / party | the dispute region's own edges |
See packages/commerce/orders/order.status-projection.ts for the full list.
Item status sync
| Order status | Item status |
|---|---|
pending_payment / active / awaiting_buyer_confirmation / disputed | reserved |
completed | sold |
cancelled | available |
Who changes the status, and when
| Transition | Event | Actor | Trigger | Notes |
|---|---|---|---|---|
→ pending_payment | (order created) | system (checkout API) | Buyer places order | Item: available → reserved; payment + fulfillment rows created |
pending_payment → active | payment_confirmed | webhook (Pagar.me) | PIX paid / CC anti-fraud passes | payments.status = paid, payout_status = hold, fulfillments.status = awaiting_seller; seller's held funds credited |
pending_payment → cancelled | payment_failed | webhook | Anti-fraud declined / payment failed | Item released |
pending_payment → cancelled | payment_expired | cron | 30-min PIX timeout | Item released |
pending_payment → cancelled | buyer_cancel | buyer | Buyer abandons checkout | Item released |
active (no state change) | label_purchased / label_generated | system / webhook (ME) | Seller clicks "Print label" — see Fulfillment | Sets fulfillments.gatewayShipmentId, labelUrl. orders.shippedAt still null. |
active (no state change) | seller_shipped | webhook (ME order.posted) | Seller drops parcel at carrier | orders.shippedAt = now, fulfillments.status = posted |
active (no state change) | courier_dispatched / courier_in_transit | webhook (Lalamove) | Same-day courier flow | fulfillments.status = courier_dispatched / in_transit |
active → awaiting_buyer_confirmation | delivery_confirmed | webhook (ME / Lalamove) | Carrier marks delivered | acceptByAt = now + 2 days, fulfillments.status = delivered |
active → awaiting_buyer_confirmation | handoff_confirmed | seller | Seller enters buyer's PIN (meetup) | fulfillments.status = handoff_confirmed, acceptByAt = now + 2 days |
active → cancelled | ship_deadline_expired | cron | 5 biz days passed, not shipped | Refund issued, item released. Guarded by notShipped. |
active → cancelled | buyer_cancel / seller_cancel / admin_cancel | buyer / seller / admin | Manual | Refund + item released |
awaiting_buyer_confirmation → completed | buyer_accepted | buyer | Buyer clicks "Confirm receipt" | payout_status = released, item: reserved → sold |
awaiting_buyer_confirmation → completed | auto_accept_timeout | cron | 2 days post-delivery, no dispute | payout_status = released. Guarded by noActiveDispute. |
awaiting_buyer_confirmation → disputed | dispute_opened | buyer | Buyer raises dispute | Dispute region none → open; auto-accept blocked, payout_status = blocked. The open fence in order.claims.ts refuses it once a settlement claim is standing. |
disputed (no state change) | dispute_triaged / dispute_party_asked / dispute_party_responded / dispute_evidence_added | admin / the awaited party | Case work | Only the dispute region moves |
disputed (no state change) | any of the five closures, first fire | admin (or buyer withdrawing) | The decision | Dispute region enters its closure; order and payout wait for the saga |
disputed → cancelled | dispute_resolved_refund, second fire | system (saga, after the settlement claim) | Dispute resolved for buyer | Buyer refunded, payments.status = refunded |
disputed → completed | dispute_resolved_seller / dispute_withdrawn / dispute_dismissed, second fire | system | Dispute closed in the seller's favour | payout_status = released |
disputed → completed | dispute_resolved_partial, second fire | system | Split decision | payments.status = partially_refunded, payout_status = released minus the refunded portion |
Deadlines & timers
| Timer | Window | Owner | What happens at zero |
|---|---|---|---|
| PIX payment | 30 minutes | cron (expire-pending-payments) | payment_expired → cancelled |
| CC anti-fraud | Pagar.me-managed | Pagar.me | Webhook fires either order.paid or order.payment_failed |
| Fulfillment / ship deadline | 5 business days from payment_confirmed | cron (overdue-orders sweep) | ship_deadline_expired → cancelled + refund (gated by notShipped) |
| Auto-accept | 7 calendar days from delivery / handoff | cron (auto-accept-orders) | auto_accept_timeout → completed + funds released (gated by noActiveDispute) |