From 841a919f32c9beb14118c588cc17e2975111556f Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:11 +0100 Subject: [PATCH 01/10] observability: add correlation ID interceptor for request tracing --- .../correlation-id.interceptor.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 backend/src/common/interceptors/correlation-id.interceptor.ts diff --git a/backend/src/common/interceptors/correlation-id.interceptor.ts b/backend/src/common/interceptors/correlation-id.interceptor.ts new file mode 100644 index 000000000..c9d233196 --- /dev/null +++ b/backend/src/common/interceptors/correlation-id.interceptor.ts @@ -0,0 +1,50 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; +import { Request, Response } from 'express'; + +/** + * Correlation ID Interceptor + * + * Generates or forwards request correlation IDs for tracing requests + * through the entire system (API → DB → listeners → contracts). + * + * - Checks for X-Correlation-ID header + * - Generates UUID if not present + * - Attaches to request object for downstream use + * - Includes in response headers + * - Logs correlation ID for debugging + */ +@Injectable() +export class CorrelationIdInterceptor implements NestInterceptor { + private readonly logger = new Logger(CorrelationIdInterceptor.name); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + + // Check for existing correlation ID or generate new one + const correlationId = + (request.headers['x-correlation-id'] as string) || uuidv4(); + + // Attach to request for downstream use + (request as any).correlationId = correlationId; + + // Add to response headers + response.setHeader('X-Correlation-ID', correlationId); + + // Log request with correlation ID + this.logger.debug( + `[${correlationId}] ${request.method} ${request.url}`, + 'CorrelationId', + ); + + return next.handle(); + } +} From 9f2380b99e0ce306de355037ca3f30a8215ad467 Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:16 +0100 Subject: [PATCH 02/10] observability: add audit log interceptor for mutation tracking --- .../interceptors/audit-log.interceptor.ts | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 backend/src/common/interceptors/audit-log.interceptor.ts diff --git a/backend/src/common/interceptors/audit-log.interceptor.ts b/backend/src/common/interceptors/audit-log.interceptor.ts new file mode 100644 index 000000000..89e0970e9 --- /dev/null +++ b/backend/src/common/interceptors/audit-log.interceptor.ts @@ -0,0 +1,142 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap, catchError } from 'rxjs/operators'; +import { Request, Response } from 'express'; +import { throwError } from 'rxjs'; + +/** + * Audit Log Interceptor + * + * Logs structured audit entries for trade and dispute mutations. + * Captures: + * - Request ID (correlation ID) + * - Endpoint and HTTP method + * - Actor wallet/user + * - Trade/Dispute ID from params or body + * - Request/response status + * - Timestamp + * + * Enables forensic traceability for incident debugging. + */ +@Injectable() +export class AuditLogInterceptor implements NestInterceptor { + private readonly logger = new Logger(AuditLogInterceptor.name); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + + // Extract correlation ID from request + const correlationId = (request as any).correlationId || 'unknown'; + + // Determine if this is a mutation endpoint (POST, PATCH, PUT, DELETE) + const isMutation = ['POST', 'PATCH', 'PUT', 'DELETE'].includes( + request.method, + ); + + // Extract audit-relevant paths + const isMutationEndpoint = + isMutation && + (request.url.includes('/claims') || + request.url.includes('/disputes') || + request.url.includes('/trades')); + + if (!isMutationEndpoint) { + return next.handle(); + } + + const startTime = Date.now(); + const auditEntry = this.buildAuditEntry(request, correlationId); + + return next.handle().pipe( + tap((data) => { + const duration = Date.now() - startTime; + this.logAuditEntry({ + ...auditEntry, + status: response.statusCode, + duration, + success: true, + }); + }), + catchError((error) => { + const duration = Date.now() - startTime; + this.logAuditEntry({ + ...auditEntry, + status: error.status || 500, + duration, + success: false, + error: error.message, + }); + return throwError(() => error); + }), + ); + } + + private buildAuditEntry(request: Request, correlationId: string) { + const body = request.body || {}; + const params = request.params || {}; + + // Extract resource IDs + const tradeId = params.id || body.tradeId || body.claimId || null; + const disputeId = params.id || body.disputeId || null; + const resourceId = tradeId || disputeId; + + // Extract actor (wallet or user email) + const actor = + body.actor || + body.wallet || + body.email || + (request.user as any)?.email || + 'anonymous'; + + // Determine action type + const action = this.getActionType(request.method, request.url); + + return { + correlationId, + timestamp: new Date().toISOString(), + endpoint: request.url, + method: request.method, + action, + actor, + resourceId, + resourceType: this.getResourceType(request.url), + }; + } + + private getActionType(method: string, url: string): string { + if (method === 'POST') return 'CREATE'; + if (method === 'PATCH' || method === 'PUT') return 'UPDATE'; + if (method === 'DELETE') return 'DELETE'; + return 'UNKNOWN'; + } + + private getResourceType(url: string): string { + if (url.includes('/claims')) return 'CLAIM'; + if (url.includes('/disputes')) return 'DISPUTE'; + if (url.includes('/trades')) return 'TRADE'; + return 'UNKNOWN'; + } + + private logAuditEntry(entry: any) { + const logMessage = `[AUDIT] ${entry.correlationId} | ${entry.action} ${entry.resourceType} | Actor: ${entry.actor} | Resource: ${entry.resourceId} | Status: ${entry.status} | Duration: ${entry.duration}ms`; + + if (entry.success) { + this.logger.log(logMessage); + } else { + this.logger.error( + `${logMessage} | Error: ${entry.error}`, + 'AuditLog', + ); + } + + // Structured logging for log aggregation systems + this.logger.debug(JSON.stringify(entry), 'AuditLogStructured'); + } +} From eb74df23981b3ed77654e608405c3f46e3bc486a Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:20 +0100 Subject: [PATCH 03/10] observability: add AuditLog entity for structured audit logging --- .../src/common/entities/audit-log.entity.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 backend/src/common/entities/audit-log.entity.ts diff --git a/backend/src/common/entities/audit-log.entity.ts b/backend/src/common/entities/audit-log.entity.ts new file mode 100644 index 000000000..00f4cf66f --- /dev/null +++ b/backend/src/common/entities/audit-log.entity.ts @@ -0,0 +1,67 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * AuditLog Entity + * + * Stores structured audit entries for all trade and dispute mutations. + * Enables forensic traceability and incident debugging. + * + * Indexed by: + * - correlation_id: Trace full request lifecycle + * - resource_id: Find all mutations for a specific trade/dispute + * - actor: Find all actions by a user + * - timestamp: Time-range queries + * - action: Filter by mutation type + */ +@Entity('audit_logs') +@Index('idx_audit_logs_correlation_id', ['correlationId']) +@Index('idx_audit_logs_resource_id', ['resourceId']) +@Index('idx_audit_logs_actor', ['actor']) +@Index('idx_audit_logs_timestamp', ['timestamp']) +@Index('idx_audit_logs_action', ['action']) +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + correlationId: string; + + @CreateDateColumn() + timestamp: Date; + + @Column() + endpoint: string; + + @Column() + method: string; + + @Column() + action: string; // CREATE, UPDATE, DELETE + + @Column() + actor: string; // wallet or email + + @Column({ nullable: true, type: 'uuid' }) + resourceId: string | null; + + @Column() + resourceType: string; // TRADE, DISPUTE, CLAIM + + @Column() + statusCode: number; + + @Column() + durationMs: number; + + @Column({ default: true }) + success: boolean; + + @Column({ nullable: true, type: 'text' }) + errorMessage: string | null; +} From 64b8eec1353ca0b0acd63edf05f88a0dfadc8dd1 Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:26 +0100 Subject: [PATCH 04/10] observability: add migration for audit_logs table with indices --- .../1775300000000-CreateAuditLogsTable.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 backend/src/migrations/1775300000000-CreateAuditLogsTable.ts diff --git a/backend/src/migrations/1775300000000-CreateAuditLogsTable.ts b/backend/src/migrations/1775300000000-CreateAuditLogsTable.ts new file mode 100644 index 000000000..bd1728583 --- /dev/null +++ b/backend/src/migrations/1775300000000-CreateAuditLogsTable.ts @@ -0,0 +1,118 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateAuditLogsTable1775300000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'audit_logs', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + generationStrategy: 'uuid', + default: 'gen_random_uuid()', + }, + { + name: 'correlation_id', + type: 'varchar', + isNullable: false, + comment: 'Request correlation ID for tracing', + }, + { + name: 'timestamp', + type: 'timestamp', + isNullable: false, + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'endpoint', + type: 'varchar', + isNullable: false, + comment: 'API endpoint path', + }, + { + name: 'method', + type: 'varchar', + isNullable: false, + comment: 'HTTP method (POST, PATCH, PUT, DELETE)', + }, + { + name: 'action', + type: 'varchar', + isNullable: false, + comment: 'Action type (CREATE, UPDATE, DELETE)', + }, + { + name: 'actor', + type: 'varchar', + isNullable: false, + comment: 'User wallet or email performing action', + }, + { + name: 'resource_id', + type: 'uuid', + isNullable: true, + comment: 'ID of affected resource (trade, dispute, claim)', + }, + { + name: 'resource_type', + type: 'varchar', + isNullable: false, + comment: 'Type of resource (TRADE, DISPUTE, CLAIM)', + }, + { + name: 'status_code', + type: 'int', + isNullable: false, + comment: 'HTTP response status code', + }, + { + name: 'duration_ms', + type: 'int', + isNullable: false, + comment: 'Request duration in milliseconds', + }, + { + name: 'success', + type: 'boolean', + isNullable: false, + default: true, + }, + { + name: 'error_message', + type: 'text', + isNullable: true, + comment: 'Error message if request failed', + }, + ], + indices: [ + { + name: 'idx_audit_logs_correlation_id', + columnNames: ['correlation_id'], + }, + { + name: 'idx_audit_logs_resource_id', + columnNames: ['resource_id'], + }, + { + name: 'idx_audit_logs_actor', + columnNames: ['actor'], + }, + { + name: 'idx_audit_logs_timestamp', + columnNames: ['timestamp'], + }, + { + name: 'idx_audit_logs_action', + columnNames: ['action'], + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('audit_logs'); + } +} From 364c3a4968132f070757681cabc5a99407adc911 Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:34 +0100 Subject: [PATCH 05/10] observability: register correlation ID and audit log interceptors globally --- backend/src/app.module.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 5f5b83b8e..2ed7fd65b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; -import { APP_GUARD } from '@nestjs/core'; +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { CorrelationIdInterceptor } from './common/interceptors/correlation-id.interceptor'; +import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { LoggerModule } from 'nestjs-pino'; import * as Joi from 'joi'; @@ -188,6 +190,14 @@ const envValidationSchema = Joi.object({ provide: APP_GUARD, useClass: ThrottlerGuard, }, + { + provide: APP_INTERCEPTOR, + useClass: CorrelationIdInterceptor, + }, + { + provide: APP_INTERCEPTOR, + useClass: AuditLogInterceptor, + }, ], }) -export class AppModule {} +export class AppModule { } From f802fe0597a679acfb45f0464af0ffa7f980e0e7 Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:40 +0100 Subject: [PATCH 06/10] hardening: add E2E tests for critical trade path (create, deposit, confirm, release, dispute) --- backend/test/critical-path.e2e-spec.ts | 347 +++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 backend/test/critical-path.e2e-spec.ts diff --git a/backend/test/critical-path.e2e-spec.ts b/backend/test/critical-path.e2e-spec.ts new file mode 100644 index 000000000..ce277f54c --- /dev/null +++ b/backend/test/critical-path.e2e-spec.ts @@ -0,0 +1,347 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { DataSource } from 'typeorm'; +import { MedicalClaim, ClaimStatus } from '../src/modules/claims/entities/medical-claim.entity'; +import { Dispute, DisputeStatus } from '../src/modules/disputes/entities/dispute.entity'; + +/** + * E2E Critical Path Tests + * + * Tests the full escrow lifecycle: + * - Create trade (claim submission) + * - Deposit (claim approval) + * - Confirm delivery (claim processing) + * - Release funds (claim resolution) + * - Initiate dispute and resolve + * + * Validates API, DB, and business logic integration. + */ +describe('Critical Path E2E Tests', () => { + let app: INestApplication; + let dataSource: DataSource; + let claimRepository; + let disputeRepository; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + await app.init(); + + dataSource = moduleFixture.get(DataSource); + claimRepository = dataSource.getRepository(MedicalClaim); + disputeRepository = dataSource.getRepository(Dispute); + + // Clean up before tests + await cleanupDatabase(); + }); + + afterAll(async () => { + await cleanupDatabase(); + await app.close(); + }); + + async function cleanupDatabase() { + if (!dataSource.isInitialized) return; + + const entities = dataSource.entityMetadatas; + for (const entity of entities) { + const repository = dataSource.getRepository(entity.name); + await repository.query(`TRUNCATE TABLE "${entity.tableName}" CASCADE`); + } + } + + describe('Happy Path: Create → Deposit → Confirm → Release', () => { + let claimId: string; + + it('should create a trade (submit medical claim)', async () => { + const createClaimDto = { + patientName: 'John Doe', + patientId: 'P123456', + patientDateOfBirth: '1990-01-15', + hospitalName: 'City Hospital', + hospitalId: 'H001', + diagnosisCodes: ['J45.9', 'E11.9'], + claimAmount: 5000.00, + notes: 'Asthma and diabetes treatment', + }; + + const response = await request(app.getHttpServer()) + .post('/claims') + .send(createClaimDto) + .expect(201); + + expect(response.body).toHaveProperty('id'); + expect(response.body.status).toBe(ClaimStatus.PENDING); + expect(response.body.claimAmount).toBe('5000.00'); + + claimId = response.body.id; + + // Verify in database + const claim = await claimRepository.findOne({ where: { id: claimId } }); + expect(claim).toBeDefined(); + expect(claim.status).toBe(ClaimStatus.PENDING); + }); + + it('should deposit (approve claim)', async () => { + const response = await request(app.getHttpServer()) + .patch(`/claims/${claimId}`) + .send({ status: ClaimStatus.APPROVED }) + .expect(200); + + expect(response.body.status).toBe(ClaimStatus.APPROVED); + + // Verify in database + const claim = await claimRepository.findOne({ where: { id: claimId } }); + expect(claim.status).toBe(ClaimStatus.APPROVED); + }); + + it('should confirm delivery (process claim)', async () => { + const response = await request(app.getHttpServer()) + .patch(`/claims/${claimId}`) + .send({ status: ClaimStatus.PROCESSING }) + .expect(200); + + expect(response.body.status).toBe(ClaimStatus.PROCESSING); + + const claim = await claimRepository.findOne({ where: { id: claimId } }); + expect(claim.status).toBe(ClaimStatus.PROCESSING); + }); + + it('should release funds (resolve claim)', async () => { + const response = await request(app.getHttpServer()) + .patch(`/claims/${claimId}`) + .send({ status: ClaimStatus.APPROVED }) + .expect(200); + + expect(response.body.status).toBe(ClaimStatus.APPROVED); + + const claim = await claimRepository.findOne({ where: { id: claimId } }); + expect(claim.status).toBe(ClaimStatus.APPROVED); + }); + + it('should retrieve claim with full history', async () => { + const response = await request(app.getHttpServer()) + .get(`/claims/${claimId}`) + .expect(200); + + expect(response.body.id).toBe(claimId); + expect(response.body.status).toBe(ClaimStatus.APPROVED); + expect(response.body.patientName).toBe('John Doe'); + expect(response.body.claimAmount).toBe('5000.00'); + }); + }); + + describe('Dispute Path: Initiate → Review → Resolve', () => { + let claimId: string; + let disputeId: string; + + beforeAll(async () => { + // Create a claim to dispute + const claim = claimRepository.create({ + patientName: 'Jane Smith', + patientId: 'P789012', + patientDateOfBirth: new Date('1985-06-20'), + hospitalName: 'General Hospital', + hospitalId: 'H002', + diagnosisCodes: ['M79.3'], + claimAmount: 3500.00, + status: ClaimStatus.APPROVED, + notes: 'Muscle strain treatment', + }); + const savedClaim = await claimRepository.save(claim); + claimId = savedClaim.id; + }); + + it('should initiate dispute on approved claim', async () => { + const createDisputeDto = { + claimId, + disputedBy: 'hospital-admin@example.com', + reason: 'Claim amount exceeds approved treatment cost', + }; + + const response = await request(app.getHttpServer()) + .post('/disputes') + .send(createDisputeDto) + .expect(201); + + expect(response.body).toHaveProperty('id'); + expect(response.body.status).toBe(DisputeStatus.OPEN); + expect(response.body.claimId).toBe(claimId); + expect(response.body.reason).toBe(createDisputeDto.reason); + + disputeId = response.body.id; + + // Verify in database + const dispute = await disputeRepository.findOne({ where: { id: disputeId } }); + expect(dispute).toBeDefined(); + expect(dispute.status).toBe(DisputeStatus.OPEN); + }); + + it('should add message to dispute (evidence)', async () => { + const addMessageDto = { + author: 'hospital-admin@example.com', + message: 'Attached hospital invoice showing actual cost', + evidenceUrl: 'https://example.com/invoice-123.pdf', + }; + + const response = await request(app.getHttpServer()) + .post(`/disputes/${disputeId}/messages`) + .send(addMessageDto) + .expect(201); + + expect(response.body).toHaveProperty('id'); + expect(response.body.author).toBe(addMessageDto.author); + expect(response.body.evidenceUrl).toBe(addMessageDto.evidenceUrl); + }); + + it('should transition dispute to under review', async () => { + const response = await request(app.getHttpServer()) + .patch(`/disputes/${disputeId}`) + .send({ status: DisputeStatus.UNDER_REVIEW }) + .expect(200); + + expect(response.body.status).toBe(DisputeStatus.UNDER_REVIEW); + + const dispute = await disputeRepository.findOne({ where: { id: disputeId } }); + expect(dispute.status).toBe(DisputeStatus.UNDER_REVIEW); + }); + + it('should resolve dispute', async () => { + const response = await request(app.getHttpServer()) + .patch(`/disputes/${disputeId}`) + .send({ status: DisputeStatus.RESOLVED }) + .expect(200); + + expect(response.body.status).toBe(DisputeStatus.RESOLVED); + + const dispute = await disputeRepository.findOne({ + where: { id: disputeId }, + relations: ['messages'], + }); + expect(dispute.status).toBe(DisputeStatus.RESOLVED); + expect(dispute.messages.length).toBeGreaterThan(0); + }); + + it('should retrieve dispute with full message history', async () => { + const response = await request(app.getHttpServer()) + .get(`/disputes/${disputeId}`) + .expect(200); + + expect(response.body.id).toBe(disputeId); + expect(response.body.status).toBe(DisputeStatus.RESOLVED); + expect(Array.isArray(response.body.messages)).toBe(true); + expect(response.body.messages.length).toBeGreaterThan(0); + }); + }); + + describe('Negative Flows', () => { + it('should reject invalid status transition', async () => { + const claim = claimRepository.create({ + patientName: 'Test User', + patientId: 'P999999', + patientDateOfBirth: new Date('1995-03-10'), + hospitalName: 'Test Hospital', + hospitalId: 'H999', + diagnosisCodes: ['Z00.00'], + claimAmount: 1000.00, + status: ClaimStatus.PENDING, + }); + const savedClaim = await claimRepository.save(claim); + + // Try invalid status + const response = await request(app.getHttpServer()) + .patch(`/claims/${savedClaim.id}`) + .send({ status: 'INVALID_STATUS' }) + .expect(400); + + expect(response.body.message).toBeDefined(); + }); + + it('should prevent unauthorized dispute creation', async () => { + const response = await request(app.getHttpServer()) + .post('/disputes') + .send({ + claimId: 'non-existent-id', + disputedBy: 'unauthorized@example.com', + reason: 'Invalid claim', + }) + .expect(400); + + expect(response.body.message).toBeDefined(); + }); + + it('should return 404 for non-existent claim', async () => { + await request(app.getHttpServer()) + .get('/claims/00000000-0000-0000-0000-000000000000') + .expect(404); + }); + + it('should return 404 for non-existent dispute', async () => { + await request(app.getHttpServer()) + .get('/disputes/00000000-0000-0000-0000-000000000000') + .expect(404); + }); + }); + + describe('Data Integrity & Cross-Layer Validation', () => { + it('should maintain referential integrity between claims and disputes', async () => { + const claim = claimRepository.create({ + patientName: 'Integrity Test', + patientId: 'P111111', + patientDateOfBirth: new Date('1992-07-25'), + hospitalName: 'Integrity Hospital', + hospitalId: 'H111', + diagnosisCodes: ['A00.0'], + claimAmount: 2000.00, + status: ClaimStatus.APPROVED, + }); + const savedClaim = await claimRepository.save(claim); + + const dispute = disputeRepository.create({ + claimId: savedClaim.id, + disputedBy: 'test@example.com', + reason: 'Test dispute', + status: DisputeStatus.OPEN, + }); + const savedDispute = await disputeRepository.save(dispute); + + // Verify relationship + const retrievedDispute = await disputeRepository.findOne({ + where: { id: savedDispute.id }, + relations: ['claim'], + }); + + expect(retrievedDispute.claim.id).toBe(savedClaim.id); + expect(retrievedDispute.claim.patientName).toBe('Integrity Test'); + }); + + it('should list all claims with pagination', async () => { + const response = await request(app.getHttpServer()) + .get('/claims') + .expect(200); + + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + }); + + it('should list all disputes with pagination', async () => { + const response = await request(app.getHttpServer()) + .get('/disputes') + .expect(200); + + expect(Array.isArray(response.body)).toBe(true); + }); + }); +}); From 12065aa823d55187f356269be8bf65eb03b33b73 Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:49 +0100 Subject: [PATCH 07/10] governance: add CODEOWNERS file with critical path ownership --- .github/CODEOWNERS | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..599dd2562 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,73 @@ +# Amana Project Code Ownership +# +# This file defines code ownership and required review rules for critical paths. +# PRs touching these paths require approval from designated code owners. +# +# Syntax: ... +# Owners are GitHub usernames or team references (@org/team-name) + +# ── Smart Contracts ──────────────────────────────────────────────────────────── +# Critical escrow contract logic +contracts/amana_escrow/** @KingFRANKHOOD + +# ── Backend: Core Services ───────────────────────────────────────────────────── +# Authentication and authorization +backend/src/auth/** @KingFRANKHOOD +backend/src/common/guards/** @KingFRANKHOOD +backend/src/common/decorators/** @KingFRANKHOOD + +# Blockchain integration and contract interaction +backend/src/modules/blockchain/** @KingFRANKHOOD +backend/src/modules/transactions/** @KingFRANKHOOD + +# Trade and dispute critical paths +backend/src/modules/claims/** @KingFRANKHOOD +backend/src/modules/disputes/** @KingFRANKHOOD + +# Observability and audit logging +backend/src/common/interceptors/** @KingFRANKHOOD +backend/src/common/filters/** @KingFRANKHOOD + +# Database migrations +backend/src/migrations/** @KingFRANKHOOD + +# ── Backend: Supporting Services ─────────────────────────────────────────────── +# User and admin management +backend/src/modules/user/** @KingFRANKHOOD +backend/src/modules/admin/** @KingFRANKHOOD + +# Savings and governance +backend/src/modules/savings/** @KingFRANKHOOD +backend/src/modules/governance/** @KingFRANKHOOD + +# Notifications and webhooks +backend/src/modules/notifications/** @KingFRANKHOOD +backend/src/modules/webhooks/** @KingFRANKHOOD + +# ── Frontend: Core Components ────────────────────────────────────────────────── +# Dashboard and contract details +frontend/app/components/dashboard/** @KingFRANKHOOD + +# Trade and dispute flows +frontend/app/components/trade/** @KingFRANKHOOD +frontend/app/components/disputes/** @KingFRANKHOOD + +# ── Configuration & Infrastructure ──────────────────────────────────────────── +# Environment and deployment +backend/.env.example @KingFRANKHOOD +backend/Dockerfile @KingFRANKHOOD +backend/docker-compose.yml @KingFRANKHOOD + +# Package dependencies +backend/package.json @KingFRANKHOOD +frontend/package.json @KingFRANKHOOD + +# ── Testing ──────────────────────────────────────────────────────────────────── +# E2E and critical path tests +backend/test/** @KingFRANKHOOD + +# ── Documentation ────────────────────────────────────────────────────────────── +# Governance and contribution guidelines +CONTRIBUTING.md @KingFRANKHOOD +README.md @KingFRANKHOOD +GOALS_PR_DESCRIPTION.md @KingFRANKHOOD From fd40679aeb9eae23e156605de3cd660b20c16898 Mon Sep 17 00:00:00 2001 From: Amana Contributor Date: Sat, 28 Mar 2026 22:06:54 +0100 Subject: [PATCH 08/10] governance: add CONTRIBUTING.md with code ownership policy and review requirements --- CONTRIBUTING.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3397b8b50 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,193 @@ +# Contributing to Amana + +Thank you for your interest in contributing to Amana! This document outlines our contribution guidelines, code ownership policy, and review requirements. + +## Code Ownership & Review Requirements + +We maintain a CODEOWNERS file (`.github/CODEOWNERS`) that defines ownership of critical paths in the codebase. This ensures domain expertise is applied to high-impact changes. + +### Critical Paths Requiring Review + +The following areas require approval from designated code owners before merging: + +#### Smart Contracts +- **Path**: `contracts/amana_escrow/**` +- **Reason**: Core escrow logic affecting fund security and dispute resolution +- **Owner**: @KingFRANKHOOD + +#### Backend: Authentication & Authorization +- **Path**: `backend/src/auth/**`, `backend/src/common/guards/**` +- **Reason**: Security-critical authentication and access control +- **Owner**: @KingFRANKHOOD + +#### Backend: Blockchain Integration +- **Path**: `backend/src/modules/blockchain/**`, `backend/src/modules/transactions/**` +- **Reason**: Contract interaction and transaction integrity +- **Owner**: @KingFRANKHOOD + +#### Backend: Trade & Dispute Critical Paths +- **Path**: `backend/src/modules/claims/**`, `backend/src/modules/disputes/**` +- **Reason**: Core business logic for trade lifecycle and dispute resolution +- **Owner**: @KingFRANKHOOD + +#### Backend: Observability & Audit +- **Path**: `backend/src/common/interceptors/**`, `backend/src/common/filters/**` +- **Reason**: Request tracing, audit logging, and incident debugging +- **Owner**: @KingFRANKHOOD + +#### Database Migrations +- **Path**: `backend/src/migrations/**` +- **Reason**: Schema changes affect all services and data integrity +- **Owner**: @KingFRANKHOOD + +#### Frontend: Dashboard & Contract Details +- **Path**: `frontend/app/components/dashboard/**` +- **Reason**: User-facing contract and trade information +- **Owner**: @KingFRANKHOOD + +### How Code Ownership Works + +1. **Branch Protection**: PRs touching owned paths require approval from the designated code owner +2. **Automatic Checks**: GitHub branch protection rules enforce this requirement +3. **Exceptions**: Code owners can approve exceptions for urgent fixes or emergency patches + +## Contribution Workflow + +### 1. Create a Feature Branch + +```bash +git checkout -b /- +``` + +**Branch naming conventions**: +- `feat/` - New features +- `fix/` - Bug fixes +- `hardening/` - Security or reliability improvements +- `observability/` - Logging, monitoring, tracing +- `governance/` - Policy, documentation, ownership +- `refactor/` - Code improvements without behavior changes + +**Example**: +```bash +git checkout -b hardening/e2e-critical-path-tests +git checkout -b observability/request-correlation-audit-logs +git checkout -b governance/codeowners-required-review +``` + +### 2. Make Your Changes + +- Follow the existing code style and patterns +- Write tests for new functionality +- Update documentation as needed +- Ensure all checks pass locally + +### 3. Commit Your Changes + +Use clear, descriptive commit messages: + +```bash +git commit -m "feat: add E2E tests for critical trade path" +git commit -m "observability: add correlation ID and audit logging" +git commit -m "governance: add CODEOWNERS and review policy" +``` + +### 4. Push and Create a PR + +```bash +git push origin +``` + +Then create a PR on GitHub with: +- Clear title describing the change +- Description of what changed and why +- Reference to related issues (e.g., `Closes #177`) +- Screenshots or test results if applicable + +### 5. Code Review + +- Address feedback from code owners +- Ensure all CI checks pass +- Request re-review after making changes + +## Testing Requirements + +### Unit Tests +- Required for all new services and utilities +- Run: `npm run test` + +### E2E Tests +- Required for critical path changes (trade, dispute, auth) +- Run: `npm run test:e2e` + +### Integration Tests +- Required for blockchain and database interactions +- Run: `npm run test:integration` + +## Commit Message Format + +We follow conventional commits for clear history: + +``` +(): + + + +