mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
191 lines
6.2 KiB
TypeScript
191 lines
6.2 KiB
TypeScript
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> = {}): 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<string, unknown> = {}) => ({
|
|
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<void>;
|
|
}
|
|
).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();
|
|
});
|
|
});
|
|
});
|