mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
79 lines
3.1 KiB
TypeScript
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],
|
|
});
|
|
});
|
|
});
|