feat: Implement dispute flow with database schema and API endpoints

This commit is contained in:
J.A.R.V.I.S. 2026-03-19 08:09:11 +00:00
parent 431ced05b5
commit 2268ef56d8
9 changed files with 454 additions and 0 deletions

View file

@ -0,0 +1,68 @@
import { DB } from '../db/index.js';
export class DisputeService {
constructor(db) {
this.db = db;
}
async createDispute(disputeData) {
const { deal_id, opened_by_user_id, reason_code, summary, requested_outcome } = disputeData;
const result = await this.db.query(
`INSERT INTO disputes (deal_id, opened_by_user_id, reason_code, summary, requested_outcome)
VALUES (?, ?, ?, ?, ?)`,
[deal_id, opened_by_user_id, reason_code, summary, requested_outcome]
);
const disputeId = result.insertId;
return await this.getDisputeById(disputeId);
}
async getDisputeById(id) {
const [rows] = await this.db.query('SELECT * FROM disputes WHERE id = ?', [id]);
return rows.length > 0 ? rows[0] : null;
}
async updateDisputeStatus(disputeId, status, updatedByUserId) {
await this.db.query(
'UPDATE disputes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[status, disputeId]
);
// Log the status change as an event
await this.logDisputeEvent(disputeId, 'status_change', updatedByUserId, {
old_status: 'open',
new_status: status
});
}
async addEvidence(disputeId, userId, evidenceData) {
// Log the evidence addition as an event
await this.logDisputeEvent(disputeId, 'evidence_added', userId, evidenceData);
}
async resolveDispute(disputeId, resolvedByUserId, decision, reason) {
await this.db.query(
`UPDATE disputes
SET status = 'resolved', final_decision = ?, final_reason = ?, decided_by_user_id = ?, decided_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[decision, reason, resolvedByUserId, disputeId]
);
// Log the resolution as an event
await this.logDisputeEvent(disputeId, 'resolved', resolvedByUserId, { decision, reason });
}
async logDisputeEvent(disputeId, eventType, actorUserId, payload) {
await this.db.query(
`INSERT INTO dispute_events (dispute_id, event_type, actor_user_id, payload_json)
VALUES (?, ?, ?, ?)`,
[disputeId, eventType, actorUserId, JSON.stringify(payload)]
);
}
async getDisputeEvents(disputeId) {
const [rows] = await this.db.query('SELECT * FROM dispute_events WHERE dispute_id = ? ORDER BY created_at ASC', [disputeId]);
return rows;
}
}

View file

@ -0,0 +1,67 @@
import { DB } from '../db';
import { Dispute, DisputeEvent, DisputeStatus, DisputeReasonCode, DisputeOutcome } from './types';
export class DisputeService {
constructor(private db: DB) {}
async createDispute(disputeData: Omit<Dispute, 'id' | 'status' | 'created_at' | 'updated_at'>): Promise<Dispute> {
const { deal_id, opened_by_user_id, reason_code, summary, requested_outcome } = disputeData;
const result = await this.db.query(
`INSERT INTO disputes (deal_id, opened_by_user_id, reason_code, summary, requested_outcome)
VALUES (?, ?, ?, ?, ?)`,
[deal_id, opened_by_user_id, reason_code, summary, requested_outcome]
);
const disputeId = result.insertId;
return await this.getDisputeById(disputeId);
}
async getDisputeById(id: number): Promise<Dispute | null> {
const [rows] = await this.db.query('SELECT * FROM disputes WHERE id = ?', [id]);
return rows.length > 0 ? rows[0] as Dispute : null;
}
async updateDisputeStatus(disputeId: number, status: DisputeStatus, updatedByUserId: number): Promise<void> {
await this.db.query(
'UPDATE disputes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[status, disputeId]
);
// Log the status change as an event
await this.logDisputeEvent(disputeId, 'status_change', updatedByUserId, {
old_status: 'open',
new_status: status
});
}
async addEvidence(disputeId: number, userId: number, evidenceData: any): Promise<void> {
// Log the evidence addition as an event
await this.logDisputeEvent(disputeId, 'evidence_added', userId, evidenceData);
}
async resolveDispute(disputeId: number, resolvedByUserId: number, decision: DisputeOutcome, reason: string): Promise<void> {
await this.db.query(
`UPDATE disputes
SET status = 'resolved', final_decision = ?, final_reason = ?, decided_by_user_id = ?, decided_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[decision, reason, resolvedByUserId, disputeId]
);
// Log the resolution as an event
await this.logDisputeEvent(disputeId, 'resolved', resolvedByUserId, { decision, reason });
}
async logDisputeEvent(disputeId: number, eventType: string, actorUserId: number, payload: any): Promise<void> {
await this.db.query(
`INSERT INTO dispute_events (dispute_id, event_type, actor_user_id, payload_json)
VALUES (?, ?, ?, ?)`,
[disputeId, eventType, actorUserId, JSON.stringify(payload)]
);
}
async getDisputeEvents(disputeId: number): Promise<DisputeEvent[]> {
const [rows] = await this.db.query('SELECT * FROM dispute_events WHERE dispute_id = ? ORDER BY created_at ASC', [disputeId]);
return rows as DisputeEvent[];
}
}

