import { ContractTransitionService } from './contract-transition.service'; import type { Contract } from './entities/contract.entity'; /** * Staff cancel is terminal, so the rules that matter are: it needs its own * permission (suspend must NOT imply it), it refuses to strand live shipments, * it works on a suspended contract, and it cannot be applied twice. */ describe('ContractTransitionService — staff cancel', () => { const contract = (over: Partial = {}): Contract => ({ id: 'c-1', reference: 'CTR-2026-00042', companyId: 'co-1', status: 'CONTRACT_ACTIVE', freightType: 'CONTAINER', ...over, }) as Contract; let current: Contract; let repo: { update: jest.Mock; createReviewNote: jest.Mock; countActiveBookings: jest.Mock; }; let notifier: { cancelledByStaff: jest.Mock }; let service: ContractTransitionService; const staff = { permissions: [{ key: 'edr_freight_app:contracts:cancel' }], }; beforeEach(() => { current = contract(); repo = { update: jest.fn().mockImplementation((_id: string, patch: object) => { current = { ...current, ...patch } as Contract; return Promise.resolve(current); }), createReviewNote: jest.fn().mockResolvedValue(undefined), countActiveBookings: jest.fn().mockResolvedValue(0), }; notifier = { cancelledByStaff: jest.fn() }; service = Object.create( ContractTransitionService.prototype, ) as ContractTransitionService; Object.assign(service, { contractsRepository: repo, contractsService: { findById: () => Promise.resolve(current) }, notifier, }); }); it('cancels, records the reason as a staff note, and notifies the customer', async () => { await service.cancelByStaff('c-1', 'Duplicate request', 'staff-1', staff as never); expect(repo.update).toHaveBeenCalledWith('c-1', { status: 'CANCELLED', statusBeforeSuspension: null, }); expect(repo.createReviewNote).toHaveBeenCalledWith( 'c-1', 'Duplicate request', 'CANCELLATION', 'staff-1', 'STAFF', ); expect(notifier.cancelledByStaff).toHaveBeenCalled(); }); it('cancels a suspended contract — freezing it is exactly when staff kill it', async () => { current = contract({ status: 'SUSPENDED', statusBeforeSuspension: 'CONTRACT_ACTIVE', } as Partial); await service.cancelByStaff('c-1', 'Customer withdrew', 'staff-1', staff as never); expect(repo.update).toHaveBeenCalledWith('c-1', { status: 'CANCELLED', statusBeforeSuspension: null, }); }); it('refuses while a shipment is still running', async () => { repo.countActiveBookings.mockResolvedValue(2); await expect( service.cancelByStaff('c-1', 'Change of plan', 'staff-1', staff as never), ).rejects.toThrow('2 active shipments'); expect(repo.update).not.toHaveBeenCalled(); }); it('refuses to cancel an already-terminal contract', async () => { current = contract({ status: 'CANCELLED' }); await expect( service.cancelByStaff('c-1', 'Again', 'staff-1', staff as never), ).rejects.toThrow(/already cancelled/i); expect(repo.update).not.toHaveBeenCalled(); }); it('rejects a user holding only the suspend key — cancel is a separate permission', async () => { const suspender = { permissions: [{ key: 'edr_freight_app:contracts:suspend' }], }; await expect( service.cancelByStaff('c-1', 'Not allowed', 'staff-1', suspender as never), ).rejects.toThrow(); expect(repo.update).not.toHaveBeenCalled(); }); });