Order lifecycle

The orders row owns business state only: status, payout_status, fulfillment_method. What the payment provider thinks lives on a 1:1 payments row; what the carrier or courier thinks lives on a 1:1 fulfillments row. The machine fires normalized events, and webhook handlers stamp payments.provider_status_raw / fulfillments.provider_status_raw, so the provider's own trail survives next to our normalized reading of it.

For the deeper dive on shipping money flow, label-buy timing, and cancellation refunds, see Fulfillment & money flow.

Canonical statuses

StatusMeaning
pending_paymentOrder created, payment not confirmed. The listing is reserved.
activePayment confirmed. Seller is fulfilling. Splits pre/post-ship by orders.shippedAt.
awaiting_buyer_confirmationDelivered or handed off. The buyer's acceptance window is running (acceptanceWindowDays, 2 in both live markets).
completedFunds released. The listing moves reserved → sold.
cancelledTerminal cancel. The listing moves reserved → available.
disputedBuyer 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.

MethodInitial payments.statusConfirmationTimeout
PIX (BR)pendingProvider webhook after the buyer scans the QR30 min, then a cron cancels
Credit cardanalyzingProvider webhook once anti-fraud passesthe provider's own timeout

In both cases the listing is reserved immediately at order creation, before payment confirms. If payment fails or expires, a cron releases it back to available.

Happy path — shipping (carrier)

The standard flow when the seller ships with a carrier.

LegendLiveWaitingCompleted

Happy path — same-day (Lalamove / Uber Direct)

An on-demand courier, dispatched once the seller marks the parcel ready.

LegendLiveWaitingCompleted

Happy path — meetup

In-person handoff. Buyer and seller agree a time and place in chat, and the buyer confirms the handoff in one tap.

LegendLiveWaitingCompleted

Dispute path

Buyer raises a dispute during the 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.

LegendWaitingCompletedCancelled / failedDecision

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.

LegendLiveWaitingCancelled / failed
StageWho can cancelTrigger / reason
pending_paymentbuyer, cron (payment_expired), webhook (payment_failed)Buyer abandons / payment fails / 30-min PIX timeout
active, shippedAt = nullbuyer, seller, admin, cron (ship_deadline_expired)Manual cancel, or the ship deadline missed
active, shippedAt set—Cannot cancel once in transit; only dispute_opened after delivery
disputedthe 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.

LegendLiveWaitingCompletedCancelled / failed

Guards

GuardConditionUsed on
methodTakesEventthe method's traits take the event (methodTakes, fulfillments/fulfillment.methods.ts)every method-owned event: the label events and seller_shipped (shipping), courier_dispatched (same-day), courier_in_transit and delivery_confirmed (both), handoff_confirmed (meetup); ship_deadline_expired where a deadline runs
notShippedorders.shippedAt IS NULLship_deadline_expired (don't auto-cancel an in-transit order)
noActiveDisputethe dispute region is none or a closureauto_accept_timeout
canOpenDisputethe dispute region is none and the event is fired by a buyer, seller or admindispute_opened
settlesResolvedRefund / settlesResolvedSeller / settlesResolvedPartial / settlesWithdrawn / settlesDismissedthe 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, asksSellerthe event's role / partythe dispute region's own edges

See packages/commerce/orders/order.status-projection.ts for the full list.

Listing status follows the order

Order statusListing status
pending_payment / active / awaiting_buyer_confirmation / disputedreserved
completedsold
cancelledavailable

Who changes the status, and when

TransitionEventActorTriggerNotes
→ pending_payment(order created)system (checkout API)Buyer places orderItem: available → reserved; payment + fulfillment rows created
pending_payment → activepayment_confirmedwebhookPayment settledpayments.status = paid, payout_status = hold, fulfillments.status = awaiting_seller; seller's held funds credited
pending_payment → cancelledpayment_failedwebhookAnti-fraud declined / payment failedItem released
pending_payment → cancelledpayment_expiredcronThe charge expiredListing released
pending_payment → cancelledbuyer_cancelbuyerBuyer abandons checkoutItem released
active (no state change)label_purchased / label_generatedsystem / webhookSeller clicks "Print label" — see FulfillmentSets fulfillments.gatewayShipmentId, labelUrl. orders.shippedAt still null.
active (no state change)seller_shippedwebhookCarrier records the parcel as postedorders.shippedAt = now, fulfillments.status = posted
active (no state change)courier_dispatched / courier_in_transitwebhookSame-day courier flowfulfillments.status = courier_dispatched / in_transit
active → awaiting_buyer_confirmationdelivery_confirmedwebhookCarrier marks it deliveredacceptByAt = now + the acceptance window, fulfillments.status = delivered
active → awaiting_buyer_confirmationhandoff_confirmedbuyerBuyer confirms the meetup handofffulfillments.status = handoff_confirmed, acceptByAt = now + the acceptance window
active → cancelledship_deadline_expiredcronShip deadline passed, nothing postedRefund issued, listing released. Guarded by notShipped.
active → cancelledbuyer_cancel / seller_cancel / admin_cancelbuyer / seller / adminManualRefund + item released
awaiting_buyer_confirmation → completedbuyer_acceptedbuyerBuyer clicks "Confirm receipt"payout_status = released, item: reserved → sold
awaiting_buyer_confirmation → completedauto_accept_timeoutcron2 days post-delivery, no disputepayout_status = released. Guarded by noActiveDispute.
awaiting_buyer_confirmation → disputeddispute_openedbuyerBuyer raises disputeDispute 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_addedadmin / the awaited partyCase workOnly the dispute region moves
disputed (no state change)any of the five closures, first fireadmin (or buyer withdrawing)The decisionDispute region enters its closure; order and payout wait for the saga
disputed → cancelleddispute_resolved_refund, second firesystem (saga, after the settlement claim)Dispute resolved for buyerBuyer refunded, payments.status = refunded
disputed → completeddispute_resolved_seller / dispute_withdrawn / dispute_dismissed, second firesystemDispute closed in the seller's favourpayout_status = released
disputed → completeddispute_resolved_partial, second firesystemSplit decisionpayments.status = partially_refunded, payout_status = released minus the refunded portion

Deadlines & timers

TimerWindowOwnerWhat happens at zero
PIX payment (BR)30 minutes (pixChargeExpiryMs)cron (expire-pending-payments)payment_expired → cancelled
Card anti-fraudprovider-managedthe payment providerA webhook confirms or fails the payment
Ship deadline7 days from payment_confirmed (shipDeadlineMs)cron (overdue-orders sweep)ship_deadline_expired → cancelled + refund (gated by notShipped)
Auto-accept2 days from delivery or handoff (acceptanceWindowDays)cron (auto-accept-orders)auto_accept_timeout → completed + funds released (gated by noActiveDispute)