Dispute resolution allows a buyer to flag a problem with a delivered or completed offer. An admin then resolves the dispute with one of three outcomes: full payment to talent, full refund to brand, or a partial resolution.
DELIVERED or COMPLETED
↓ Buyer opens dispute
DISPUTED
↓ Admin resolves
COMPLETED (with resolution note)
A dispute can be opened from either DELIVERED (before the buyer has approved) or COMPLETED (after approval, if a problem is discovered later).
Who: The buyer (offer's buyerAccountId)
Endpoint: POST /api/offers/dispute
Required fields: offerId and reason
What happens:
DELIVERED or COMPLETEDDISPUTED via updateOfferStatusDISPUTE_OPENED notification for the seller (routed through agency if applicable)DISPUTE_OPENED notification for each admin account listed in OFFER_ADMIN_NOTIFY_ACCOUNT_IDSImplementation: apps/zooly-app/app/api/offers/dispute/route.ts
Admin account IDs are read from the OFFER_ADMIN_NOTIFY_ACCOUNT_IDS environment variable (comma-separated). Each admin receives a separate notification.
Who: Admin only (via assertAdmin)
Endpoint: POST /api/offers/admin/resolve-dispute
Required fields: offerId, resolution
Resolution types:
| Resolution | What happens | Financial outcome |
|---|---|---|
talent_wins | Calls releaseOfferEscrowForOffer | Full payment released to talent |
brand_wins | Calls processRefund on paymentStripeId | Full refund to brand |
partial | Calls processRefund, then transitions with partial amount note | Full refund + record of partial amount owed to talent |
Releases the escrow by calling releaseOfferEscrowForOffer(offerId, buyerAccountId) from @zooly/srv-stripe-payment. This creates share tracking records and the daily payout daemon will handle the actual Stripe Connect transfer.
Requires offer.paymentStripeId to exist. Calls processRefund(paymentStripeId) from @zooly/srv-stripe-payment for a full refund. Transitions the offer to COMPLETED with a note indicating full refund.
Requires partialAmountCent in the request body. Processes a full refund via processRefund, then transitions the offer to COMPLETED with a note recording the partial amount. The actual partial payment to talent is handled out-of-band in V1.
Partial resolution in V1 uses a full Stripe refund followed by a manual or future automated partial payment. A native Stripe partial refund can be implemented in a future iteration.
All three resolution types create DISPUTE_RESOLVED notifications for both the seller and buyer with resolution-specific messages.
Implementation: apps/zooly-app/app/api/offers/admin/resolve-dispute/route.ts
| Function | Package | Purpose |
|---|---|---|
getOfferById | @zooly/db | Load offer for validation |
updateOfferStatus | @zooly/db | Transition to DISPUTED or COMPLETED |
createNotification | @zooly/db | Create notifications for all parties |
getNotificationRecipient | @zooly/db | Agency routing for seller notifications |
releaseOfferEscrowForOffer | @zooly/srv-stripe-payment | Release escrowed funds to talent |
processRefund | @zooly/srv-stripe-payment | Stripe refund processing |