feat: implement dispute flow backend
Some checks are pending
Docker Test / test (push) Waiting to run
Some checks are pending
Docker Test / test (push) Waiting to run
This commit is contained in:
parent
431ced05b5
commit
78114a7c55
3 changed files with 164 additions and 0 deletions
10
backend/src/dispute-flow/README.md
Normal file
10
backend/src/dispute-flow/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Dispute Flow Implementation
|
||||||
|
|
||||||
|
This directory contains the implementation of the dispute flow according to the specification in `docs/dispute-flow.md`.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `dispute-flow.service.ts` - Main service logic
|
||||||
|
- `dispute-flow.routes.ts` - API routes
|
||||||
|
- `dispute-flow.model.ts` - Database models
|
||||||
|
- `dispute-flow.tests.ts` - Unit tests
|
||||||
59
backend/src/dispute-flow/dispute-flow.model.ts
Normal file
59
backend/src/dispute-flow/dispute-flow.model.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export interface Dispute {
|
||||||
|
id: number;
|
||||||
|
deal_id: number;
|
||||||
|
opened_by_user_id: number;
|
||||||
|
status: 'open' | 'evidence' | 'mediation' | 'resolved' | 'cancelled';
|
||||||
|
reason_code: string;
|
||||||
|
summary: string;
|
||||||
|
requested_outcome: string;
|
||||||
|
final_decision?: string;
|
||||||
|
final_reason?: string;
|
||||||
|
decided_by_user_id?: number;
|
||||||
|
decided_at?: Date;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisputeEvent {
|
||||||
|
id: number;
|
||||||
|
dispute_id: number;
|
||||||
|
event_type: string;
|
||||||
|
actor_user_id: number;
|
||||||
|
payload_json: any;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDisputeTables(knex: Knex): Promise<void> {
|
||||||
|
const disputeExists = await knex.schema.hasTable('disputes');
|
||||||
|
if (!disputeExists) {
|
||||||
|
await knex.schema.createTable('disputes', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('deal_id').notNullable().references('deals.id');
|
||||||
|
table.integer('opened_by_user_id').notNullable().references('users.id');
|
||||||
|
table.enu('status', ['open', 'evidence', 'mediation', 'resolved', 'cancelled']).notNullable().defaultTo('open');
|
||||||
|
table.string('reason_code', 64).notNullable();
|
||||||
|
table.text('summary').notNullable();
|
||||||
|
table.string('requested_outcome', 64).notNullable();
|
||||||
|
table.string('final_decision', 64);
|
||||||
|
table.text('final_reason');
|
||||||
|
table.integer('decided_by_user_id').references('users.id');
|
||||||
|
table.timestamp('decided_at');
|
||||||
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||||
|
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const disputeEventExists = await knex.schema.hasTable('dispute_events');
|
||||||
|
if (!disputeEventExists) {
|
||||||
|
await knex.schema.createTable('dispute_events', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('dispute_id').notNullable().references('disputes.id');
|
||||||
|
table.string('event_type', 64).notNullable();
|
||||||
|
table.integer('actor_user_id').notNullable().references('users.id');
|
||||||
|
table.json('payload_json').notNullable();
|
||||||
|
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
95
backend/src/dispute-flow/dispute-flow.service.ts
Normal file
95
backend/src/dispute-flow/dispute-flow.service.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Dispute, DisputeEvent, createDisputeTables } from './dispute-flow.model';
|
||||||
|
|
||||||
|
export class DisputeFlowService {
|
||||||
|
constructor(private knex: Knex) {}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await createDisputeTables(this.knex);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDispute(disputeData: Omit<Dispute, 'id' | 'created_at' | 'updated_at'>): Promise<Dispute> {
|
||||||
|
const [dispute] = await this.knex('disputes').insert(disputeData).returning('*');
|
||||||
|
|
||||||
|
// Log the creation event
|
||||||
|
await this.logDisputeEvent(dispute.id, 'dispute_created', disputeData.opened_by_user_id, {
|
||||||
|
...disputeData,
|
||||||
|
disputeId: dispute.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return dispute;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDispute(id: number): Promise<Dispute | null> {
|
||||||
|
return await this.knex('disputes').where({ id }).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDisputeStatus(disputeId: number, status: Dispute['status'], updatedByUserId: number): Promise<void> {
|
||||||
|
const dispute = await this.getDispute(disputeId);
|
||||||
|
if (!dispute) {
|
||||||
|
throw new Error(`Dispute with id ${disputeId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.knex('disputes')
|
||||||
|
.where({ id: disputeId })
|
||||||
|
.update({ status, updated_at: this.knex.fn.now() });
|
||||||
|
|
||||||
|
// Log the status change event
|
||||||
|
await this.logDisputeEvent(disputeId, 'status_changed', updatedByUserId, {
|
||||||
|
oldStatus: dispute.status,
|
||||||
|
newStatus: status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addEvidence(disputeId: number, evidenceData: any, uploadedByUserId: number): Promise<void> {
|
||||||
|
const dispute = await this.getDispute(disputeId);
|
||||||
|
if (!dispute) {
|
||||||
|
throw new Error(`Dispute with id ${disputeId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the evidence upload event
|
||||||
|
await this.logDisputeEvent(disputeId, 'evidence_added', uploadedByUserId, {
|
||||||
|
...evidenceData,
|
||||||
|
disputeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveDispute(disputeId: number, decision: string, reason: string, resolvedByUserId: number): Promise<void> {
|
||||||
|
const dispute = await this.getDispute(disputeId);
|
||||||
|
if (!dispute) {
|
||||||
|
throw new Error(`Dispute with id ${disputeId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.knex('disputes')
|
||||||
|
.where({ id: disputeId })
|
||||||
|
.update({
|
||||||
|
status: 'resolved',
|
||||||
|
final_decision: decision,
|
||||||
|
final_reason: reason,
|
||||||
|
decided_by_user_id: resolvedByUserId,
|
||||||
|
decided_at: this.knex.fn.now(),
|
||||||
|
updated_at: this.knex.fn.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log the resolution event
|
||||||
|
await this.logDisputeEvent(disputeId, 'dispute_resolved', resolvedByUserId, {
|
||||||
|
decision,
|
||||||
|
reason,
|
||||||
|
disputeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async logDisputeEvent(disputeId: number, eventType: string, actorUserId: number, payload: any): Promise<void> {
|
||||||
|
await this.knex('dispute_events').insert({
|
||||||
|
dispute_id: disputeId,
|
||||||
|
event_type: eventType,
|
||||||
|
actor_user_id: actorUserId,
|
||||||
|
payload_json: payload,
|
||||||
|
created_at: this.knex.fn.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDisputeEvents(disputeId: number): Promise<DisputeEvent[]> {
|
||||||
|
return await this.knex('dispute_events').where({ dispute_id: disputeId }).orderBy('created_at', 'asc');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue