Skip to main content

Shop Disputes

Admins arbitrate disputes buyers open on Shop orders (see Shop) — resolving as a full refund or denying the dispute, with a note. This was previously undocumented on the admin side — new doc.

Implementation checklist

  • List open product disputes (adminOpenProductDisputes)
  • Resolve a dispute — refund or deny, with admin notes (adminResolveProductDispute)

Where this lives

Backendgraphql/types/product-dispute.type.js, graphql/resolvers/product-dispute.resolver.js, managers/product-managers/product-dispute.manager.js. Note: adminOpenProductDisputes / adminResolveProductDispute were renamed this cycle (from openProductDisputes/resolveProductDispute) to be admin-prefixed, so apps/backend/api/server.js's schema split (only fields matching /^admin[A-Z]/ stay on the admin instance) now routes them onto the admin schema (/admin/graphql) — structurally unreachable via the client /web/graphql. The resolver file still lives outside the admin/ folder, and each resolver additionally enforces context.admin + the MODERATE_CONTENT permission.

Frontendapps/frontend-admin/src/app/moderation/disputes/page.tsx. Lists open disputes tied to shop orders; an admin (requires MODERATE_CONTENT, per the Shop doc) resolves each as a refund or a denial with notes. The query/mutation are generated admin operations in @closegram/apollo-admin (AdminGetOpenProductDisputesInline / AdminResolveProductDisputeInline), imported by the page as generated Documents.

GraphQL reference

query AdminGetOpenProductDisputesInline($limit: Int, $offset: Int) {
adminOpenProductDisputes(limit: $limit, offset: $offset) {
id orderId reason status createdAt
raisedBy { id username profilePicture }
order { id quantity totalPriceCoins status product { id name } seller { id username } }
}
}

mutation AdminResolveProductDisputeInline($disputeId: ID!, $resolution: ProductDisputeResolution!, $adminNotes: String) {
adminResolveProductDispute(disputeId: $disputeId, resolution: $resolution, adminNotes: $adminNotes) {
id status
}
}

adminResolveProductDispute returns the updated ProductDispute (not a success/message payload). The buyer who opened the dispute resolves as raisedBy, not buyer — there's no buyer/seller field directly on ProductDispute; the seller is reached via order.seller.

ProductDisputeResolution is an enum with two values: resolved_refund and resolved_denied. Choosing resolved_refund reverses the coin transfer (credits the buyer, debits the seller's balance and lifetime-earned) the same way cancelOrder does, but does not restore stock or change the order's own status — the order stays shipped/delivered as a shipping record, and the dispute row is what tracks the refund. resolved_denied just closes the dispute with no coin movement. Either way the buyer gets a dispute_resolved notification (best-effort, swallowed on failure).