import { BadRequestException } from '@nestjs/common'; import type { DataSource } from 'typeorm'; import { ClearanceMilestoneService } from './clearance-milestone.service'; import type { ClearanceMilestone } from './entities/clearance-milestone.entity'; type Status = 'PENDING' | 'COMPLETED' | 'SKIPPED'; /** * Risk assignment is gated on the T1 being closed (catalog order * T1_CLOSED → RISK_ASSIGNED): customs cannot rate cargo still under transit. */ function makeService(t1Status: Status | 'MISSING') { const rows = new Map(); if (t1Status !== 'MISSING') { rows.set('T1_CLOSED', { milestoneCode: 'T1_CLOSED', status: t1Status } as ClearanceMilestone); } const risk = { milestoneCode: 'RISK_ASSIGNED', status: 'PENDING' } as ClearanceMilestone; rows.set('RISK_ASSIGNED', risk); const repo = { findOne: jest.fn(({ where }: { where: { milestoneCode: string } }) => Promise.resolve(rows.get(where.milestoneCode) ?? null), ), save: jest.fn((m: ClearanceMilestone) => Promise.resolve(m)), }; const dataSource = { getRepository: () => repo } as unknown as DataSource; return { service: new ClearanceMilestoneService(dataSource), repo, risk }; } describe('ClearanceMilestoneService.assignRisk', () => { it('rejects the assignment while the T1 is still open', async () => { const { service, repo } = makeService('PENDING'); await expect(service.assignRisk('b-1', 'GREEN')).rejects.toBeInstanceOf( BadRequestException, ); expect(repo.save).not.toHaveBeenCalled(); }); it('rejects the assignment when the booking has no T1_CLOSED milestone', async () => { const { service, repo } = makeService('MISSING'); await expect(service.assignRisk('b-1', 'GREEN')).rejects.toBeInstanceOf( BadRequestException, ); expect(repo.save).not.toHaveBeenCalled(); }); it('assigns the risk level once the T1 is closed', async () => { const { service, risk } = makeService('COMPLETED'); const saved = await service.assignRisk('b-1', 'RED', 'user-1'); expect(saved.status).toBe('COMPLETED'); expect(saved.metadata?.riskLevel).toBe('RED'); expect(risk.triggeredByUserId).toBe('user-1'); }); it('assigns the risk level when the T1 step was skipped', async () => { const { service } = makeService('SKIPPED'); const saved = await service.assignRisk('b-1', 'YELLOW'); expect(saved.status).toBe('COMPLETED'); expect(saved.metadata?.riskLevel).toBe('YELLOW'); }); /** * The level is customer-visible and stays correctable until duty is advised, * so a changed level must leave a trail rather than overwrite the last one. */ describe('risk history', () => { it('records the first assignment with no previous level', async () => { const { service } = makeService('COMPLETED'); const saved = await service.assignRisk('b-1', 'RED', 'user-1', 'initial rating', 'Abebe K.'); expect(saved.metadata?.riskHistory).toHaveLength(1); expect(saved.metadata?.riskHistory?.[0]).toMatchObject({ level: 'RED', assignedByUserId: 'user-1', assignedBy: 'Abebe K.', note: 'initial rating', }); expect(saved.metadata?.riskHistory?.[0]).not.toHaveProperty('previousLevel'); }); it('keeps the earlier decision when the level is reassigned', async () => { const { service } = makeService('COMPLETED'); await service.assignRisk('b-1', 'RED', 'user-1', undefined, 'Abebe K.'); const saved = await service.assignRisk('b-1', 'GREEN', 'user-2', 'downgraded', 'Sara M.'); expect(saved.metadata?.riskLevel).toBe('GREEN'); expect(saved.metadata?.riskHistory).toHaveLength(2); // The original RED decision survives, with who made it. expect(saved.metadata?.riskHistory?.[0]).toMatchObject({ level: 'RED', assignedBy: 'Abebe K.', }); expect(saved.metadata?.riskHistory?.[1]).toMatchObject({ level: 'GREEN', previousLevel: 'RED', assignedByUserId: 'user-2', assignedBy: 'Sara M.', note: 'downgraded', }); }); it('keeps the whole chain across several reassignments, oldest first', async () => { const { service } = makeService('COMPLETED'); await service.assignRisk('b-1', 'GREEN'); await service.assignRisk('b-1', 'YELLOW'); const saved = await service.assignRisk('b-1', 'RED'); expect(saved.metadata?.riskHistory?.map((e) => e.level)).toEqual([ 'GREEN', 'YELLOW', 'RED', ]); }); it('does not record a repeat of the level already assigned', async () => { const { service } = makeService('COMPLETED'); await service.assignRisk('b-1', 'GREEN'); const saved = await service.assignRisk('b-1', 'GREEN'); expect(saved.metadata?.riskHistory).toHaveLength(1); }); it('always leaves riskLevel equal to the last history entry', async () => { const { service } = makeService('COMPLETED'); await service.assignRisk('b-1', 'RED'); const saved = await service.assignRisk('b-1', 'YELLOW'); const history = saved.metadata?.riskHistory ?? []; expect(saved.metadata?.riskLevel).toBe(history[history.length - 1]?.level); }); }); });