Files
nick-doc/02 - Data Models/Dispute.md
Siavash Sameni d072238fe8 docs: update PG migration status, data models, architecture + add Telegram Mini App flow (v2.8.59)
- Postgres Runtime Cutover Status: 17 migrations (0000–0017), dual-write repo matrix
- Backend Architecture: dual-DB architecture, repo factory, MONGO_CONNECT_MODE modes
- Data Model Overview: 23-model index with PG table names and migration status
- User, PurchaseRequest, SellerOffer, Chat, Dispute: Drizzle schema + cutover status added
- 04 - Flows/Telegram Mini App.md: new doc covering Mini App architecture and flows
- mongo-to-pg-migration-prd.md: status block prepended with 2026-06-03 milestone tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:30:51 +04:00

10 KiB

title, tags, aliases
title tags aliases
Dispute
data-model
mongoose
postgres
Complaint
IDispute

Dispute

Last updated: 2026-06-03 — added Postgres / Drizzle schema and migration status (see Doc vs Code Audit Report)

Buyer-raised complaint tied to a PurchaseRequest. Captures the reason, priority, category, an array of evidence uploads, a chronological timeline of actions, an optional resolution, and SLA deadlines. An admin (adminId) is assigned during triage and resolves the dispute with a structured action (refund, replacement, compensation, warning_seller, ban_seller, or no_action).

[!note] Implementation status backend/src/models/Dispute.ts, backend/src/services/dispute/DisputeService.ts, backend/src/routes/disputeRoutes.ts, and release-hold helper routes now exist. The remaining gap is canonical state alignment between the full dispute document and the lighter PurchaseRequest/Payment hold flags used by release gating.

Sources: backend/src/models/Dispute.ts (Mongoose schema), backend/src/db/schema/dispute.ts (Drizzle/Postgres schema).

WARNING — The dispute status update endpoint and the resolve endpoint currently have no role guards. Any authenticated user (not just admins) can modify dispute status or submit a resolution. This is a known gap pending a role-guard audit.

Migration Status

DUAL-WRITEDualWriteDisputeRepo + DrizzleDisputeRepo + MongoDisputeRepo. Writes go to both Mongo and Postgres. Reads still come from Mongo (cutover not yet executed).

Mongo Schema

Field Type Required Default Validation Index Description
purchaseRequestId ObjectId → PurchaseRequest yes yes The disputed request.
buyerId ObjectId → User yes yes Complaining buyer.
sellerId ObjectId → User no yes Implicated seller.
adminId ObjectId → User no yes (single + compound) Admin owning the case.
reason String yes trim, maxlength 200 Short reason.
description String yes trim, maxlength 2000 Detailed description.
priority String no medium enum: low / medium / high / urgent yes Triage priority.
category String yes enum: product_quality / delivery_delay / wrong_item / payment_issue / seller_behavior / other yes Issue type.
status String no pending enum: pending / in_progress / waiting_response / resolved / rejected / closed yes (single + compound) Lifecycle state.
evidence[].type String yes enum: image / document / screenshot / video Evidence kind.
evidence[].url String yes Stored URL.
evidence[].description String no Notes.
evidence[].uploadedBy ObjectId → User yes Uploader.
evidence[].uploadedAt Date no Date.now Upload time.
chatId ObjectId → Chat no Linked support chat.
timeline[].action String yes Action label.
timeline[].performedBy ObjectId → User yes Actor.
timeline[].performedAt Date no Date.now When.
timeline[].details String no Free-form notes.
resolution.action String no enum: refund / replacement / compensation / warning_seller / ban_seller / no_action Outcome.
resolution.amount Number no Monetary amount (refund/compensation).
resolution.currency String no enum: USD / EUR / IRR / USDT Currency.
resolution.notes String no maxlength 1000 Resolution notes.
resolution.resolvedBy ObjectId → User no Admin who resolved.
resolution.resolvedAt Date no When resolved.
deadline Date no Overall SLA deadline.
responseDeadline Date no Response SLA.
tags[] String[] no trim Filter tags.
closedAt Date no When closed.
createdAt Date auto yes (desc) Mongoose timestamp.
updatedAt Date auto Mongoose timestamp.

Category enum

Valid values: product_quality · delivery_delay · wrong_item · payment_issue · seller_behavior · other

Note: fraud is NOT a valid category value. Use seller_behavior or other for fraud-related complaints.

Status enum

Valid values: pending · in_progress · waiting_response · resolved · rejected · closed

Note: under_review does NOT exist in the schema. The equivalent lifecycle state is in_progress.

Resolution action enum

Valid values: refund · replacement · compensation · warning_seller · ban_seller · no_action

Note: The TypeScript interface mentions an optional embedded messages[] array, but the actual Mongoose schema does not declare it — messages live in Chat via chatId.

