mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
feat: implement document clearance workflow for bookings
- Added ClearanceCard component to display and manage clearance documents in ReadonlyBookingView. - Introduced new API endpoints for clearance operations: getClearance, submitClearanceDocuments, and proceedToOperation. - Created BookingDocumentReview entity and migration for document review status tracking. - Developed GlClearancePage for Global Logistics to review and manage document submissions. - Implemented utility functions for determining clearance setting codes based on trade direction and freight type. - Added tests for booking transition clearance logic and clearance utility functions.
This commit is contained in:
@@ -192,6 +192,169 @@ const COMPANY_ONBOARDING_DOCUMENTS: OnboardingDocumentSetting[] = [
|
||||
const COMPANY_ONBOARDING_DESCRIPTION =
|
||||
"Required documents for external company onboarding, by company nationality.";
|
||||
|
||||
// ── Clearance document settings ────────────────────────────────────────────
|
||||
// Operation/clearance documents collected after contract counter-sign, resolved
|
||||
// at runtime from (operationType, freightType, includesCustoms). The `entity`
|
||||
// is "booking_clearance" so the backoffice file-settings editor can filter them.
|
||||
// Two kinds of set per customs category: a CUSTOMER-INPUT set (the customer
|
||||
// uploads) and a GL-OUTPUT set (Global Logistics uploads the customs outputs).
|
||||
|
||||
const JPG_EXTENSIONS = ["jpg", "jpeg", "png", "pdf"];
|
||||
const CLEARANCE_ENTITY = "booking_clearance";
|
||||
|
||||
/** Build a clearance field with sensible defaults; `critical` marks isRequired. */
|
||||
function clearanceField(
|
||||
fileKey: string,
|
||||
fileLabel: string,
|
||||
displayOrder: number,
|
||||
opts?: { required?: boolean; help?: string; extensions?: string[] },
|
||||
): OnboardingField {
|
||||
return {
|
||||
fileKey,
|
||||
fileLabel,
|
||||
helpText: opts?.help ?? "",
|
||||
isRequired: opts?.required ?? true,
|
||||
isMultiple: false,
|
||||
maxFiles: 1,
|
||||
allowedExtensions: opts?.extensions ?? DOC_EXTENSIONS,
|
||||
maxSizeMb: 10,
|
||||
displayOrder,
|
||||
};
|
||||
}
|
||||
|
||||
/** Documents shared by every container import category (with/without customs). */
|
||||
const IMPORT_CONTAINER_FIELDS: OnboardingField[] = [
|
||||
clearanceField("commercial_invoice", "Commercial Invoice", 1),
|
||||
clearanceField("packing_list", "Packing List", 2),
|
||||
clearanceField("import_license", "Import License", 3),
|
||||
clearanceField("certificate_of_origin", "Certificate of Origin", 4),
|
||||
clearanceField(
|
||||
"external_freight_cost",
|
||||
"External Freight Cost / Checkup Documentation",
|
||||
5,
|
||||
),
|
||||
clearanceField("bill_of_lading", "Bill of Lading / Railway Bill", 6),
|
||||
clearanceField("vgm", "Verified Gross Mass (VGM)", 7, { required: true }),
|
||||
clearanceField("release_order", "Release Order", 8, { required: true }),
|
||||
];
|
||||
|
||||
/** Documents shared by every container export category (with/without customs). */
|
||||
const EXPORT_CONTAINER_FIELDS: OnboardingField[] = [
|
||||
clearanceField("booking_confirmation", "Booking Confirmation", 1),
|
||||
clearanceField("commercial_invoice", "Commercial Invoice", 2),
|
||||
clearanceField("packing_list", "Packing List", 3),
|
||||
clearanceField("shipping_instruction", "Shipping Instruction", 4),
|
||||
clearanceField("bank_permit", "Bank Permit", 5),
|
||||
clearanceField("export_license", "Export License", 6),
|
||||
clearanceField("vgm_letter", "VGM Letter", 7, { required: true }),
|
||||
clearanceField("railway_bill", "Railway Bill", 8),
|
||||
clearanceField("delegation_letter", "Delegation Letter / POA", 9, {
|
||||
required: false,
|
||||
help: "Required only if EDR manages all transit activity.",
|
||||
}),
|
||||
];
|
||||
|
||||
/** Bulk import documents (shorter, transit-focused set). */
|
||||
const IMPORT_BULK_FIELDS: OnboardingField[] = [
|
||||
clearanceField("packing_list", "Packing List", 1, { required: true }),
|
||||
clearanceField("bill_of_loading", "Bill of Loading", 2, { required: true }),
|
||||
clearanceField("port_invoice", "Port Invoice", 3),
|
||||
];
|
||||
|
||||
/** Bulk export documents (transit/customs corridor docs). */
|
||||
const EXPORT_BULK_FIELDS: OnboardingField[] = [
|
||||
clearanceField("release_order_djibouti", "Release Order (Djibouti)", 1),
|
||||
clearanceField("port_gate_pass", "Port Gate Pass", 2),
|
||||
clearanceField("port_invoice", "Port Invoice", 3),
|
||||
];
|
||||
|
||||
/** GL-uploaded customs output documents (import container). */
|
||||
const IMPORT_CONTAINER_OUTPUT_FIELDS: OnboardingField[] = [
|
||||
clearanceField("im4", "IM4 — Permanent Import Document", 1),
|
||||
clearanceField("im5", "IM5 — Temporary Import Document", 2, {
|
||||
required: false,
|
||||
}),
|
||||
clearanceField("transit_permitted", "Transit Permitted Screenshot", 3, {
|
||||
extensions: JPG_EXTENSIONS,
|
||||
}),
|
||||
];
|
||||
|
||||
/** GL-uploaded customs output documents (export container). */
|
||||
const EXPORT_CONTAINER_OUTPUT_FIELDS: OnboardingField[] = [
|
||||
clearanceField("ex3", "EX3 — Permanent Export Document", 1),
|
||||
clearanceField("ex8", "EX8 — Export Transit Document", 2),
|
||||
clearanceField("export_release", "Export Release", 3),
|
||||
clearanceField("t1", "T1 — Transport Document", 4),
|
||||
];
|
||||
|
||||
const CLEARANCE_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [
|
||||
// ── Customer-input sets ──
|
||||
{
|
||||
code: "clearance_import_container_with_customs",
|
||||
label: "Import container clearance documents (with customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: IMPORT_CONTAINER_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_import_container_without_customs",
|
||||
label: "Import container documents (without customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: IMPORT_CONTAINER_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_export_container_with_customs",
|
||||
label: "Export container clearance documents (with customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: EXPORT_CONTAINER_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_export_container_without_customs",
|
||||
label: "Export container documents (without customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: EXPORT_CONTAINER_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_import_bulk_with_customs",
|
||||
label: "Import bulk clearance documents (with customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: IMPORT_BULK_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_import_bulk_without_customs",
|
||||
label: "Import bulk documents (without customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: IMPORT_BULK_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_export_bulk_with_customs",
|
||||
label: "Export bulk clearance documents (with customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: EXPORT_BULK_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_export_bulk_without_customs",
|
||||
label: "Export bulk documents (without customs)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: EXPORT_BULK_FIELDS,
|
||||
},
|
||||
// ── GL-output sets (customs only) ──
|
||||
{
|
||||
code: "clearance_output_import_container",
|
||||
label: "Customs output documents (import container)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: IMPORT_CONTAINER_OUTPUT_FIELDS,
|
||||
},
|
||||
{
|
||||
code: "clearance_output_export_container",
|
||||
label: "Customs output documents (export container)",
|
||||
entity: CLEARANCE_ENTITY,
|
||||
fields: EXPORT_CONTAINER_OUTPUT_FIELDS,
|
||||
},
|
||||
];
|
||||
|
||||
const CLEARANCE_DESCRIPTION =
|
||||
"Operation/clearance documents collected after contract execution, by operation, freight type and customs.";
|
||||
|
||||
@Injectable()
|
||||
export class FileUploadSettingsSeeder {
|
||||
private readonly logger = new Logger(FileUploadSettingsSeeder.name);
|
||||
@@ -203,12 +366,25 @@ export class FileUploadSettingsSeeder {
|
||||
const settingRepository = manager.getRepository(FileUploadSetting);
|
||||
const fieldRepository = manager.getRepository(FileUploadField);
|
||||
|
||||
for (const documentSetting of COMPANY_ONBOARDING_DOCUMENTS) {
|
||||
const allSettings: Array<
|
||||
OnboardingDocumentSetting & { description: string }
|
||||
> = [
|
||||
...COMPANY_ONBOARDING_DOCUMENTS.map((s) => ({
|
||||
...s,
|
||||
description: COMPANY_ONBOARDING_DESCRIPTION,
|
||||
})),
|
||||
...CLEARANCE_DOCUMENT_SETTINGS.map((s) => ({
|
||||
...s,
|
||||
description: CLEARANCE_DESCRIPTION,
|
||||
})),
|
||||
];
|
||||
|
||||
for (const documentSetting of allSettings) {
|
||||
await settingRepository.upsert(
|
||||
{
|
||||
code: documentSetting.code,
|
||||
label: documentSetting.label,
|
||||
description: COMPANY_ONBOARDING_DESCRIPTION,
|
||||
description: documentSetting.description,
|
||||
entity: documentSetting.entity,
|
||||
},
|
||||
{
|
||||
@@ -245,7 +421,7 @@ export class FileUploadSettingsSeeder {
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
"Ensured company onboarding file upload settings for external companies",
|
||||
"Ensured company onboarding + booking clearance file upload settings",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [
|
||||
perm('a1000001-0001-4000-8000-00000000000c', 'edr_freight_app:bookings:payment_verify', 'Verify payment'),
|
||||
perm('a1000001-0001-4000-8000-00000000000d', 'edr_freight_app:bookings:operations', 'Booking operations'),
|
||||
perm('a1000001-0001-4000-8000-00000000000e', 'edr_freight_app:bookings:cancel', 'Cancel booking'),
|
||||
perm('a1000001-0001-4000-8000-000000000020', 'edr_freight_app:bookings:review_documents', 'Review clearance documents'),
|
||||
perm('a1000001-0001-4000-8000-000000000021', 'edr_freight_app:bookings:upload_clearance_output', 'Upload customs output documents'),
|
||||
perm('a1000001-0001-4000-8000-000000000022', 'edr_freight_app:bookings:finalize_clearance', 'Finalize document clearance'),
|
||||
perm('a1000001-0001-4000-8000-00000000000f', 'edr_freight_app:train_scheduling:view', 'View train scheduling'),
|
||||
perm('a1000001-0001-4000-8000-000000000010', 'edr_freight_app:train_scheduling:manage', 'Manage train scheduling'),
|
||||
perm('a1000001-0001-4000-8000-000000000011', 'edr_freight_app:fleet:view', 'View fleet'),
|
||||
@@ -107,6 +110,9 @@ export const FREIGHT_PERMS = {
|
||||
signStaff: 'edr_freight_app:bookings:sign_staff',
|
||||
operations: 'edr_freight_app:bookings:operations',
|
||||
cancel: 'edr_freight_app:bookings:cancel',
|
||||
reviewDocuments: 'edr_freight_app:bookings:review_documents',
|
||||
uploadClearanceOutput: 'edr_freight_app:bookings:upload_clearance_output',
|
||||
finalizeClearance: 'edr_freight_app:bookings:finalize_clearance',
|
||||
},
|
||||
trainScheduling: {
|
||||
view: 'edr_freight_app:train_scheduling:view',
|
||||
@@ -167,6 +173,14 @@ export const ROLE_PERMISSION_PRESETS = {
|
||||
...allRuleEngineViewKeys(),
|
||||
],
|
||||
finance: [FREIGHT_PERMS.bookings.view],
|
||||
// Global Logistics: reviews post-counter-sign clearance documents, uploads
|
||||
// customs output documents, and finalizes the clearance gate.
|
||||
globalLogistics: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.reviewDocuments,
|
||||
FREIGHT_PERMS.bookings.uploadClearanceOutput,
|
||||
FREIGHT_PERMS.bookings.finalizeClearance,
|
||||
],
|
||||
// Marketing handles intake through contract (same as line staff here).
|
||||
marketing: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
|
||||
Reference in New Issue
Block a user