Files
edr-platform/apps/edr-freight-api/src/modules/contracts/contract-expiry.service.spec.ts
2026-08-07 12:42:33 +00:00

79 lines
3.1 KiB
TypeScript

import { ContractExpiryService } from './contract-expiry.service';
import type { Contract } from './entities/contract.entity';
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
/**
* The reminder must warn each customer once, ten days out, and must never let a
* notification failure escape into the scheduler (that would also take out the
* expiry sweep sharing this service).
*/
describe('ContractExpiryService — expiry reminder', () => {
const contract = (over: Partial<Contract> = {}): Contract =>
({
id: 'c-1',
reference: 'CTR-2026-00042',
companyId: 'co-1',
contractValidUntil: new Date('2026-08-10T00:00:00.000Z'),
status: 'CONTRACT_ACTIVE',
...over,
}) as Contract;
let repo: { expireLapsedContracts: jest.Mock; findExpiringInDays: jest.Mock };
let inbox: { notify: jest.Mock };
let service: ContractExpiryService;
beforeEach(() => {
repo = {
expireLapsedContracts: jest.fn().mockResolvedValue(0),
findExpiringInDays: jest.fn().mockResolvedValue([]),
};
inbox = { notify: jest.fn().mockResolvedValue(undefined) };
service = new ContractExpiryService(repo as never, inbox as never);
});
it('asks for the contracts lapsing ten days out', async () => {
await service.remindExpiringContracts();
expect(repo.findExpiringInDays).toHaveBeenCalledWith(10);
});
it('notifies the owning company once, deep-linking the contract list', async () => {
repo.findExpiringInDays.mockResolvedValue([contract()]);
await service.remindExpiringContracts();
expect(inbox.notify).toHaveBeenCalledTimes(1);
const sent = inbox.notify.mock.calls[0][0];
expect(sent.recipients).toEqual({ companyId: 'co-1' });
expect(sent.title).toContain('CTR-2026-00042');
expect(sent.title).toContain('10 days');
expect(sent.link).toBe('/contracts');
expect(sent.data).toMatchObject({ contractId: 'c-1', action: 'CONTRACT_EXPIRING' });
});
it('skips a contract with no owning company (nobody to notify)', async () => {
repo.findExpiringInDays.mockResolvedValue([contract({ companyId: null })]);
await service.remindExpiringContracts();
expect(inbox.notify).not.toHaveBeenCalled();
});
it('swallows a notification failure instead of throwing into the scheduler', async () => {
repo.findExpiringInDays.mockResolvedValue([contract()]);
inbox.notify.mockRejectedValue(new Error('inbox down'));
await expect(service.remindExpiringContracts()).resolves.toBeUndefined();
});
// The sweep-failure alert is staff-facing. It used to go to every employee;
// it belongs to the people who would notice expired contracts still listed
// as active, i.e. the contract desk.
it('alerts the contract desk when the sweep itself fails', async () => {
repo.expireLapsedContracts.mockRejectedValue(new Error('deadlock'));
await service.expireLapsedContracts();
expect(inbox.notify).toHaveBeenCalledTimes(1);
expect(inbox.notify.mock.calls[0][0].recipients).toEqual({
permissionKeys: [FREIGHT_PERMS.contracts.getNotification],
});
});
});