Postgres / Drizzle Schema

Source: backend/src/db/schema/dispute.ts — migration 0012.

disputes table

Column PG Type Notes
id uuid PK Generated UUID primary key.
legacy_object_id text Mongo ObjectId bridge; partial-unique WHERE NOT NULL.
purchase_request_id text Stored as text (not uuid FK) to accommodate Mongo ObjectIds and PG UUIDs during cutover. No hard FK.
buyer_id text Same cutover reason — text, no hard FK.
seller_id text Optional; text, no hard FK.
admin_id text Optional; text, no hard FK.
reason text Short reason.
description text Detailed description.
priority text No DB-level enum; app-layer validated.
category text No DB-level enum; app-layer validated.
status text No DB-level enum; app-layer validated.
evidence jsonb Array of evidence objects (serialized).
chat_id text Optional; text reference to Chat.
messages jsonb Embedded messages blob (conservative shim; normalization pending).
timeline jsonb Array of timeline action objects.
resolution jsonb Resolution object when resolved.
deadline timestamptz Overall SLA deadline.
response_deadline timestamptz Response SLA.
tags jsonb Array of tag strings.
created_at timestamptz Auto-managed.
updated_at timestamptz Auto-managed.
closed_at timestamptz Set when status reaches closed.

[!note] ID columns as text purchase_request_id, buyer_id, seller_id, and admin_id are all stored as text (not uuid with a FK) to accommodate both legacy Mongo ObjectIds and PG UUIDs transparently during the cutover window. No referential integrity constraints exist at the DB layer for these columns.

[!note] messages jsonb column The Postgres schema includes a messages jsonb column that is absent from the Mongo schema (where messages live in Chat via chatId). This is a conservative shim added during migration scaffolding. Full normalization of chat/messages is flagged as an open blocker.

Postgres Indexes

Index Type Notes
(legacy_object_id) WHERE NOT NULL partial-unique Idempotent backfill upserts.
(purchase_request_id) regular Lookup by request.
(buyer_id) regular Buyer's disputes.
(seller_id) regular Seller's disputes.
(admin_id) regular Admin workload.
(status) regular Lifecycle filtering.
(priority) regular Priority filtering.
(category) regular Category filtering.
(created_at) regular Time-ordered listing.
(status, priority) compound Admin queue sort.
(admin_id, status) compound Per-admin workload view.

Mirrors the Mongo index set exactly.

Virtuals

None defined.

Mongo Indexes

Defined at backend/src/models/Dispute.ts:

  • { purchaseRequestId: 1 }
  • { buyerId: 1 }
  • { sellerId: 1 }
  • { adminId: 1 }
  • { status: 1 }
  • { priority: 1 }
  • { category: 1 }
  • { createdAt: -1 }
  • { status: 1, priority: -1 } — admin queue
  • { adminId: 1, status: 1 } — per-admin workload

Pre/Post Hooks

Hook Behaviour
pre('save') (backend/src/models/Dispute.ts) On new documents pushes a dispute_created entry into timeline attributed to buyerId.

Instance Methods

None defined.

Static Methods

None defined.

Relationships

  • References: PurchaseRequest (purchaseRequestId), User (buyerId, sellerId, adminId, evidence and timeline contributors, resolution.resolvedBy), Chat (chatId).
  • Referenced by: none directly.

State Transitions

stateDiagram-v2
    [*] --> pending
    pending --> in_progress : admin assigned
    in_progress --> waiting_response : awaiting party
    waiting_response --> in_progress : response received
    in_progress --> resolved : action applied
    in_progress --> rejected : invalid
    resolved --> closed
    rejected --> closed
    closed --> [*]

Common Queries

// Admin queue (Mongo)
Dispute.find({ status: { $in: ['pending', 'in_progress', 'waiting_response'] } })
       .sort({ priority: -1, createdAt: 1 });

// Buyer's disputes (Mongo)
Dispute.find({ buyerId }).sort({ createdAt: -1 });

// Seller's open disputes (Mongo)
Dispute.find({ sellerId, status: { $nin: ['resolved', 'rejected', 'closed'] } });

// Append timeline entry atomically (Mongo)
Dispute.updateOne(
  { _id },
  { $push: { timeline: { action, performedBy: adminId, performedAt: new Date(), details } } }
);
-- Admin queue (Postgres)
SELECT * FROM disputes
WHERE status IN ('pending', 'in_progress', 'waiting_response')
ORDER BY priority DESC, created_at ASC;

-- Buyer's disputes (Postgres)
SELECT * FROM disputes WHERE buyer_id = $1 ORDER BY created_at DESC;

-- Seller's open disputes (Postgres)
SELECT * FROM disputes
WHERE seller_id = $1 AND status NOT IN ('resolved', 'rejected', 'closed');

Related: PurchaseRequest, User, Chat, Payment.