feat: add hazardous goods declaration feature

- 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.
This commit is contained in:
Marshal
2026-07-25 21:10:09 +00:00
parent fde5e6de4b
commit 9a1c8e5603
41 changed files with 1663 additions and 99 deletions

View File

@@ -0,0 +1,47 @@
import { ForbiddenException } from '@nestjs/common';
import {
assertCanApproveContractStep,
canEditContractStep,
} from './freight-permission.util';
import { FREIGHT_PERMS } from '../seed/freight-permissions.registry';
const userWith = (...keys: string[]) => ({
permissions: keys.map((key) => ({ key })),
});
describe('hazardous contract approval steps', () => {
it('rejects an approver who only holds ordinary contract-approve permissions', () => {
// The blanket "any contract approve permission" fallback must NOT reach
// dangerous goods — that is the whole point of the dedicated desks.
const lineStaff = userWith(FREIGHT_PERMS.contracts.approveLineStaff);
expect(() =>
assertCanApproveContractStep(lineStaff, 'HAZARDOUS_APPROVAL_ONE'),
).toThrow(ForbiddenException);
expect(canEditContractStep(lineStaff, 'HAZARDOUS_APPROVAL_ONE')).toBe(false);
});
it('accepts only the matching hazardous permission', () => {
const first = userWith(FREIGHT_PERMS.contracts.hazardousApprovalOne);
expect(() =>
assertCanApproveContractStep(first, 'HAZARDOUS_APPROVAL_ONE'),
).not.toThrow();
// Holding step one does not confer step two.
expect(() =>
assertCanApproveContractStep(first, 'HAZARDOUS_APPROVAL_TWO'),
).toThrow(ForbiddenException);
});
it('does not let a hazardous approver stand in for the commercial chain', () => {
const hazardOnly = userWith(
FREIGHT_PERMS.contracts.hazardousApprovalOne,
FREIGHT_PERMS.contracts.hazardousApprovalTwo,
);
expect(() => assertCanApproveContractStep(hazardOnly, 'CEO')).toThrow(
ForbiddenException,
);
});
});

View File

@@ -151,6 +151,24 @@ const APPROVE_ROLE_PERMISSION: Record<string, string> = {
CEO: FREIGHT_PERMS.bookings.approveCeo,
};
/**
* Approval-chain roles synthesized for hazardous contracts (see
* `instantiateApprovalSteps`). Unlike the legacy roles below they are NOT
* position types — they authorize purely on their own dedicated permission, and
* they deliberately opt out of the blanket "holds any contract-approve
* permission" fallback so a normal approver cannot sign off dangerous goods.
*/
export const HAZARDOUS_APPROVAL_ROLE_PERMISSION: Record<string, string> = {
HAZARDOUS_APPROVAL_ONE: FREIGHT_PERMS.contracts.hazardousApprovalOne,
HAZARDOUS_APPROVAL_TWO: FREIGHT_PERMS.contracts.hazardousApprovalTwo,
};
/** The two hazardous steps, in the order they are prepended to the chain. */
export const HAZARDOUS_APPROVAL_ROLES = [
'HAZARDOUS_APPROVAL_ONE',
'HAZARDOUS_APPROVAL_TWO',
] as const;
const CONTRACT_APPROVE_ROLE_PERMISSION: Record<string, string> = {
LINE_STAFF: FREIGHT_PERMS.contracts.approveLineStaff,
DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
@@ -183,6 +201,16 @@ export function assertCanApproveContractStep(
): void {
if (isFreightApprovalAdmin(user)) return;
// Hazardous steps are permission-only and strict — no legacy alias, no
// blanket approve fallback.
const hazardousPermission = HAZARDOUS_APPROVAL_ROLE_PERMISSION[requiredRole];
if (hazardousPermission) {
if (hasFreightPermission(user, hazardousPermission)) return;
throw new ForbiddenException(
`Missing permission: ${hazardousPermission}`,
);
}
const positionTypes = collectPositionTypeKeys(user);
if (positionTypes.includes(requiredRole)) return;
@@ -219,6 +247,11 @@ export function canEditContractStep(
): boolean {
if (isFreightApprovalAdmin(user)) return true;
const hazardousPermission = HAZARDOUS_APPROVAL_ROLE_PERMISSION[requiredRole];
if (hazardousPermission) {
return hasFreightPermission(user, hazardousPermission);
}
const positionTypes = collectPositionTypeKeys(user);
if (positionTypes.includes(requiredRole)) return true;