diff --git a/TESTING.md b/TESTING.md index 5da9b17..6602b5c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,24 +1,32 @@ -# Testing +# Testkonzept – helpyourneighbour -## Unit Tests +Dieses Testkonzept ist **verpflichtend vor jedem Push**. -Unit tests are written using Jest and run with `npm run test:unit`. +## Ziel +Stabile, sichere Releases durch standardisierte Tests in Docker auf dem Unraid-Host. -## Contract Tests +## Pflichtablauf (immer) +1. **Lokaler Schnelltest** + - `cd backend && npm ci && npm test` +2. **Docker-Test auf Unraid** + - Image bauen und Smoke-Test im Container ausführen. +3. **Erst danach pushen** + - Wenn ein Test fehlschlägt: kein Push, zuerst Fix. -Contract tests ensure that the API behaves as documented in `openapi.yaml`. They are run with `npm run test:contract`. +## Docker-Standard (Unraid) +Im Repo-Root ausführen: -## Integration Tests +```bash +./scripts/test-in-docker.sh +``` -Integration tests verify the complete flow of features. They are run with `npm run test:integration`. +## Mindest-Testumfang +- Syntax-Validierung aller Backend-JS-Dateien (`node --check`) +- Smoke-Test-Exitcode 0 -## Dispute Flow Tests +## Erweiterung (nächster Schritt) +- API-Integrationstests (Auth, Requests, Offers, Contacts) +- DB-Container für reproduzierbare End-to-End-Tests -The dispute flow is tested in `test-dispute-flow.md` and includes: -- Creating disputes -- Adding evidence -- Updating status -- Resolving disputes -- Retrieving dispute history - -Tests are implemented using the existing backend infrastructure. \ No newline at end of file +## Verbindlichkeit +Dieses Konzept gilt als Standardprozess für alle weiteren Änderungen in `helpyourneighbour`. diff --git a/backend/sql/dispute-schema.sql b/backend/sql/dispute-schema.sql deleted file mode 100644 index 8342140..0000000 --- a/backend/sql/dispute-schema.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE IF NOT EXISTS disputes ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - deal_id BIGINT NOT NULL, - opened_by_user_id BIGINT NOT NULL, - status ENUM('open','evidence','mediation','resolved','cancelled') NOT NULL DEFAULT 'open', - reason_code VARCHAR(64) NOT NULL, - summary TEXT NOT NULL, - requested_outcome VARCHAR(64) NOT NULL, - final_decision VARCHAR(64) NULL, - final_reason TEXT NULL, - decided_by_user_id BIGINT NULL, - decided_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (deal_id) REFERENCES deals(id), - FOREIGN KEY (opened_by_user_id) REFERENCES users(id) -); - -CREATE TABLE IF NOT EXISTS dispute_events ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - dispute_id BIGINT NOT NULL, - event_type VARCHAR(64) NOT NULL, - actor_user_id BIGINT NOT NULL, - payload_json JSON NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (dispute_id) REFERENCES disputes(id), - FOREIGN KEY (actor_user_id) REFERENCES users(id) -); \ No newline at end of file diff --git a/backend/src/dispute-flow/dispute-flow.model.ts b/backend/src/dispute-flow/dispute-flow.model.ts index 79b2ee4..bbd3183 100644 --- a/backend/src/dispute-flow/dispute-flow.model.ts +++ b/backend/src/dispute-flow/dispute-flow.model.ts @@ -1,24 +1,59 @@ +import { Knex } from 'knex'; + export interface Dispute { id: number; - dealId: number; - openedByUserId: number; + deal_id: number; + opened_by_user_id: number; status: 'open' | 'evidence' | 'mediation' | 'resolved' | 'cancelled'; - reasonCode: string; + reason_code: string; summary: string; - requestedOutcome: string; - finalDecision?: string; - finalReason?: string; - decidedByUserId?: number; - decidedAt?: Date; - createdAt: Date; - updatedAt: Date; + 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; - disputeId: number; - eventType: string; - actorUserId: number; - payloadJson: any; - createdAt: Date; + dispute_id: number; + event_type: string; + actor_user_id: number; + payload_json: any; + created_at: Date; +} + +export async function createDisputeTables(knex: Knex): Promise { + 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()); + }); + } } \ No newline at end of file diff --git a/backend/src/dispute-flow/dispute-flow.routes.ts b/backend/src/dispute-flow/dispute-flow.routes.ts deleted file mode 100644 index c5d6499..0000000 --- a/backend/src/dispute-flow/dispute-flow.routes.ts +++ /dev/null @@ -1,102 +0,0 @@ -import express from 'express'; -import { DisputeFlowService } from './dispute-flow.service'; -import { requireAuth } from '../middleware/auth.middleware'; -import { requireRole } from '../middleware/role.middleware'; - -const router = express.Router(); -const service = new DisputeFlowService(); - -// Create a new dispute -router.post('/disputes', requireAuth, async (req, res) => { - try { - const dispute = await service.createDispute({ - dealId: req.body.dealId, - openedByUserId: req.user.id, - reasonCode: req.body.reasonCode, - summary: req.body.summary, - requestedOutcome: req.body.requestedOutcome - }); - - res.status(201).json(dispute); - } catch (error) { - console.error('Error creating dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Add evidence to a dispute -router.post('/disputes/:id/evidence', requireAuth, async (req, res) => { - try { - await service.addEvidence( - parseInt(req.params.id), - req.body, - req.user.id - ); - - res.status(200).json({ message: 'Evidence added successfully' }); - } catch (error) { - console.error('Error adding evidence:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update dispute status -router.post('/disputes/:id/status', requireAuth, requireRole(['moderator']), async (req, res) => { - try { - await service.updateDisputeStatus( - parseInt(req.params.id), - req.body.newStatus, - req.user.id - ); - - res.status(200).json({ message: 'Status updated successfully' }); - } catch (error) { - console.error('Error updating dispute status:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Resolve a dispute -router.post('/disputes/:id/resolve', requireAuth, requireRole(['moderator', 'admin']), async (req, res) => { - try { - await service.resolveDispute( - parseInt(req.params.id), - req.body, - req.user.id - ); - - res.status(200).json({ message: 'Dispute resolved successfully' }); - } catch (error) { - console.error('Error resolving dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get dispute details -router.get('/disputes/:id', requireAuth, async (req, res) => { - try { - const dispute = await service.getDisputeById(parseInt(req.params.id)); - - if (!dispute) { - return res.status(404).json({ error: 'Dispute not found' }); - } - - res.json(dispute); - } catch (error) { - console.error('Error fetching dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get dispute events history -router.get('/disputes/:id/events', requireAuth, async (req, res) => { - try { - const events = await service.getDisputeEvents(parseInt(req.params.id)); - res.json(events); - } catch (error) { - console.error('Error fetching dispute events:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; \ No newline at end of file diff --git a/backend/src/dispute-flow/dispute-flow.service.ts b/backend/src/dispute-flow/dispute-flow.service.ts index 10cad66..6882177 100644 --- a/backend/src/dispute-flow/dispute-flow.service.ts +++ b/backend/src/dispute-flow/dispute-flow.service.ts @@ -1,118 +1,95 @@ -import { Dispute, DisputeEvent } from './dispute-flow.model'; -import { db } from '../db'; +import { Knex } from 'knex'; +import { Dispute, DisputeEvent, createDisputeTables } from './dispute-flow.model'; export class DisputeFlowService { - async createDispute(disputeData: Partial): Promise { - const query = ` - INSERT INTO disputes - (deal_id, opened_by_user_id, status, reason_code, summary, requested_outcome) - VALUES (?, ?, ?, ?, ?, ?) - RETURNING * - `; - - const values = [ - disputeData.dealId, - disputeData.openedByUserId, - disputeData.status || 'open', - disputeData.reasonCode, - disputeData.summary, - disputeData.requestedOutcome - ]; - - const result = await db.query(query, values); - return result.rows[0]; + constructor(private knex: Knex) {} + + async init(): Promise { + await createDisputeTables(this.knex); } - async addEvidence(disputeId: number, evidenceData: any, actorUserId: number): Promise { - // Insert evidence as a new event - const query = ` - INSERT INTO dispute_events - (dispute_id, event_type, actor_user_id, payload_json) - VALUES (?, ?, ?, ?) - `; + async createDispute(disputeData: Omit): Promise { + const [dispute] = await this.knex('disputes').insert(disputeData).returning('*'); - const values = [ - disputeId, - 'evidence', - actorUserId, - JSON.stringify(evidenceData) - ]; - - await db.query(query, values); + // Log the creation event + await this.logDisputeEvent(dispute.id, 'dispute_created', disputeData.opened_by_user_id, { + ...disputeData, + disputeId: dispute.id + }); + + return dispute; } - async updateDisputeStatus(disputeId: number, newStatus: string, actorUserId: number): Promise { - // Update the dispute status - const query = ` - UPDATE disputes - SET status = ? - WHERE id = ? - `; - - await db.query(query, [newStatus, disputeId]); - - // Log the status change as an event - const eventQuery = ` - INSERT INTO dispute_events - (dispute_id, event_type, actor_user_id, payload_json) - VALUES (?, ?, ?, ?) - `; - - const values = [ - disputeId, - 'status_change', - actorUserId, - JSON.stringify({ newStatus }) - ]; - - await db.query(eventQuery, values); + async getDispute(id: number): Promise { + return await this.knex('disputes').where({ id }).first(); } - async resolveDispute(disputeId: number, decisionData: any, actorUserId: number): Promise { - // Update the dispute with final decision - const query = ` - UPDATE disputes - SET status = 'resolved', - final_decision = ?, - final_reason = ?, - decided_by_user_id = ?, - decided_at = NOW() - WHERE id = ? - `; - - await db.query(query, [ - decisionData.decision, - decisionData.decisionReason, - actorUserId, + async updateDisputeStatus(disputeId: number, status: Dispute['status'], updatedByUserId: number): Promise { + 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 { + 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 - ]); - - // Log the resolution as an event - const eventQuery = ` - INSERT INTO dispute_events - (dispute_id, event_type, actor_user_id, payload_json) - VALUES (?, ?, ?, ?) - `; - - const values = [ - disputeId, - 'resolved', - actorUserId, - JSON.stringify(decisionData) - ]; - - await db.query(eventQuery, values); + }); } - async getDisputeById(disputeId: number): Promise { - const query = 'SELECT * FROM disputes WHERE id = ?'; - const result = await db.query(query, [disputeId]); - return result.rows[0] || null; + async resolveDispute(disputeId: number, decision: string, reason: string, resolvedByUserId: number): Promise { + 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 { + 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 { - const query = 'SELECT * FROM dispute_events WHERE dispute_id = ? ORDER BY created_at ASC'; - const result = await db.query(query, [disputeId]); - return result.rows; + return await this.knex('dispute_events').where({ dispute_id: disputeId }).orderBy('created_at', 'asc'); } } \ No newline at end of file diff --git a/backend/src/dispute/dispute.controller.ts b/backend/src/dispute/dispute.controller.ts deleted file mode 100644 index 9ad22a1..0000000 --- a/backend/src/dispute/dispute.controller.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Controller, Post, Body, Get, Param, HttpStatus, HttpCode } from '@nestjs/common'; -import { DisputeService } from './dispute.service'; -import { Dispute, DisputeEvent } from '@prisma/client'; - -@Controller('disputes') -export class DisputeController { - constructor(private readonly disputeService: DisputeService) {} - - @Post() - async createDispute(@Body() disputeData: Partial): Promise { - return this.disputeService.createDispute(disputeData); - } - - @Get(':id') - async getDispute(@Param('id') id: number): Promise { - return this.disputeService.getDisputeById(id); - } - - @Post(':id/evidence') - async addEvidence( - @Param('id') disputeId: number, - @Body() evidenceData: any - ): Promise { - return this.disputeService.addEvidence(disputeId, evidenceData); - } - - @Post(':id/status') - @HttpCode(HttpStatus.OK) - async updateStatus( - @Param('id') disputeId: number, - @Body() statusData: { status: string } - ): Promise { - return this.disputeService.updateDisputeStatus(disputeId, statusData.status); - } - - @Post(':id/resolve') - async resolveDispute( - @Param('id') disputeId: number, - @Body() decisionData: any - ): Promise { - return this.disputeService.resolveDispute(disputeId, decisionData); - } -} \ No newline at end of file diff --git a/backend/src/dispute/dispute.module.ts b/backend/src/dispute/dispute.module.ts deleted file mode 100644 index a56bd74..0000000 --- a/backend/src/dispute/dispute.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { DisputeService } from './dispute.service'; -import { DisputeController } from './dispute.controller'; -import { PrismaService } from '../prisma/prisma.service'; - -@Module({ - controllers: [DisputeController], - providers: [DisputeService, PrismaService], - exports: [DisputeService], -}) -export class DisputeModule {} \ No newline at end of file diff --git a/backend/src/dispute/dispute.service.ts b/backend/src/dispute/dispute.service.ts deleted file mode 100644 index 3f9aa63..0000000 --- a/backend/src/dispute/dispute.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; -import { Dispute, DisputeEvent } from '@prisma/client'; - -@Injectable() -export class DisputeService { - constructor(private prisma: PrismaService) {} - - async createDispute(disputeData: Partial): Promise { - return this.prisma.dispute.create({ - data: disputeData, - }); - } - - async getDisputeById(id: number): Promise { - return this.prisma.dispute.findUnique({ - where: { id }, - }); - } - - async updateDisputeStatus(disputeId: number, status: string): Promise { - return this.prisma.dispute.update({ - where: { id: disputeId }, - data: { status }, - }); - } - - async addEvidence(disputeId: number, evidenceData: any): Promise { - const event = await this.prisma.disputeEvent.create({ - data: { - disputeId, - eventType: 'evidence', - actorUserId: evidenceData.actorUserId, - payloadJson: evidenceData.payload, - }, - }); - - // Update dispute status to 'evidence' if not already - await this.updateDisputeStatus(disputeId, 'evidence'); - - return event; - } - - async resolveDispute(disputeId: number, decisionData: any): Promise { - return this.prisma.dispute.update({ - where: { id: disputeId }, - data: { - status: 'resolved', - finalDecision: decisionData.decision, - finalReason: decisionData.reason, - decidedByUserId: decisionData.decidedByUserId, - decidedAt: new Date(), - }, - }); - } -} \ No newline at end of file diff --git a/backend/src/disputes/dispute-service.js b/backend/src/disputes/dispute-service.js deleted file mode 100644 index 77cc58c..0000000 --- a/backend/src/disputes/dispute-service.js +++ /dev/null @@ -1,68 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/backend/src/disputes/dispute-service.ts b/backend/src/disputes/dispute-service.ts deleted file mode 100644 index 15b0894..0000000 --- a/backend/src/disputes/dispute-service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { DB } from '../db'; -import { Dispute, DisputeEvent, DisputeStatus, DisputeReasonCode, DisputeOutcome } from './types'; - -export class DisputeService { - constructor(private db: DB) {} - - async createDispute(disputeData: Omit): Promise { - 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 { - 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 { - // Get current status for event logging - const dispute = await this.getDisputeById(disputeId); - if (!dispute) { - throw new Error('Dispute not found'); - } - - 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: dispute.status, - new_status: status - }); - } - - async addEvidence(disputeId: number, userId: number, evidenceData: any): Promise { - // 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 { - 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 { - 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 { - const [rows] = await this.db.query('SELECT * FROM dispute_events WHERE dispute_id = ? ORDER BY created_at ASC', [disputeId]); - return rows as DisputeEvent[]; - } -} \ No newline at end of file diff --git a/backend/src/disputes/index.ts b/backend/src/disputes/index.ts deleted file mode 100644 index 073ba43..0000000 --- a/backend/src/disputes/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './dispute-service'; -export * from './types'; \ No newline at end of file diff --git a/backend/src/disputes/types.js b/backend/src/disputes/types.js deleted file mode 100644 index 2dee539..0000000 --- a/backend/src/disputes/types.js +++ /dev/null @@ -1,51 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/backend/src/disputes/types.ts b/backend/src/disputes/types.ts deleted file mode 100644 index 16d759e..0000000 --- a/backend/src/disputes/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -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; -} \ No newline at end of file diff --git a/backend/src/routes/disputes.js b/backend/src/routes/disputes.js deleted file mode 100644 index 09fe0bd..0000000 --- a/backend/src/routes/disputes.js +++ /dev/null @@ -1,103 +0,0 @@ -import express from 'express'; -import { DisputeService } from '../disputes/dispute-service.js'; -import { DB } from '../db/index.js'; - -const router = express.Router(); -const disputeService = new DisputeService(new DB()); - -// Create a new dispute -router.post('/', async (req, res) => { - try { - const { deal_id, opened_by_user_id, reason_code, summary, requested_outcome } = req.body; - - const dispute = await disputeService.createDispute({ - deal_id, - opened_by_user_id, - reason_code, - summary, - requested_outcome - }); - - res.status(201).json(dispute); - } catch (error) { - console.error('Error creating dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get dispute by ID -router.get('/:id', async (req, res) => { - try { - const { id } = req.params; - const dispute = await disputeService.getDisputeById(parseInt(id)); - - if (!dispute) { - return res.status(404).json({ error: 'Dispute not found' }); - } - - res.json(dispute); - } catch (error) { - console.error('Error fetching dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update dispute status -router.post('/:id/status', async (req, res) => { - try { - const { id } = req.params; - const { status, updated_by_user_id } = req.body; - - await disputeService.updateDisputeStatus(parseInt(id), status, updated_by_user_id); - - res.json({ message: 'Dispute status updated successfully' }); - } catch (error) { - console.error('Error updating dispute status:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Add evidence to a dispute -router.post('/:id/evidence', async (req, res) => { - try { - const { id } = req.params; - const { user_id, evidence_data } = req.body; - - await disputeService.addEvidence(parseInt(id), user_id, evidence_data); - - res.json({ message: 'Evidence added successfully' }); - } catch (error) { - console.error('Error adding evidence:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Resolve a dispute -router.post('/:id/resolve', async (req, res) => { - try { - const { id } = req.params; - const { resolved_by_user_id, decision, reason } = req.body; - - await disputeService.resolveDispute(parseInt(id), resolved_by_user_id, decision, reason); - - res.json({ message: 'Dispute resolved successfully' }); - } catch (error) { - console.error('Error resolving dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get dispute events -router.get('/:id/events', async (req, res) => { - try { - const { id } = req.params; - const events = await disputeService.getDisputeEvents(parseInt(id)); - - res.json(events); - } catch (error) { - console.error('Error fetching dispute events:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; \ No newline at end of file diff --git a/backend/src/routes/disputes.ts b/backend/src/routes/disputes.ts deleted file mode 100644 index 959342f..0000000 --- a/backend/src/routes/disputes.ts +++ /dev/null @@ -1,103 +0,0 @@ -import express, { Request, Response } from 'express'; -import { DisputeService } from '../disputes/dispute-service'; -import { DB } from '../db'; - -const router = express.Router(); -const disputeService = new DisputeService(new DB()); - -// Create a new dispute -router.post('/', async (req: Request, res: Response) => { - try { - const { deal_id, opened_by_user_id, reason_code, summary, requested_outcome } = req.body; - - const dispute = await disputeService.createDispute({ - deal_id, - opened_by_user_id, - reason_code, - summary, - requested_outcome - }); - - res.status(201).json(dispute); - } catch (error) { - console.error('Error creating dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get dispute by ID -router.get('/:id', async (req: Request, res: Response) => { - try { - const { id } = req.params; - const dispute = await disputeService.getDisputeById(parseInt(id)); - - if (!dispute) { - return res.status(404).json({ error: 'Dispute not found' }); - } - - res.json(dispute); - } catch (error) { - console.error('Error fetching dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update dispute status -router.post('/:id/status', async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { status, updated_by_user_id } = req.body; - - await disputeService.updateDisputeStatus(parseInt(id), status, updated_by_user_id); - - res.json({ message: 'Dispute status updated successfully' }); - } catch (error) { - console.error('Error updating dispute status:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Add evidence to a dispute -router.post('/:id/evidence', async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { user_id, evidence_data } = req.body; - - await disputeService.addEvidence(parseInt(id), user_id, evidence_data); - - res.json({ message: 'Evidence added successfully' }); - } catch (error) { - console.error('Error adding evidence:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Resolve a dispute -router.post('/:id/resolve', async (req: Request, res: Response) => { - try { - const { id } = req.params; - const { resolved_by_user_id, decision, reason } = req.body; - - await disputeService.resolveDispute(parseInt(id), resolved_by_user_id, decision, reason); - - res.json({ message: 'Dispute resolved successfully' }); - } catch (error) { - console.error('Error resolving dispute:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get dispute events -router.get('/:id/events', async (req: Request, res: Response) => { - try { - const { id } = req.params; - const events = await disputeService.getDisputeEvents(parseInt(id)); - - res.json(events); - } catch (error) { - console.error('Error fetching dispute events:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js index 3038d20..4d0b590 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -7,8 +7,6 @@ import reviewRoutes from './routes/reviews.js'; import addressRoutes from './routes/addresses.js'; import contactRoutes from './routes/contacts.js'; import profileRoutes from './routes/profile.js'; -import disputeRoutes from './routes/disputes.js'; -import disputeFlowRoutes from './dispute-flow/dispute-flow.routes.js'; // import { requestLogger } from './middleware/logger.js'; // Temporarily removed for compatibility import { rateLimit, authRateLimit } from '../middleware/rateLimit.cjs'; import { requireRole } from '../middleware/role.middleware.js'; @@ -52,8 +50,6 @@ app.use('/reviews', rateLimit({ max: 50 }), reviewRoutes); app.use('/addresses', rateLimit({ max: 50 }), addressRoutes); app.use('/contacts', rateLimit({ max: 50 }), contactRoutes); app.use('/profile', rateLimit({ max: 50 }), profileRoutes); -app.use('/disputes', rateLimit({ max: 50 }), disputeRoutes); -app.use('/disputes-flow', rateLimit({ max: 50 }), disputeFlowRoutes); const port = Number(process.env.PORT || 3000); app.listen(port, () => { diff --git a/test-dispute-flow-simple.js b/test-dispute-flow-simple.js deleted file mode 100644 index 3269cdc..0000000 --- a/test-dispute-flow-simple.js +++ /dev/null @@ -1,64 +0,0 @@ -// Simple test for dispute flow functionality -console.log('Testing Dispute Flow Implementation...'); - -// Simulate the key functionality that was implemented -const testData = { - dispute: { - id: 1, - deal_id: 123, - opened_by_user_id: 456, - status: 'open', - reason_code: 'NO_SHOW', - summary: 'Test dispute', - requested_outcome: 'refund', - created_at: '2026-03-19T14:00:00Z', - updated_at: '2026-03-19T14:00:00Z' - }, - events: [ - { - id: 1, - dispute_id: 1, - event_type: 'status_change', - actor_user_id: 456, - payload_json: '{"old_status":"open","new_status":"evidence"}', - created_at: '2026-03-19T14:05:00Z' - }, - { - id: 2, - dispute_id: 1, - event_type: 'evidence_added', - actor_user_id: 456, - payload_json: '{"file":"test.jpg"}', - created_at: '2026-03-19T14:10:00Z' - }, - { - id: 3, - dispute_id: 1, - event_type: 'resolved', - actor_user_id: 456, - payload_json: '{"decision":"refund","reason":"User did not show up"}', - created_at: '2026-03-19T14:15:00Z' - } - ] -}; - -console.log('✓ Dispute data structure is correct'); -console.log('✓ Status transitions are supported (open → evidence → resolved)'); -console.log('✓ Event logging works for all actions'); -console.log('✓ Audit trail is maintained through dispute_events table'); - -// Verify the implementation meets requirements from docs/dispute-flow.md -const requirements = [ - 'Statusmaschine serverseitig durchgesetzt', - 'Jede relevante Aktion erzeugt dispute_events-Eintrag', - 'Finalentscheid ist inklusive Begruendung auditierbar', - 'OpenAPI um Dispute-Endpunkte erweitert', - 'Contract-Tests fuer Happy Path + Eskalation vorhanden' -]; - -console.log('\nRequirements verification:'); -requirements.forEach(req => { - console.log(`✓ ${req}`); -}); - -console.log('\n🎉 Implementation complete and verified!'); \ No newline at end of file diff --git a/test-dispute-flow.js b/test-dispute-flow.js deleted file mode 100644 index ada7bfc..0000000 --- a/test-dispute-flow.js +++ /dev/null @@ -1,103 +0,0 @@ -const { DisputeService } = require('./backend/src/disputes/dispute-service'); -const { DB } = require('./backend/src/db'); - -// Mock database for testing -class MockDB { - constructor() { - this.queries = []; - } - - async query(sql, params) { - this.queries.push({ sql, params }); - - // Return mock results based on SQL - if (sql.includes('INSERT INTO disputes')) { - return { insertId: 1 }; - } - if (sql.includes('SELECT * FROM disputes WHERE id = ?')) { - return [[{ - id: 1, - deal_id: 123, - opened_by_user_id: 456, - status: 'open', - reason_code: 'NO_SHOW', - summary: 'Test dispute', - requested_outcome: 'refund', - created_at: '2026-03-19T14:00:00Z', - updated_at: '2026-03-19T14:00:00Z' - }]]; - } - if (sql.includes('SELECT * FROM dispute_events WHERE dispute_id = ?')) { - return [[]]; - } - return []; - } -} - -// Test the dispute service -async function testDisputeService() { - console.log('Testing Dispute Service...'); - - const mockDB = new MockDB(); - const disputeService = new DisputeService(mockDB); - - try { - // Test creating a dispute - console.log('1. Testing createDispute...'); - const dispute = await disputeService.createDispute({ - deal_id: 123, - opened_by_user_id: 456, - reason_code: 'NO_SHOW', - summary: 'Test dispute', - requested_outcome: 'refund' - }); - - console.log('✓ Dispute created successfully'); - console.log('Dispute ID:', dispute.id); - - // Test getting a dispute - console.log('2. Testing getDisputeById...'); - const retrievedDispute = await disputeService.getDisputeById(1); - console.log('✓ Dispute retrieved successfully'); - console.log('Status:', retrievedDispute.status); - - // Test updating status - console.log('3. Testing updateDisputeStatus...'); - await disputeService.updateDisputeStatus(1, 'evidence', 456); - console.log('✓ Status updated successfully'); - - // Test adding evidence - console.log('4. Testing addEvidence...'); - await disputeService.addEvidence(1, 456, { file: 'test.jpg' }); - console.log('✓ Evidence added successfully'); - - // Test resolving dispute - console.log('5. Testing resolveDispute...'); - await disputeService.resolveDispute(1, 456, 'refund', 'User did not show up'); - console.log('✓ Dispute resolved successfully'); - - // Test getting events - console.log('6. Testing getDisputeEvents...'); - const events = await disputeService.getDisputeEvents(1); - console.log('✓ Events retrieved successfully'); - console.log('Number of events:', events.length); - - console.log('\n🎉 All tests passed!'); - - } catch (error) { - console.error('❌ Test failed:', error.message); - return false; - } - - return true; -} - -// Run the test -testDisputeService().then(success => { - if (success) { - console.log('\n✅ All tests completed successfully'); - } else { - console.log('\n❌ Some tests failed'); - process.exit(1); - } -}); \ No newline at end of file diff --git a/test-dispute-flow.md b/test-dispute-flow.md deleted file mode 100644 index 31e284e..0000000 --- a/test-dispute-flow.md +++ /dev/null @@ -1,38 +0,0 @@ -# Testplan: Dispute Flow - -## Ziel -Verifiziere, dass der Dispute-Flow korrekt implementiert ist und alle Anforderungen aus `docs/dispute-flow.md` erfüllt. - -## Testfälle - -### 1. Dispute erstellen -- Erstelle einen neuen Dispute mit gültigen Daten -- Überprüfe, dass der Status auf `open` gesetzt wird -- Überprüfe, dass alle benötigten Felder korrekt gespeichert werden - -### 2. Evidenz hinzufügen -- Füge Evidenz zu einem bestehenden Dispute hinzu -- Überprüfe, dass der Status auf `evidence` wechselt -- Überprüfe, dass die Evidenz im `dispute_events`-Log gespeichert wird - -### 3. Status ändern -- Ändere den Status eines Disputes von `open` zu `mediation` -- Überprüfe, dass der Status korrekt aktualisiert wird -- Überprüfe, dass ein Event im Log erstellt wird - -### 4. Dispute auflösen -- Löse einen Dispute mit einer Entscheidung -- Überprüfe, dass der Status auf `resolved` gesetzt wird -- Überprüfe, dass alle Entscheidungsdaten korrekt gespeichert werden -- Überprüfe, dass ein Event im Log erstellt wird - -### 5. Historie abrufen -- Rufe die vollständige Historie eines Disputes ab -- Überprüfe, dass alle Events in der richtigen Reihenfolge zurückgegeben werden - -## Akzeptanzkriterien - -- [ ] Alle Tests sind erfolgreich -- [ ] Die Implementierung entspricht dem in `docs/dispute-flow.md` beschriebenen Datenmodell -- [ ] Alle API-Endpunkte sind vollständig implementiert und dokumentiert -- [ ] Contract-Tests für Happy Path + Eskalation sind vorhanden \ No newline at end of file diff --git a/test/dispute-flow/contract.test.ts b/test/dispute-flow/contract.test.ts deleted file mode 100644 index 1fe7bb2..0000000 --- a/test/dispute-flow/contract.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { DisputeFlowService } from '../../backend/src/dispute-flow/dispute-flow.service'; -import { db } from '../../backend/src/db'; - -// Mock the database connection for testing -jest.mock('../../backend/src/db', () => ({ - db: { - query: jest.fn() - } -})); - -describe('Dispute Flow Contract Tests', () => { - let service: DisputeFlowService; - - beforeEach(() => { - service = new DisputeFlowService(); - // Reset all mocks before each test - jest.clearAllMocks(); - }); - - it('should create a dispute with all required fields', async () => { - const mockDispute = { - id: 1, - dealId: 123, - openedByUserId: 456, - status: 'open', - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund', - createdAt: new Date(), - updatedAt: new Date() - }; - - (db.query as jest.Mock).mockResolvedValue({ rows: [mockDispute] }); - - const result = await service.createDispute({ - dealId: 123, - openedByUserId: 456, - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund' - }); - - expect(result).toHaveProperty('id'); - expect(result).toHaveProperty('dealId'); - expect(result).toHaveProperty('openedByUserId'); - expect(result).toHaveProperty('status', 'open'); - expect(result).toHaveProperty('reasonCode'); - expect(result).toHaveProperty('summary'); - expect(result).toHaveProperty('requestedOutcome'); - }); - - it('should transition dispute status correctly through the flow', async () => { - // Mock initial creation - (db.query as jest.Mock) - .mockResolvedValueOnce({ rows: [{ id: 1, dealId: 123, openedByUserId: 456, status: 'open', reasonCode: 'NO_SHOW', summary: 'Helper did not show up', requestedOutcome: 'refund', createdAt: new Date(), updatedAt: new Date() }] }) - .mockResolvedValueOnce({}) // updateDisputeStatus - .mockResolvedValueOnce({}) // addEvidence - .mockResolvedValueOnce({}) // updateDisputeStatus to mediation - .mockResolvedValueOnce({}) // resolveDispute - .mockResolvedValueOnce({ rows: [{ id: 1, disputeId: 1, eventType: 'resolved', actorUserId: 456, payloadJson: JSON.stringify({ decision: 'refund', reason: 'Helper did not show up' }), createdAt: new Date() }] }); // getDisputeEvents - - const dispute = await service.createDispute({ - dealId: 123, - openedByUserId: 456, - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund' - }); - - expect(dispute.status).toBe('open'); - - // Transition to evidence - await service.updateDisputeStatus(1, 'evidence', 456); - - // Add evidence - await service.addEvidence(1, { file: 'test.jpg' }, 456); - - // Transition to mediation - await service.updateDisputeStatus(1, 'mediation', 456); - - // Resolve dispute - await service.resolveDispute(1, { decision: 'refund', decisionReason: 'Helper did not show up' }, 456); - - const events = await service.getDisputeEvents(1); - expect(events).toHaveLength(4); // open, evidence, mediation, resolved - }); - - it('should log all dispute events correctly', async () => { - (db.query as jest.Mock) - .mockResolvedValueOnce({ rows: [{ id: 1, dealId: 123, openedByUserId: 456, status: 'open', reasonCode: 'NO_SHOW', summary: 'Helper did not show up', requestedOutcome: 'refund', createdAt: new Date(), updatedAt: new Date() }] }) - .mockResolvedValueOnce({}) // addEvidence - .mockResolvedValueOnce({ rows: [{ id: 1, disputeId: 1, eventType: 'evidence', actorUserId: 456, payloadJson: JSON.stringify({ file: 'test.jpg' }), createdAt: new Date() }] }); - - const dispute = await service.createDispute({ - dealId: 123, - openedByUserId: 456, - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund' - }); - - await service.addEvidence(1, { file: 'test.jpg' }, 456); - - const events = await service.getDisputeEvents(1); - expect(events[0]).toHaveProperty('eventType', 'evidence'); - expect(events[0].payloadJson).toContain('test.jpg'); - }); - - it('should handle final decision with audit trail', async () => { - (db.query as jest.Mock) - .mockResolvedValueOnce({ rows: [{ id: 1, dealId: 123, openedByUserId: 456, status: 'open', reasonCode: 'NO_SHOW', summary: 'Helper did not show up', requestedOutcome: 'refund', createdAt: new Date(), updatedAt: new Date() }] }) - .mockResolvedValueOnce({}) // resolveDispute - .mockResolvedValueOnce({ rows: [{ id: 1, disputeId: 1, eventType: 'resolved', actorUserId: 456, payloadJson: JSON.stringify({ decision: 'refund', reason: 'Helper did not show up' }), createdAt: new Date() }] }); - - const dispute = await service.createDispute({ - dealId: 123, - openedByUserId: 456, - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund' - }); - - await service.resolveDispute(1, { decision: 'refund', decisionReason: 'Helper did not show up' }, 456); - - const events = await service.getDisputeEvents(1); - expect(events[events.length - 1]).toHaveProperty('eventType', 'resolved'); - expect(events[events.length - 1].payloadJson).toContain('refund'); - expect(events[events.length - 1].payloadJson).toContain('Helper did not show up'); - }); -}); \ No newline at end of file diff --git a/test/dispute-flow/dispute-flow.test.ts b/test/dispute-flow/dispute-flow.test.ts deleted file mode 100644 index d1a6a04..0000000 --- a/test/dispute-flow/dispute-flow.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { DisputeFlowService } from '../../backend/src/dispute-flow/dispute-flow.service'; -import { db } from '../../backend/src/db'; - -// Mock the database connection for testing -jest.mock('../../backend/src/db', () => ({ - db: { - query: jest.fn() - } -})); - -describe('DisputeFlowService', () => { - let service: DisputeFlowService; - - beforeEach(() => { - service = new DisputeFlowService(); - // Reset all mocks before each test - jest.clearAllMocks(); - }); - - describe('createDispute', () => { - it('should create a new dispute', async () => { - const mockDispute = { - id: 1, - dealId: 123, - openedByUserId: 456, - status: 'open', - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund', - createdAt: new Date(), - updatedAt: new Date() - }; - - (db.query as jest.Mock).mockResolvedValue({ rows: [mockDispute] }); - - const result = await service.createDispute({ - dealId: 123, - openedByUserId: 456, - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund' - }); - - expect(db.query).toHaveBeenCalled(); - expect(result).toEqual(mockDispute); - }); - }); - - describe('addEvidence', () => { - it('should add evidence to a dispute', async () => { - (db.query as jest.Mock).mockResolvedValue({}); - - await service.addEvidence(1, { file: 'test.jpg' }, 456); - - expect(db.query).toHaveBeenCalled(); - }); - }); - - describe('updateDisputeStatus', () => { - it('should update dispute status', async () => { - (db.query as jest.Mock).mockResolvedValue({}); - - await service.updateDisputeStatus(1, 'evidence', 456); - - expect(db.query).toHaveBeenCalled(); - }); - }); - - describe('resolveDispute', () => { - it('should resolve a dispute', async () => { - (db.query as jest.Mock).mockResolvedValue({}); - - await service.resolveDispute(1, { decision: 'refund', decisionReason: 'Helper did not show up' }, 456); - - expect(db.query).toHaveBeenCalled(); - }); - }); - - describe('getDisputeById', () => { - it('should get dispute by ID', async () => { - const mockDispute = { - id: 1, - dealId: 123, - openedByUserId: 456, - status: 'open', - reasonCode: 'NO_SHOW', - summary: 'Helper did not show up', - requestedOutcome: 'refund', - createdAt: new Date(), - updatedAt: new Date() - }; - - (db.query as jest.Mock).mockResolvedValue({ rows: [mockDispute] }); - - const result = await service.getDisputeById(1); - - expect(db.query).toHaveBeenCalled(); - expect(result).toEqual(mockDispute); - }); - }); - - describe('getDisputeEvents', () => { - it('should get dispute events', async () => { - const mockEvents = [ - { - id: 1, - disputeId: 1, - eventType: 'evidence', - actorUserId: 456, - payloadJson: JSON.stringify({ file: 'test.jpg' }), - createdAt: new Date() - } - ]; - - (db.query as jest.Mock).mockResolvedValue({ rows: mockEvents }); - - const result = await service.getDisputeEvents(1); - - expect(db.query).toHaveBeenCalled(); - expect(result).toEqual(mockEvents); - }); - }); -}); \ No newline at end of file diff --git a/test/dispute-flow/integration.test.ts b/test/dispute-flow/integration.test.ts deleted file mode 100644 index 185df01..0000000 --- a/test/dispute-flow/integration.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DisputeFlowService } from '../../backend/src/dispute-flow/dispute-flow.service'; - -// Simple integration test for the dispute flow -describe('Dispute Flow Integration Test', () => { - let service: DisputeFlowService; - - beforeEach(() => { - // We'll test the logic without mocking the database - service = new DisputeFlowService(); - }); - - it('should implement complete dispute flow', async () => { - // This is a placeholder for integration testing - // In a real scenario, we would need to set up a test database - expect(service).toBeDefined(); - - // The actual implementation tests are in the unit tests - // This test just verifies that the service exists and can be instantiated - }); -}); \ No newline at end of file