Disputes & Refunds
Things don’t always go smoothly. Sometimes sellers can’t ship, buyers change their mind, or items arrive damaged. This page covers how we handle these situations — from simple cancellations to complex dispute resolution.
We’ll cover two related but distinct systems:
- Refunds: Getting money back to buyers
- Disputes: Resolving problems between buyers and sellers
Refund System
Refunds are the mechanism for returning money to buyers. They can be triggered by cancellations, dispute resolutions, or manual admin actions.
When Can Refunds Happen?
Not all order statuses allow refunds. Here’s a quick reference:
Refundable
These statuses allow refunds to be processed:
| Status | Typical refund reason |
|---|---|
pending_shipment | Seller or buyer cancels |
cancellation_requested | 48h grace period expires |
dispute_open | Dispute resolved in buyer’s favor |
dispute_escalated | Admin resolves in buyer’s favor |
Refund Reasons
We track why each refund happened:
| Reason | Typical trigger |
|---|---|
seller_no_ship | Seller didn’t ship within the window |
item_not_as_described | Dispute: item doesn’t match listing |
item_damaged | Dispute: item arrived damaged |
item_not_received | Dispute: buyer never got the package |
buyer_requested | Buyer requested cancellation |
dispute_resolved | Dispute settlement |
other | Manual/admin refund |
How Refunds Work
Under the hood, refunds follow a careful process to ensure we don’t mess up the money:
Stripe is the source of truth. We always attempt the Stripe refund first, then update our database. This ensures our records always match what Stripe has.
Concurrency Control
What happens if two refund requests come in at the same time? We use a locking mechanism to prevent race conditions:
interface OrderFunds {
operationLock: 'none' | 'refund' | 'transfer'
// ...
}Before any refund (or transfer), we set operationLock. If it’s already set, the second request fails immediately. After the operation completes, we release the lock.
Cancellation Flow
Cancellations are the simplest way for an order to end early. They trigger automatic refunds.
Who Can Cancel?
Buyer Cancellation
Buyers can request cancellation within 7 days of the order:
Buyer Requests Cancel
Order moves to cancellation_requested
48-Hour Window Opens
Seller has 48 hours to respond
If Seller Ships
Cancellation is voided, order continues as shipped
If 48h Expires
Order becomes cancelled_buyer, buyer gets full refund
The 48-hour grace period protects sellers who were already packing the order when the cancellation request came in.
Dispute System
Disputes handle more complex problems — items that arrived damaged, don’t match the listing, or never arrived at all. Unlike cancellations, disputes involve negotiation between buyer and seller.
Opening a Dispute
Buyers can open disputes on orders that are shipped or delivered, within 30 days of the order date.
When opening a dispute, buyers must provide:
| Field | Required | Purpose |
|---|---|---|
| Issue categories | Yes | What went wrong (can select multiple) |
| Description | Yes | Detailed explanation of the problem |
| Evidence URLs | No | Photos or other proof |
| Preferred outcome | No | What they’d like to happen (indicative only) |
Issue Categories
| Category | When to use |
|---|---|
missing_item | Some items from the order are missing |
wrong_condition | Item condition doesn’t match listing |
counterfeit_resealed | Item appears fake or tampered with |
wrong_product | Received a different card/product |
damaged | Item was damaged during shipping |
not_received | Package never arrived |
other | Anything else |
The Resolution Process
Once a dispute is open, the seller needs to respond with a proposal. Here’s how the back-and-forth works:
Seller Reviews the Dispute
Seller sees the buyer’s complaint, description, and evidence
Seller Makes a Proposal
Seller proposes a resolution (full refund, partial refund, no refund, or return)
Buyer Responds
Buyer can accept the proposal or decline and wait for a new one
If No Agreement
Either party can escalate to the platform, or it auto-escalates after 2 business days
Platform Decides
For escalated disputes, an admin reviews and makes a final decision via setPlatformDecision. This routes through the same resolve_dispute state machine transition used for peer-resolved disputes, but includes platformDecision metadata (admin user ID, optional notes).
Dispute Outcomes
| Outcome | What happens | Who pays |
|---|---|---|
full_refund | Buyer gets 100% back, seller keeps nothing | Platform (from held funds) |
partial_refund | Buyer gets agreed amount, seller keeps the rest | Split |
no_refund | Seller keeps all funds | Buyer loses |
return_refund | Buyer returns item, then gets full refund | Depends on return shipping |
Auto-Escalation
If the seller doesn’t respond to a dispute within 2 business days (no proposals submitted), the dispute automatically escalates to dispute_escalated. This is handled by the process-dispute-lifecycle background job that runs every 2 hours.
The job queries for disputes matching these criteria:
- Status is
dispute_open - Opened more than 2 business days ago (weekends excluded)
- Seller has made zero proposals
When auto-escalated, the seller receives an issue-auto-escalated notification (distinct from the manual escalation notification) informing them that the dispute was escalated due to no response.
Either party can also manually escalate if they feel the negotiation isn’t going anywhere.
Platform Decision (Admin Resolution)
When a dispute is escalated, an admin resolves it via the setPlatformDecision method on OrderOrchestratorService. This follows the same state machine path as peer-resolved disputes:
- Validation — Checks the order is in
dispute_escalatedstatus and has no existing platform decision - State machine transition — Uses the
resolve_disputetransition (same asacceptProposal), extended withplatformDecisionmetadata - Fund operations — Triggers refund/transfer via
handleDisputeResolutionFunds()(best-effort; failures don’t block the resolution) - Event emission — Emits
order.dispute_resolvedwithacceptedBy: "platform"
The platform decision is stored on the order’s dispute subdocument:
dispute.platformDecision = {
outcome: string // "full_refund" | "partial_refund" | "no_refund" | "return"
decidedBy: string // Admin user ID
decidedAt: Date // When the decision was made
notes?: string // Optional admin notes
}
dispute.agreedOutcome = outcome
dispute.resolvedAt = new Date()Both peer-resolved and platform-resolved disputes use the same resolve_dispute transition in the state machine. The difference is the presence of platformDecision metadata, which determines whether acceptedBy is "platform" or "buyer" in the emitted event.
The Return Process
For return_refund outcomes, the buyer needs to ship the item back:
API Endpoints
Cancellation Endpoints
| Endpoint | Actor | Purpose |
|---|---|---|
order.requestCancellation | Buyer | Request cancel (7-day window) |
order.acceptCancellation | Seller | Accept buyer’s request |
order.cancelOrder | Seller | Direct cancel before shipping |
Dispute Endpoints
| Endpoint | Actor | Purpose |
|---|---|---|
order.openDispute | Buyer | Start a dispute |
order.proposeResolution | Seller | Make a proposal |
order.acceptProposal | Buyer | Accept seller’s proposal |
order.declineProposal | Buyer | Reject and wait for new proposal |
order.escalateDispute | Buyer | Escalate to platform |
order.sellerEscalateDispute | Seller | Escalate to platform |
Admin Dispute Endpoints
| Endpoint | Actor | Purpose |
|---|---|---|
adminOrders.setPlatformDecision | Admin | Resolve an escalated dispute with a final decision |
adminOrders.getDetail | Admin | Get full order detail with enriched user info |
adminOrders.listOrders | Admin | List/filter orders for admin dashboard |
Code Locations
| Component | Location |
|---|---|
| Refund Service | packages/backend/features/orders/src/refund.service.ts |
| Dispute Logic | packages/backend/features/orders/src/dispute.service.ts |
| Dispute Lifecycle Handler | packages/backend/features/orders/src/dispute-lifecycle.handler.ts |
| Order Domain Rules | packages/backend/domains/order/src/order-domain.service.ts |
| Order State Machine | packages/backend/domains/order/src/order.transitions.ts |
| Order Orchestrator | packages/backend/features/orders/src/order-orchestrator.service.ts |
| Dispute Contracts | packages/core/api-dtos/src/lib/marketplace/order/dispute.ts |
| Admin Order Handlers | packages/backend/api/src/lib/orpc/handlers/marketplace-admin/orders/ |