View file

@ -0,0 +1,2 @@
export * from './dispute-service';
export * from './types';

View file

@ -0,0 +1,51 @@
export const DisputeStatus = {
OPEN: 'open',
EVIDENCE: 'evidence',
MEDIATION: 'mediation',
RESOLVED: 'resolved',
CANCELLED: 'cancelled'
};
export const DisputeReasonCode = {
NO_SHOW: 'NO_SHOW',
QUALITY_ISSUE: 'QUALITY_ISSUE',
PAYMENT_DISPUTE: 'PAYMENT_DISPUTE',
ABUSE: 'ABUSE',
OTHER: 'OTHER'
};
export const DisputeOutcome = {
REFUND: 'refund',
PARTIAL_REFUND: 'partial_refund',
COMPLETE_WORK: 'complete_work',
OTHER: 'other'
};
export class Dispute {
constructor(id, deal_id, opened_by_user_id, status, reason_code, summary, requested_outcome, final_decision = null, final_reason = null, decided_by_user_id = null, decided_at = null, created_at, updated_at) {
this.id = id;
this.deal_id = deal_id;
this.opened_by_user_id = opened_by_user_id;
this.status = status;
this.reason_code = reason_code;
this.summary = summary;
this.requested_outcome = requested_outcome;
this.final_decision = final_decision;
this.final_reason = final_reason;
this.decided_by_user_id = decided_by_user_id;
this.decided_at = decided_at;
this.created_at = created_at;
this.updated_at = updated_at;
}
}
export class DisputeEvent {
constructor(id, dispute_id, event_type, actor_user_id, payload_json, created_at) {
this.id = id;
this.dispute_id = dispute_id;
this.event_type = event_type;
this.actor_user_id = actor_user_id;
this.payload_json = payload_json;
this.created_at = created_at;
}
}

View file

@ -0,0 +1,30 @@
export type DisputeStatus = 'open' | 'evidence' | 'mediation' | 'resolved' | 'cancelled';
export type DisputeReasonCode = 'NO_SHOW' | 'QUALITY_ISSUE' | 'PAYMENT_DISPUTE' | 'ABUSE' | 'OTHER';
export type DisputeOutcome = 'refund' | 'partial_refund' | 'complete_work' | 'other';
export interface Dispute {
id: number;
deal_id: number;
opened_by_user_id: number;
status: DisputeStatus;
reason_code: DisputeReasonCode;
summary: string;
requested_outcome: DisputeOutcome;
final_decision?: DisputeOutcome | null;
final_reason?: string | null;
decided_by_user_id?: number | null;
decided_at?: string | null;
created_at: string;
updated_at: string;
}
export interface DisputeEvent {
id: number;
dispute_id: number;
event_type: string;
actor_user_id: number;
payload_json: string;
created_at: string;
}