Files
edr-platform/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts
Marshal 6485cbdd71 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.
2026-06-23 21:33:34 +00:00

428 lines
14 KiB
TypeScript

import { Injectable, Logger } from "@nestjs/common";
import { DataSource } from "typeorm";
import { FileUploadField } from "../modules/file-upload-settings/entities/file-upload-field.entity";
import { FileUploadSetting } from "../modules/file-upload-settings/entities/file-upload-setting.entity";
interface OnboardingField {
fileKey: string;
fileLabel: string;
helpText: string;
isRequired: boolean;
isMultiple: boolean;
maxFiles: number;
allowedExtensions: string[];
maxSizeMb: number;
displayOrder: number;
}
const DOC_EXTENSIONS = ["pdf", "jpg", "jpeg", "png"];
/** Documents required from an Ethiopian company at onboarding. */
const ETHIOPIAN_ONBOARDING_FIELDS: OnboardingField[] = [
{
fileKey: "tin_certificate",
fileLabel: "TIN Certificate",
helpText: "Verified against the TIN registry during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 1,
},
{
fileKey: "commercial_license",
fileLabel: "Commercial License",
helpText: "Verified against the government trade system during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 2,
},
{
fileKey: "national_id",
fileLabel: "National ID",
helpText: "Verified against the National ID API during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 3,
},
];
/** Documents required from a Foreign company at onboarding. */
const FOREIGN_ONBOARDING_FIELDS: OnboardingField[] = [
{
fileKey: "tin_certificate",
fileLabel: "TIN Certificate",
helpText: "Verified against the TIN registry during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 1,
},
{
fileKey: "investment_license",
fileLabel: "Investment License",
helpText: "Investment license issued for operating in Ethiopia.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 2,
},
{
fileKey: "national_id",
fileLabel: "National ID",
helpText: "National ID of the company's authorized representative.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 3,
},
{
fileKey: "passport",
fileLabel: "Passport",
helpText: "Passport of the company's authorized representative.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 4,
},
];
/** Legacy combined set, kept for the older per-company-type codes. */
const LEGACY_ONBOARDING_FIELDS: OnboardingField[] = [
{
fileKey: "business_license",
fileLabel: "Business License / Trade License",
helpText: "Verified against the government trade system during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 1,
},
{
fileKey: "tin_certificate",
fileLabel: "TIN Certificate",
helpText: "Verified against the TIN registry during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 2,
},
{
fileKey: "national_id_passport",
fileLabel: "National ID / Passport",
helpText: "Verified against the National ID API during registration.",
isRequired: true,
isMultiple: false,
maxFiles: 1,
allowedExtensions: DOC_EXTENSIONS,
maxSizeMb: 10,
displayOrder: 3,
},
];
interface OnboardingDocumentSetting {
code: string;
label: string;
entity: string;
fields: OnboardingField[];
}
const COMPANY_ONBOARDING_DOCUMENTS: OnboardingDocumentSetting[] = [
// Nationality-based sets — the document requirements depend only on whether
// the company is Ethiopian or Foreign (same for importer/exporter/forwarder).
{
code: "company_onboarding_documents_ethiopian",
label: "Ethiopian company onboarding documents",
entity: "customer",
fields: ETHIOPIAN_ONBOARDING_FIELDS,
},
{
code: "company_onboarding_documents_foreign",
label: "Foreign company onboarding documents",
entity: "customer",
fields: FOREIGN_ONBOARDING_FIELDS,
},
// Legacy per-company-type codes (kept for back-compat; no longer used by the portal).
{
code: "company_onboarding_documents_customer",
label: "Customer onboarding documents",
entity: "customer",
fields: LEGACY_ONBOARDING_FIELDS,
},
{
code: "company_onboarding_documents_forwarder",
label: "Forwarder onboarding documents",
entity: "other",
fields: LEGACY_ONBOARDING_FIELDS,
},
{
code: "company_onboarding_documents_transporter",
label: "Transporter onboarding documents",
entity: "other",
fields: LEGACY_ONBOARDING_FIELDS,
},
{
code: "company_onboarding_documents_forwarder_dj",
label: "Djibouti forwarder onboarding documents",
entity: "other",
fields: LEGACY_ONBOARDING_FIELDS,
},
];
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);
constructor(private readonly dataSource: DataSource) {}
async run() {
await this.dataSource.transaction(async (manager) => {
const settingRepository = manager.getRepository(FileUploadSetting);
const fieldRepository = manager.getRepository(FileUploadField);
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: documentSetting.description,
entity: documentSetting.entity,
},
{
conflictPaths: { code: true },
},
);
const setting = await settingRepository.findOne({
where: { code: documentSetting.code },
select: { id: true, code: true },
});
if (!setting) {
throw new Error(`file_upload_setting_seed_failed:${documentSetting.code}`);
}
await fieldRepository.delete({ settingId: setting.id });
await fieldRepository.insert(
documentSetting.fields.map((field, index) => ({
settingId: setting.id,
fileKey: field.fileKey,
fileLabel: field.fileLabel,
helpText: field.helpText,
isRequired: field.isRequired,
isMultiple: field.isMultiple,
maxFiles: field.maxFiles,
allowedExtensions: [...field.allowedExtensions],
maxSizeMb: field.maxSizeMb,
displayOrder: field.displayOrder ?? index + 1,
})),
);
}
});
this.logger.log(
"Ensured company onboarding + booking clearance file upload settings",
);
}
}