import { ContractExpiryService } from './contract-expiry.service'; import type { Contract } from './entities/contract.entity'; /** * 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 => ({ 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(); }); });