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)

LayerLives inSource of truth
Order stateorders.statusCüte's order machine
Payment statepayments.statusCüte, informed by Pagar.me
Fulfillment statefulfillments.statusCüte, informed by ME / courier / local handoff
Payout stateorders.payout_statusCü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?

  1. Cüte funds its ME wallet from float. If 100 paid orders sit unshipped, that's R$ 100×label-cost locked.
  2. 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.
  3. Industry standard: Mercado Livre, Etsy, Shopify all buy at ship time.

Happy path — shipping

Happy path — same_day (Lalamove / Uber Direct)

Happy path — meetup

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:

SliceWho receivesWhen
Item priceSeller's Pagar.me recipientAt capture (held in seller's wallet, held state until released)
Buyer protection feeCüte's platform recipientAt capture, immediately released
Shipping/delivery feeCüte's platform recipientAt capture; consumed by ME label purchase at ship time. Refunded if order never ships.

All cancel + refund cases

Cancel reasonOrder status at cancelLabel bought?Buyer refundedSeller balance impact
payment_failed (anti-fraud declined, etc.)pending_paymentNoN/A — payment never capturedN/A
payment_expired (PIX 30-min timeout)pending_paymentNoN/AN/A
buyer_cancel (buyer abandons before payment)pending_paymentNoN/AN/A
buyer_cancel (buyer regrets after payment)active, shippedAt = NULLNoItem + protection fee + shipping fee (full refund)Held funds released back, removed from seller's wallet
seller_cancel (out of stock, etc.)active, shippedAt = NULLNoItem + protection fee + shipping fee (full refund)Held funds released back
admin_cancelactive, shippedAt = NULLNoFull refundHeld funds released back
ship_deadline_expired (5 biz days passed, no ship)active, shippedAt = NULLNoFull refundHeld funds released back
dispute_resolved_refund (admin sides with buyer)disputedYes, already usedItem + 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)disputedYesNoneHeld 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_dispatched or 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?

Stagepayout_statusWhat it means
Just creatednot_readyPayment hasn't confirmed; nothing held in seller's wallet yet.
Payment confirmedholdSeller's wallet has the item price in held bucket (held_balance). Cannot withdraw.
Delivered + acceptance window passed (or buyer accepts)releasableOrder qualifies for fund release; cron / buyer_accepted action will move it to released.
ReleasedreleasedFunds moved held → available in seller's wallet. Seller can now request a payout (Pagar.me transfer) via the wallet UI.
Dispute openblockedPayout frozen pending admin resolution.

Deadlines & timers

TimerWindowTriggerWhat fires
PIX payment30 minutesCron: expire-pending-paymentspayment_expired → order cancelled
CC anti-fraudPagar.me-managedPagar.me webhookpayment_confirmed or payment_failed
Ship deadline5 business days from payment_confirmedCron: overdue-ordersship_deadline_expired → order cancelled, buyer fully refunded
Buyer acceptance7 calendar days from delivery_confirmed / handoff_confirmedCron: auto-accept-orders (guarded by noActiveDispute)auto_accept_timeout → order completed, funds released

Who can do what, when

TransitionEventActorGuard
pending_payment(order created)system (checkout API)seller eligible
pending_paymentactivepayment_confirmedwebhook (Pagar.me)
pending_paymentcancelledpayment_failedwebhook
pending_paymentcancelledpayment_expiredcron
pending_paymentcancelledbuyer_cancelbuyer
activeactiveseller_readyseller / system
activeactivelabel_purchased / label_generatedwebhook / systemisShipping
activeactiveseller_shippedseller / webhook / systemisShipping (buys ME label, sets shippedAt)
activeactivecourier_dispatchedwebhook / systemisSameDay
activeactivecourier_in_transit / fulfillment_failed / fulfillment_pausedwebhook / system
activeawaiting_buyer_confirmationdelivery_confirmedwebhook / system
activeawaiting_buyer_confirmationhandoff_confirmedsellerisPickup
activecancelledship_deadline_expiredcron / system / adminnotShipped
activecancelledbuyer_cancel / seller_cancel / admin_cancelbuyer / seller / admin
awaiting_buyer_confirmationcompletedbuyer_acceptedbuyer
awaiting_buyer_confirmationcompletedauto_accept_timeoutcron / systemnoActiveDispute
awaiting_buyer_confirmationdisputeddispute_openedbuyercanOpenDispute (dispute region none)
disputedcancelleddispute_resolved_refund (second fire)system (saga, after the admin's decision)settlesResolvedRefund
disputedcompleteddispute_resolved_seller / dispute_resolved_partial / dispute_withdrawn / dispute_dismissed (second fire)systemthe matching settles* guard, see Dispute region

Failure modes worth knowing

  • Cüte's ME wallet is empty when seller clicks shipseller_shipped fails, order stays at active, fulfillments.status stays awaiting_seller. Seller sees error toast. Cüte ops needs to top up the ME wallet, then seller retries.
  • ME generates label but order.posted webhook never arrivesfulfillments.status stays label_generated (set at purchase time); orders.shippedAt stays null. The seller_shipped event (fired by order.posted) is what advances both. The notShipped ship-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 by notShipped guard, so this is fine.
  • Buyer never confirms after delivery → 2-day timer fires auto_accept_timeout (gated by noActiveDispute). If the dispute region is active, payout stays blocked until the dispute closes.
  • Buyer disputes during the acceptance windowdispute_opened flips order to disputed, payout_status = blocked. Auto-accept is disabled. Cron skips.