12 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Dispute Flow |
|
|
|
Dispute Flow
When something goes wrong (item not delivered, wrong item, fraud), either party can open a dispute. A three-way chat (buyer, seller, admin mediator) is created automatically. After evidence is gathered, the admin resolves the dispute — releasing the escrow to the seller, refunding the buyer, splitting the funds, or rejecting the claim.
Actors
- Buyer — typical initiator.
- Seller — party against whom the dispute is raised (or in rarer cases, initiator).
- Admin / Mediator — assigned to investigate.
- Frontend — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard.
- Backend —
DisputeService(backend/src/services/dispute/DisputeService.ts),DisputeController(backend/src/controllers/disputeController.ts), routes atbackend/src/routes/disputeRoutes.ts. - MongoDB —
disputes,chats,purchaserequests,payments. - Socket.IO —
new-notification,new-message,dispute-updated(planned).
Preconditions
- The related
PurchaseRequestexists. - The initiator is the request's buyer or the related seller.
- Funds are typically held in escrow (
Payment.escrowState = 'funded') — disputes on unfunded orders are accepted but have no monetary impact.
Dispute state machine (Dispute.status)
stateDiagram-v2
[*] --> pending: createDispute()\nresponseDeadline=+48h\ndeadline=+7d
pending --> in_progress: admin assigned\nassignAdmin()
in_progress --> resolved: admin resolves\naction ∈ {refund, partial, release, reject}
in_progress --> closed: admin closes without resolution\n(e.g. duplicate/spam)
pending --> closed: same
resolved --> [*]
closed --> [*]
Resolution actions (from Dispute.resolution.action enum, see Dispute.ts): refund, partial, release, reject (the wording differs slightly in the model — verify with backend/src/models/Dispute.ts).
Step-by-step narrative
Phase 1 — Opening
- Buyer or seller opens the request detail and clicks "Report problem" (
frontend/src/sections/request/components/report-problem-to-admin.tsx). - They select a
category(delivery, payment, quality, fraud, other), apriority(low | medium | high | urgent), write adescription, and optionally uploadevidence(images, screenshots, video, document) viaPOST /api/files/upload. - Frontend POSTs
POST /api/disputeswith{ purchaseRequestId, reason, description, priority, category, evidence: [...] }. - Backend
DisputeService.createDispute(:12-119):- Loads the purchase request with
populate('selectedOfferId'). - Resolves the counter-party
sellerIdby priority: explicitdata.sellerId→selectedOffer.sellerId→ first ofpreferredSellerIds. This means once an offer is accepted, the dispute targets the actual seller, not the entire preferred list. - Creates the
Disputewithstatus: 'pending',responseDeadline = now + 48h,deadline = now + 7 days, and an emptytimeline[]. - Creates a
Chatof typegroupwith the buyer and the resolved seller as participants. The opening message is a system-typed line"اختلاف جدید ایجاد شد: {reason}". The chat'srelatedTo = { type: 'PurchaseRequest', id }. - Persists
dispute.chatId = chat._id.
- Loads the purchase request with
- Notifications (currently a
TODOin the service —:107-116) should firenew-notificationto the seller. Today the chat creation alone provides real-time presence via thenew-messagesocket emit insideChat.create's lifecycle.
[!warning] Dispute does not auto-pause escrow Today, opening a dispute does not flip
Payment.escrowStateaway fromfunded. An admin could theoretically still release the escrow before resolving the dispute. Until adisputedflag is added to Payment, admins must check the dispute table before any release/refund action.
Phase 2 — Admin assignment
- The admin dispute dashboard lists pending disputes (sorted by
priority: -1, createdAt: -1). - Admin clicks "Pick up" →
POST /api/disputes/:id/assignwith{ adminId }(currently the admin's own id). DisputeService.assignAdmin(:184-223):dispute.adminId = adminId; dispute.status = 'in_progress'.- Appends
timelineentry{ action: 'admin_assigned', performedBy: adminId, ... }. - Adds the admin to the dispute
chat.participants[](roleadmin). - Saves.
Phase 3 — Investigation
- All three parties chat in the dispute chat room (same socket mechanics as Chat Flow). Each party can upload more evidence via
POST /api/disputes/:id/evidence—DisputeService.addEvidence(:305-337) appends todispute.evidence[]and writes atimelineentryevidence_added. - The admin may also
PATCH /api/disputes/:id/statuswith intermediate states or notes; this updatesdispute.statusand writes atimelineentrystatus_changed.
Phase 4 — Resolution
- Once the admin has enough information, they call
POST /api/disputes/:id/resolvewith{ action, amount?, currency?, notes? }. DisputeService.resolveDispute(:262-300):dispute.status = 'resolved'dispute.resolution = { action, amount, currency, notes, resolvedBy: adminId, resolvedAt: now }dispute.closedAt = now- Appends
timelineentrydispute_resolved. - Saves.
- Financial side-effect (manual today): depending on the action, the admin then triggers either the payout (Payout Flow with
kind: 'release') or the refund (kind: 'refund', see Escrow Flow). The dispute service does not automatically dispatch the on-chain action. - Both parties are notified (TODOs in code — planned:
notifyDisputeResolved).
Sequence diagram
sequenceDiagram
autonumber
actor B as Buyer
actor S as Seller
actor A as Admin
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
participant IO as Socket.IO
B->>FE: "Report problem" on request
B->>FE: Choose category, priority, evidence
FE->>BE: POST /api/disputes
BE->>DB: Dispute.create({status:"pending"})
BE->>DB: Chat.create({type:"group", participants:[buyer, seller], system message})
BE->>DB: dispute.chatId = chat._id
BE-->>FE: { dispute }
FE-->>B: chat opens (real-time via existing chat join)
FE-->>S: chat opens (real-time via existing chat join)
A->>FE: Admin dashboard, click "Pick up"
FE->>BE: POST /api/disputes/{id}/assign
BE->>DB: dispute.adminId, status="in_progress", timeline.push
BE->>DB: chat.participants.push(admin)
BE-->>FE: { dispute }
loop investigation
A->>FE: Chat with B & S
B-->>BE: POST /api/disputes/{id}/evidence (image)
BE->>DB: dispute.evidence.push, timeline.push
end
A->>FE: Click "Resolve" choose action
FE->>BE: POST /api/disputes/{id}/resolve { action, amount, notes }
BE->>DB: dispute.status="resolved", resolution={...}
alt action="refund"
A->>BE: trigger refund payout to buyer\n[[Escrow Flow]] / [[Payout Flow]]
else action="release"
A->>BE: trigger payout to seller\n[[Payout Flow]]
else action="partial"
A->>BE: split — refund X to buyer, release Y to seller
end
BE-->>FE: { dispute }
IO-->>B: 'new-notification' dispute resolved (planned)
IO-->>S: 'new-notification' dispute resolved (planned)
API calls
| Method | Endpoint | Source |
|---|---|---|
POST |
/api/disputes |
disputeRoutes.ts:12 → DisputeController.createDispute |
GET |
/api/disputes |
disputeRoutes.ts:15 (filters: status, priority, category, adminId, buyer/seller) |
GET |
/api/disputes/statistics |
disputeRoutes.ts:18 |
GET |
/api/disputes/:id |
disputeRoutes.ts:21 |
POST |
/api/disputes/:id/assign |
disputeRoutes.ts:24 |
PATCH |
/api/disputes/:id/status |
disputeRoutes.ts:27 |
POST |
/api/disputes/:id/resolve |
disputeRoutes.ts:30 |
POST |
/api/disputes/:id/evidence |
disputeRoutes.ts:33 |
All require authenticateToken (router-level middleware).
Database writes
disputes— insert on open; updatesadminId,status,timeline[],evidence[],resolution,closedAtover the lifecycle.chats— newgroupchat on open; admin appended toparticipants[]on assignment; messages appended throughout.purchaserequests— not directly mutated by the dispute service; the resolution side-effect (release/refund) updates the request via Escrow Flow.payments— touched indirectly when the admin performs the financial resolution.notifications—TODOmarkers in code; planned addition.
Socket events emitted
new-message→chat-{disputeChatId}for each chat line (via the standardChatService.sendMessageand the system message created inDisputeService.createDispute).new-notification(planned) →user-{buyerId}anduser-{sellerId}on creation, assignment, evidence-added, resolution.
Side effects
- Three-way chat creation is the most visible side effect — pulls the buyer and seller into a controlled conversation room.
- Timeline append-only log is the audit trail. Surface it in the admin UI for compliance.
- Response deadline = 48h — used by reminders / SLA dashboards (no automated enforcement today). Past-deadline disputes could auto-escalate priority.
- Hard deadline = 7d — same intent: a watchdog could mark long-unresolved disputes for admin attention.
Error / edge cases
- Purchase request missing →
400 Purchase request not found. - No seller identifiable (orphan request) → dispute still created but with
sellerId: undefined; the chat becomes 2-party (buyer + admin only). Recommended: reject creation in this case to avoid mediator-less situations. - Initiator is neither buyer nor seller → not enforced at service level — should be validated in
DisputeController(recommended hardening). - Same user opens multiple disputes for the same request → no uniqueness constraint today. Consider adding
unique on (purchaseRequestId, status:'pending'|'in_progress')to prevent duplicates. - Evidence URL is hot-linked → frontend uploads through
POST /api/files/uploadand the URL is served from/uploads. Ensure auth on the upload endpoint to prevent random users from polluting evidence. - Dispute resolved without financial follow-up → the dispute is "resolved" in record only; the escrow stays in its previous state. Add automation that auto-fires the payout/refund when the admin selects
releaseorrefund. - Admin resigns mid-dispute → no transfer-of-mediator endpoint today. Add
POST /api/disputes/:id/reassign.
[!tip] Sort disputes by priority + age The query
Dispute.find().sort({ priority: -1, createdAt: -1 })already used ingetDisputesensuresurgentones bubble to the top. Make sure the admin dashboard uses this default sort.
Linked flows
- Chat Flow — message-level mechanics inside the dispute chat.
- Escrow Flow — the financial state being contested.
- Payout Flow — executed on
releaseresolutions. - Notification Flow — channels for dispute alerts.
- Delivery Confirmation Flow — disputes often arise from failed delivery.
Source files
- Backend:
backend/src/services/dispute/DisputeService.ts - Backend:
backend/src/controllers/disputeController.ts - Backend:
backend/src/routes/disputeRoutes.ts - Backend:
backend/src/models/Dispute.ts - Frontend:
frontend/src/sections/request/components/report-problem-to-admin.tsx - Frontend: admin dispute dashboard under
frontend/src/sections/admin/(subject to organisation)