Fulfillment & money flow
What happens after an order is paid: how the item reaches the buyer, and how the money moves. Covers all three fulfillment methods — shipping (a carrier), same_day (an on-demand courier), meetup (in person) — and every cancel and refund path.
Four layers
| Layer | Column | Source of truth |
|---|---|---|
| Order state | orders.status | Cüte's order machine |
| Payment state | payments.status | Cüte, informed by the payment provider |
| Fulfillment state | fulfillments.status | Cüte, informed by the delivery gateway or the handoff |
| Payout state | orders.payout_status | Cüte's held-funds lifecycle |
The machine fires normalized events; webhook handlers stamp payments.provider_status_raw and fulfillments.provider_status_raw, so the provider's own words survive next to our reading of them.
The provider behind each layer is chosen per market — Stripe or Pagar.me for payment, Sendcloud or Melhor Envio for labels, a courier gateway for same-day. Nothing on this page is specific to one of them.
When the label is bought
When the seller starts shipping — not at payment_confirmed.
payment_confirmed
→ fulfillments.status = awaiting_seller
→ the buyer's shipping fee is held alongside the item price
→ no label yet
seller starts the shipment
→ createShipment → purchaseLabel → generateLabel → getLabel
→ fulfillments.gatewayShipmentId and labelUrl set
→ fulfillments.status = label_generated
→ the PDF opens
→ orders.shippedAt is STILL null — printing is not shipping
seller hands the parcel over
→ the carrier scans it
→ webhook → `seller_shipped`
→ orders.shippedAt = now, fulfillments.status = posted
→ the buyer sees "in transit"Why printing is not shipping. If shippedAt were set at print time, the ship-deadline guard would stop protecting the buyer: a seller who prints and never posts would hold the money indefinitely. Tying shippedAt to the carrier's own scan keeps the deadline honest.
Why not at payment time. Labels are bought from a funded gateway balance, so buying early locks money against orders that may never ship — and cancellations between payment and dispatch are common. Not every gateway refunds an unused label. Buying at dispatch is also what Mercado Livre, Etsy and Shopify do.
Each step is a separately journaled step in the durable saga, which is why they are separate gateway methods rather than one call: purchaseLabel is the balance debit and has to be independently resumable.
Happy path — shipping
No diagram code provided
Happy path — same-day
No diagram code provided
Happy path — meetup
The buyer's confirmation is the handoff. There is no separate seller step: one tap completes the order and releases the funds.
No diagram code provided
A meetup has no shipping to be late on, so the ship deadline never applies to it.
What the buyer pays, and where it goes
Buyer's total = item price + buyer protection fee + delivery fee (zero for meetup).
| Slice | Goes to | When |
|---|---|---|
| Item price | The seller's payment account | At capture, into the held bucket until release |
| Buyer protection fee | Cüte | At capture, released immediately |
| Delivery fee | Cüte | At capture; consumed by the label at dispatch. Refunded if the order never ships |
Cancellations and refunds
| Reason | Order status | Label bought | Buyer gets back | Seller balance |
|---|---|---|---|---|
payment_failed | pending_payment | no | nothing was captured | — |
payment_expired | pending_payment | no | nothing was captured | — |
buyer_cancel before payment | pending_payment | no | nothing was captured | — |
buyer_cancel after payment | active, not shipped | no | everything | held funds reversed |
seller_cancel | active, not shipped | no | everything | held funds reversed |
admin_cancel | active, not shipped | no | everything | held funds reversed |
ship_deadline_expired | active, not shipped | no | everything | held funds reversed |
dispute_resolved_refund | disputed | yes, consumed | item + protection fee. Not the delivery fee | held funds reversed |
dispute_resolved_seller | disputed | yes | nothing | held funds released to the seller |
For same-day, the delivery fee is refundable only while the courier has not been dispatched. After dispatch the courier is owed for the run either way, so a refund is the buyer's total minus the courier cost — an admin decision. For meetup there is no delivery fee to argue about.
orders.payout_status
| Stage | Value | Meaning |
|---|---|---|
| Just created | not_ready | Payment has not confirmed; nothing is held |
| Payment confirmed | hold | The item price sits in the seller's held bucket. Not withdrawable |
| Acceptance window passed, or the buyer accepted | releasable | Qualifies for release |
| Released | released | Moved held → available. The seller can request a payout |
| Dispute open | blocked | Frozen until the dispute closes |
Deadlines
| Timer | Window | Cron | Fires |
|---|---|---|---|
| PIX charge (BR) | 30 minutes (pixChargeExpiryMs) | expire-pending-payments | payment_expired → cancelled |
| Card anti-fraud | provider-managed | — | payment_confirmed or payment_failed |
| Ship deadline | 7 days from payment_confirmed (shipDeadlineMs) | overdue-orders | ship_deadline_expired → cancelled + refunded. Guarded by notShipped |
| Buyer acceptance | 2 days from delivery or handoff (acceptanceWindowDays) | auto-accept-orders | auto_accept_timeout → completed, funds released. Guarded by noActiveDispute |
Every window is a market config value, not a constant in the code.
Who can do what
| Transition | Event | Actor | Guard |
|---|---|---|---|
→ pending_payment | order created | system | seller eligible |
pending_payment → active | payment_confirmed | webhook | — |
pending_payment → cancelled | payment_failed / payment_expired / buyer_cancel | webhook / cron / buyer | — |
active (no state change) | seller_ready | seller / system | — |
active (no state change) | label_purchased / label_generated / seller_shipped | webhook / system | methodTakesEvent (shipping) |
active (no state change) | courier_dispatched | webhook / system | methodTakesEvent (same-day) |
active (no state change) | courier_in_transit | webhook / system | methodTakesEvent (shipping, same-day) |
active (no state change) | fulfillment_failed / fulfillment_paused | webhook / system | — |
active → awaiting_buyer_confirmation | delivery_confirmed | webhook / system | methodTakesEvent (shipping, same-day) |
active → awaiting_buyer_confirmation | handoff_confirmed | seller | methodTakesEvent (meetup) |
active → cancelled | ship_deadline_expired | cron / system / admin | notShipped |
active → cancelled | buyer_cancel / seller_cancel / admin_cancel | buyer / seller / admin | — |
awaiting_buyer_confirmation → completed | buyer_accepted | buyer | — |
awaiting_buyer_confirmation → completed | auto_accept_timeout | cron / system | noActiveDispute |
awaiting_buyer_confirmation → disputed | dispute_opened | buyer | canOpenDispute |
disputed → cancelled / completed | the five closures, second fire | system (the saga) | the matching settles* guard — see Dispute region |
Failure modes worth knowing
- The gateway balance is empty when the seller dispatches. The label purchase fails, the order stays
activeandfulfillments.statusstaysawaiting_seller. The seller sees an error; ops tops the balance up and the seller retries. - The label is generated but the carrier's scan webhook never arrives.
fulfillments.statusstayslabel_generatedandshippedAtstays null, so the ship-deadline cron can still cancel and refund. An admin can advance it by hand. - The seller posts on the last day. The ship deadline runs from
payment_confirmed, so a cron pass may land right after a genuine dispatch — which is exactly what thenotShippedguard is for. - The buyer never confirms. Auto-accept fires after the acceptance window, unless the dispute region is active, in which case the payout stays
blockeduntil it closes.