Skip to Content
MarketplaceDisputes & Refunds

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:

These statuses allow refunds to be processed:

StatusTypical refund reason
pending_shipmentSeller or buyer cancels
cancellation_requested48h grace period expires
dispute_openDispute resolved in buyer’s favor
dispute_escalatedAdmin resolves in buyer’s favor

Refund Reasons

We track why each refund happened:

ReasonTypical trigger
seller_no_shipSeller didn’t ship within the window
item_not_as_describedDispute: item doesn’t match listing
item_damagedDispute: item arrived damaged
item_not_receivedDispute: buyer never got the package
buyer_requestedBuyer requested cancellation
dispute_resolvedDispute settlement
otherManual/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?

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:

FieldRequiredPurpose
Issue categoriesYesWhat went wrong (can select multiple)
DescriptionYesDetailed explanation of the problem
Evidence URLsNoPhotos or other proof
Preferred outcomeNoWhat they’d like to happen (indicative only)

Issue Categories

CategoryWhen to use
missing_itemSome items from the order are missing
wrong_conditionItem condition doesn’t match listing
counterfeit_resealedItem appears fake or tampered with
wrong_productReceived a different card/product
damagedItem was damaged during shipping
not_receivedPackage never arrived
otherAnything 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

OutcomeWhat happensWho pays
full_refundBuyer gets 100% back, seller keeps nothingPlatform (from held funds)
partial_refundBuyer gets agreed amount, seller keeps the restSplit
no_refundSeller keeps all fundsBuyer loses
return_refundBuyer returns item, then gets full refundDepends 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:

  1. Validation — Checks the order is in dispute_escalated status and has no existing platform decision
  2. State machine transition — Uses the resolve_dispute transition (same as acceptProposal), extended with platformDecision metadata
  3. Fund operations — Triggers refund/transfer via handleDisputeResolutionFunds() (best-effort; failures don’t block the resolution)
  4. Event emission — Emits order.dispute_resolved with acceptedBy: "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

EndpointActorPurpose
order.requestCancellationBuyerRequest cancel (7-day window)
order.acceptCancellationSellerAccept buyer’s request
order.cancelOrderSellerDirect cancel before shipping

Dispute Endpoints

EndpointActorPurpose
order.openDisputeBuyerStart a dispute
order.proposeResolutionSellerMake a proposal
order.acceptProposalBuyerAccept seller’s proposal
order.declineProposalBuyerReject and wait for new proposal
order.escalateDisputeBuyerEscalate to platform
order.sellerEscalateDisputeSellerEscalate to platform

Admin Dispute Endpoints

EndpointActorPurpose
adminOrders.setPlatformDecisionAdminResolve an escalated dispute with a final decision
adminOrders.getDetailAdminGet full order detail with enriched user info
adminOrders.listOrdersAdminList/filter orders for admin dashboard

Code Locations

ComponentLocation
Refund Servicepackages/backend/features/orders/src/refund.service.ts
Dispute Logicpackages/backend/features/orders/src/dispute.service.ts
Dispute Lifecycle Handlerpackages/backend/features/orders/src/dispute-lifecycle.handler.ts
Order Domain Rulespackages/backend/domains/order/src/order-domain.service.ts
Order State Machinepackages/backend/domains/order/src/order.transitions.ts
Order Orchestratorpackages/backend/features/orders/src/order-orchestrator.service.ts
Dispute Contractspackages/core/api-dtos/src/lib/marketplace/order/dispute.ts
Admin Order Handlerspackages/backend/api/src/lib/orpc/handlers/marketplace-admin/orders/
Last updated on