mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
- Introduced HazardDeclarationPanel component to display dangerous goods declaration details. - Updated URL constants to include CLEARANCE_PROCEED endpoint for re-requesting operations. - Enhanced permissions to include hazardous approval roles for contract approvals. - Integrated HazardDeclarationPanel into ContractRequestDetailPage and ContractClearanceDetailPage. - Added proceedToOperation method in bookings service for handling operation re-requests. - Updated contract forms and schemas to include hazard class and UN number fields. - Implemented validation for hazardous contracts in the contract creation flow. - Added expiry notice functionality for contracts nearing validity end. - Created tests for expiry notice calculations and labels. - Updated UI components to reflect hazardous cargo information and validation errors.
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
import { BadRequestException, ConflictException } from '@nestjs/common';
|
|
|
|
import { ContractTransitionService } from './contract-transition.service';
|
|
|
|
/**
|
|
* Signing is one-shot. The single exception: a contract signed before company
|
|
* stamps were required must be re-signable so the customer can attach one —
|
|
* otherwise counterSign's both-stamps gate strands it forever. These specs pin
|
|
* that exception open and pin everything else shut.
|
|
*/
|
|
describe('customer re-sign to attach a missing stamp', () => {
|
|
const contractReady = { id: 'c-1', reference: 'CTR-1', status: 'CONTRACT_READY' };
|
|
const signedNoStamp = { id: 'c-1', reference: 'CTR-1', status: 'SIGNED_CUSTOMER' };
|
|
|
|
const build = (contract: unknown, existingSignature: unknown) => {
|
|
const applied: unknown[] = [];
|
|
const service = Object.create(
|
|
ContractTransitionService.prototype,
|
|
) as ContractTransitionService;
|
|
Object.assign(service, {
|
|
contractsService: {
|
|
findById: jest.fn().mockResolvedValue(contract),
|
|
assertCustomerCanAccessContract: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
contractsRepository: {
|
|
findSignature: jest.fn().mockResolvedValue(existingSignature),
|
|
update: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
otpService: {
|
|
verifyOtpForAction: jest.fn().mockResolvedValue(undefined),
|
|
sendOtp: jest.fn().mockResolvedValue(undefined),
|
|
},
|
|
notifier: { customerSignedToStaff: jest.fn() },
|
|
resolveSignerContacts: jest.fn().mockResolvedValue({ phone: '+251900000000' }),
|
|
applySignature: jest.fn((...args: unknown[]) => {
|
|
applied.push(args);
|
|
return Promise.resolve();
|
|
}),
|
|
regenerateContractPdf: jest.fn().mockResolvedValue(undefined),
|
|
});
|
|
return { service, applied };
|
|
};
|
|
|
|
const dto = {
|
|
role: 'CUSTOMER' as const,
|
|
signerDisplayName: 'C. Customer',
|
|
signatureImageBase64: 'data:image/png;base64,AAAA',
|
|
stampImageBase64: 'data:image/png;base64,BBBB',
|
|
otp: '123456',
|
|
};
|
|
|
|
it('lets a customer sign again when their signature has no stamp', async () => {
|
|
const { service, applied } = build(signedNoStamp, {
|
|
id: 's-1',
|
|
role: 'CUSTOMER',
|
|
stampFileId: null,
|
|
});
|
|
|
|
await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).resolves.toBeDefined();
|
|
expect(applied).toHaveLength(1);
|
|
});
|
|
|
|
it('still refuses a second signature once a stamp is on file', async () => {
|
|
const { service } = build(signedNoStamp, {
|
|
id: 's-1',
|
|
role: 'CUSTOMER',
|
|
stampFileId: 'file-1',
|
|
});
|
|
|
|
// Stamped already → not the re-sign case, so the status guard rejects
|
|
// SIGNED_CUSTOMER before the already-signed check is reached.
|
|
await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).rejects.toBeInstanceOf(
|
|
ConflictException,
|
|
);
|
|
});
|
|
|
|
it('refuses a second signature on a still-ready contract', async () => {
|
|
const { service } = build(contractReady, {
|
|
id: 's-1',
|
|
role: 'CUSTOMER',
|
|
stampFileId: 'file-1',
|
|
});
|
|
|
|
await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).rejects.toThrow(
|
|
/already signed/i,
|
|
);
|
|
});
|
|
|
|
it('signs normally when nothing is on file yet', async () => {
|
|
const { service, applied } = build(contractReady, null);
|
|
|
|
await expect(service.sign('c-1', dto, { signerUserId: 'u-1' })).resolves.toBeDefined();
|
|
expect(applied).toHaveLength(1);
|
|
});
|
|
|
|
it('sends a signing OTP for the stamp re-sign', async () => {
|
|
const { service } = build(signedNoStamp, {
|
|
id: 's-1',
|
|
role: 'CUSTOMER',
|
|
stampFileId: null,
|
|
});
|
|
|
|
await expect(
|
|
service.sendSigningOtp('c-1', { signerUserId: 'u-1' }),
|
|
).resolves.toEqual(expect.objectContaining({ sentTo: expect.any(String) }));
|
|
});
|
|
|
|
it('refuses a signing OTP once the contract is signed and stamped', async () => {
|
|
const { service } = build(signedNoStamp, {
|
|
id: 's-1',
|
|
role: 'CUSTOMER',
|
|
stampFileId: 'file-1',
|
|
});
|
|
|
|
await expect(
|
|
service.sendSigningOtp('c-1', { signerUserId: 'u-1' }),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
});
|
|
|
|
it('requires the OTP on the re-sign path too', async () => {
|
|
const { service } = build(signedNoStamp, {
|
|
id: 's-1',
|
|
role: 'CUSTOMER',
|
|
stampFileId: null,
|
|
});
|
|
|
|
await expect(
|
|
service.sign('c-1', { ...dto, otp: undefined }, { signerUserId: 'u-1' }),
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
});
|
|
});
|