import { BadRequestException } from '@nestjs/common'; import { ContractClearanceService } from './contract-clearance.service'; import type { Contract } from './entities/contract.entity'; /** * Pre-declaration transit-assignee handshake. GL Ethiopia asks Djibouti who will * handle the shipment in transit; Djibouti answers with a name. The customs * declaration stays shut until that name exists, and Djibouti may send a * different one later. */ describe('ContractClearanceService — transit assignee', () => { const contract = (over: Partial = {}): Contract => ({ id: 'ctr-1', reference: 'CTR-2026-00042', tradeDirection: 'IMPORT', customsClearingEnabled: true, contractKind: 'ONE_TIME', ...over, }) as Contract; let repo: { currentCycle: jest.Mock; updateCycle: jest.Mock }; let contractsService: { findById: jest.Mock }; let notifier: { transitAssigneeRequested: jest.Mock; transitAssigneeAssigned: jest.Mock; }; let transitAgentsService: { getAssignable: jest.Mock }; let service: ContractClearanceService; const cycle = (over: Record = {}) => ({ id: 'cyc-1', transitAssigneeRequestedAt: null, transitAssigneeName: null, ...over, }); beforeEach(() => { repo = { currentCycle: jest.fn().mockResolvedValue(cycle()), updateCycle: jest.fn().mockResolvedValue(undefined), }; contractsService = { findById: jest.fn().mockResolvedValue(contract()) }; notifier = { transitAssigneeRequested: jest.fn(), transitAssigneeAssigned: jest.fn(), }; transitAgentsService = { getAssignable: jest.fn().mockResolvedValue({ id: 'agent-1', name: 'Ahmed Bourhan' }), }; service = new ContractClearanceService( repo as never, contractsService as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, notifier as never, transitAgentsService as never, {} as never, // dataSource ); }); describe('request (GL Ethiopia)', () => { it('stamps the ask and pings Djibouti', async () => { await service.requestTransitAssignee('ctr-1', ' Reefer, needs a cold-chain officer ', 'et-1'); const patch = repo.updateCycle.mock.calls[0][1]; expect(patch.transitAssigneeRequestedAt).toBeInstanceOf(Date); expect(patch.transitAssigneeRequestedByUserId).toBe('et-1'); expect(patch.transitAssigneeRequestNote).toBe( 'Reefer, needs a cold-chain officer', ); expect(notifier.transitAssigneeRequested).toHaveBeenCalled(); }); }); describe('assign (GL Djibouti)', () => { it('records the officer and tells Ethiopia they can proceed', async () => { repo.currentCycle.mockResolvedValue( cycle({ transitAssigneeRequestedAt: new Date() }), ); await service.assignTransitAssignee('ctr-1', 'agent-1', 'dj-1'); expect(transitAgentsService.getAssignable).toHaveBeenCalledWith('agent-1'); const patch = repo.updateCycle.mock.calls[0][1]; expect(patch.transitAssigneeName).toBe('Ahmed Bourhan'); expect(patch.transitAssigneeAssignedByUserId).toBe('dj-1'); expect(notifier.transitAssigneeAssigned).toHaveBeenCalledWith( expect.objectContaining({ id: 'ctr-1' }), 'Ahmed Bourhan', null, ); }); it('reassigns, carrying the previous name into the notice', async () => { repo.currentCycle.mockResolvedValue( cycle({ transitAssigneeRequestedAt: new Date(), transitAssigneeName: 'Ahmed Bourhan', }), ); transitAgentsService.getAssignable.mockResolvedValue({ id: 'agent-2', name: 'Fatouma Ali', }); await service.assignTransitAssignee('ctr-1', 'agent-2', 'dj-1'); expect(notifier.transitAssigneeAssigned).toHaveBeenCalledWith( expect.anything(), 'Fatouma Ali', 'Ahmed Bourhan', ); }); it('refuses a suspended or out-of-window agent', async () => { repo.currentCycle.mockResolvedValue( cycle({ transitAssigneeRequestedAt: new Date() }), ); transitAgentsService.getAssignable.mockRejectedValue( new BadRequestException('suspended'), ); await expect( service.assignTransitAssignee('ctr-1', 'agent-1', 'dj-1'), ).rejects.toBeInstanceOf(BadRequestException); }); it('refuses before Ethiopia has asked', async () => { await expect( service.assignTransitAssignee('ctr-1', 'agent-1', 'dj-1'), ).rejects.toThrow(/not requested/i); expect(transitAgentsService.getAssignable).not.toHaveBeenCalled(); }); }); describe('declaration gate', () => { const ensure = (c: Contract) => ( service as unknown as { ensureDeclarationPrerequisites: (id: string, c: Contract) => Promise; } ).ensureDeclarationPrerequisites('ctr-1', c); beforeEach(() => { // Documents are approved; only the assignee decides the outcome here. ( service as unknown as { isClearanceFullyApproved: unknown } ).isClearanceFullyApproved = jest.fn().mockResolvedValue(true); }); it('tells GL to raise the request when none exists', async () => { await expect(ensure(contract())).rejects.toThrow( /Request a transit assignee/i, ); }); it('tells GL to wait when Djibouti has not answered', async () => { repo.currentCycle.mockResolvedValue( cycle({ transitAssigneeRequestedAt: new Date() }), ); await expect(ensure(contract())).rejects.toThrow(/has not assigned/i); }); it('lets the declaration through once the officer is named', async () => { repo.currentCycle.mockResolvedValue( cycle({ transitAssigneeRequestedAt: new Date(), transitAssigneeName: 'Ahmed Bourhan', }), ); ( service as unknown as { workflowService: unknown } ).workflowService = { listMilestones: jest .fn() .mockResolvedValue([ { milestoneCode: 'DOCUMENTS_APPROVED', status: 'COMPLETED' }, ]), }; await expect(ensure(contract())).resolves.toBeUndefined(); }); }); });