mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 20:53:39 +00:00
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
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> = {}): 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<Contract>);
|
|
|
|
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();
|
|
});
|
|
});
|