Fulfillment & money flow
How fulfillment + shipping money flows through the system once an order is paid. Covers all three fulfillment methods (shipping carrier, same_day courier, meetup in-person) and every cancel/refund path.
The four layers (recap)
| Layer | Lives in | Source of truth |
|---|---|---|
| Order state | orders.status | Cüte's order machine |
| Payment state | payments.status | Cüte, informed by Pagar.me |
| Fulfillment state | fulfillments.status | Cüte, informed by ME / courier / local handoff |
| Payout state | orders.payout_status | Cüte (held-funds lifecycle) |
The order machine fires normalized events; webhook handlers stamp payments.provider_status_raw / fulfillments.provider_status_raw so the raw provider truth is always preserved alongside cüte's normalized read.
When is the Melhor Envio label bought?
When the seller clicks "Print label", NOT on payment_confirmed.
payment_confirmed
→ fulfillments.status = awaiting_seller
→ buyer's shipping fee held (buyer protection) alongside item price
→ NO label purchased yet
seller clicks "Print label" on the order card
→ cüte calls ME: addToCart → checkout (buys label from ME wallet)
→ generateLabel + printLabel
→ fulfillments.gatewayShipmentId set, fulfillments.labelUrl set
→ fulfillments.status = label_generated
→ PDF opens for the seller
→ orders.shippedAt is STILL null — printing != shipping
seller drops the parcel at a ME service point
→ ME scans it
→ ME webhook order.posted → cüte fires `seller_shipped` event
→ orders.shippedAt = now
→ fulfillments.status = posted
→ buyer sees "In transit"Why printing != shipping: if we set shippedAt = now the moment seller prints, the notShipped deadline guard would stop protecting the buyer. A seller who prints but never drops off would keep the money held indefinitely. Tying shippedAt to the carrier's scan keeps the 5-business-day deadline cron honest.
Why not on payment_confirmed?
- Cüte funds its ME wallet from float. If 100 paid orders sit unshipped, that's R$ 100×label-cost locked.
- Cancellations between payment + ship are common (seller out of stock, item damaged, buyer changed mind). Buying the label early means wasting ME wallet money on refunds — and ME does NOT always refund cancelled labels.
- Industry standard: Mercado Livre, Etsy, Shopify all buy at ship time.
Happy path — shipping
No diagram code provided
Happy path — same_day (Lalamove / Uber Direct)
No diagram code provided
Happy path — meetup
No diagram code provided
Money flow — what the buyer pays vs. what gets refunded
Buyer's total at checkout = item price + buyer protection fee + shipping/delivery fee (0 for meetup)
The Pagar.me split at capture:
| Slice | Who receives | When |
|---|---|---|
| Item price | Seller's Pagar.me recipient | At capture (held in seller's wallet, held state until released) |
| Buyer protection fee | Cüte's platform recipient | At capture, immediately released |
| Shipping/delivery fee | Cüte's platform recipient | At capture; consumed by ME label purchase at ship time. Refunded if order never ships. |
All cancel + refund cases
| Cancel reason | Order status at cancel | Label bought? | Buyer refunded | Seller balance impact |
|---|---|---|---|---|
payment_failed (anti-fraud declined, etc.) | pending_payment | No | N/A — payment never captured | N/A |
payment_expired (PIX 30-min timeout) | pending_payment | No | N/A | N/A |
buyer_cancel (buyer abandons before payment) | pending_payment | No | N/A | N/A |
buyer_cancel (buyer regrets after payment) | active, shippedAt = NULL | No | Item + protection fee + shipping fee (full refund) | Held funds released back, removed from seller's wallet |
seller_cancel (out of stock, etc.) | active, shippedAt = NULL | No | Item + protection fee + shipping fee (full refund) | Held funds released back |
admin_cancel | active, shippedAt = NULL | No | Full refund | Held funds released back |
ship_deadline_expired (5 biz days passed, no ship) | active, shippedAt = NULL | No | Full refund | Held funds released back |
dispute_resolved_refund (admin sides with buyer) | disputed | Yes, already used | Item + protection fee. Shipping fee is not refunded (label was consumed). | Held funds released back (item price returns to platform for refund) |
dispute_resolved_seller (admin sides with seller) | disputed | Yes | None | Held funds released to seller |
Meetup / same_day variant: there's no carrier label to consume. The "delivery fee" line on cancellations is:
- For meetup: 0 anyway
- For same_day: refundable only if the courier was never dispatched. Once dispatched (
fulfillments.status = courier_dispatchedor later), Lalamove charges Cüte for the courier's run regardless — refund the buyer minus the courier cost (admin decision).
What does orders.payout_status mean at each stage?
| Stage | payout_status | What it means |
|---|---|---|
| Just created | not_ready | Payment hasn't confirmed; nothing held in seller's wallet yet. |
| Payment confirmed | hold | Seller's wallet has the item price in held bucket (held_balance). Cannot withdraw. |
| Delivered + acceptance window passed (or buyer accepts) | releasable | Order qualifies for fund release; cron / buyer_accepted action will move it to released. |
| Released | released | Funds moved held → available in seller's wallet. Seller can now request a payout (Pagar.me transfer) via the wallet UI. |
| Dispute open | blocked | Payout frozen pending admin resolution. |
Deadlines & timers
| Timer | Window | Trigger | What fires |
|---|---|---|---|
| PIX payment | 30 minutes | Cron: expire-pending-payments | payment_expired → order cancelled |
| CC anti-fraud | Pagar.me-managed | Pagar.me webhook | payment_confirmed or payment_failed |
| Ship deadline | 5 business days from payment_confirmed | Cron: overdue-orders | ship_deadline_expired → order cancelled, buyer fully refunded |
| Buyer acceptance | 7 calendar days from delivery_confirmed / handoff_confirmed | Cron: auto-accept-orders (guarded by noActiveDispute) | auto_accept_timeout → order completed, funds released |
Who can do what, when
| Transition | Event | Actor | Guard |
|---|---|---|---|
→ pending_payment | (order created) | system (checkout API) | seller eligible |
pending_payment → active | payment_confirmed | webhook (Pagar.me) | — |
pending_payment → cancelled | payment_failed | webhook | — |
pending_payment → cancelled | payment_expired | cron | — |
pending_payment → cancelled | buyer_cancel | buyer | — |
active → active | seller_ready | seller / system | — |
active → active | label_purchased / label_generated | webhook / system | isShipping |
active → active | seller_shipped | seller / webhook / system | isShipping (buys ME label, sets shippedAt) |
active → active | courier_dispatched | webhook / system | isSameDay |
active → active | courier_in_transit / fulfillment_failed / fulfillment_paused | webhook / system | — |
active → awaiting_buyer_confirmation | delivery_confirmed | webhook / system | — |
active → awaiting_buyer_confirmation | handoff_confirmed | seller | isPickup |
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 (dispute region none) |
disputed → cancelled | dispute_resolved_refund (second fire) | system (saga, after the admin's decision) | settlesResolvedRefund |
disputed → completed | dispute_resolved_seller / dispute_resolved_partial / dispute_withdrawn / dispute_dismissed (second fire) | system | the matching settles* guard, see Dispute region |
Failure modes worth knowing
- Cüte's ME wallet is empty when seller clicks ship →
seller_shippedfails, order stays atactive, fulfillments.status staysawaiting_seller. Seller sees error toast. Cüte ops needs to top up the ME wallet, then seller retries. - ME generates label but
order.postedwebhook never arrives →fulfillments.statusstayslabel_generated(set at purchase time);orders.shippedAtstays null. Theseller_shippedevent (fired byorder.posted) is what advances both. ThenotShippedship-deadline cron still applies — if it fires before ME catches up, the order cancels and the buyer is refunded. Admin can override manually. - Seller marks shipped but the parcel never moves at ME → 5-biz-day deadline is from
payment_confirmed, not from ship. If seller ships on day 4, deadline still fires on day 5 by the cron — which would auto-cancel a shipped order. The cron is gated bynotShippedguard, so this is fine. - Buyer never confirms after delivery → 2-day timer fires
auto_accept_timeout(gated bynoActiveDispute). If the dispute region is active, payout staysblockeduntil the dispute closes. - Buyer disputes during the acceptance window →
dispute_openedflips order todisputed,payout_status = blocked. Auto-accept is disabled. Cron skips.