import { Injectable, Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; 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"]; /** fileKey of the delegation letter attached to the Power of Attorney step. */ export const POA_DELEGATION_FILE_KEY = "poa_delegation_letter"; /** * Seeded as optional: the delegation letter is only mandatory once a PoA has * been entered, or when the company operates as a freight forwarder. That rule * spans form fields as well as files, so it lives in the onboarding gate * (companies.service.getOnboardingRequirements) rather than in `isRequired`. */ const poaDelegationField = (displayOrder: number): OnboardingField => ({ fileKey: POA_DELEGATION_FILE_KEY, fileLabel: "PoA Delegation Letter", helpText: "Signed letter in which the General Manager delegates the representative named above.", isRequired: false, isMultiple: false, maxFiles: 1, allowedExtensions: DOC_EXTENSIONS, maxSizeMb: 10, displayOrder, }); /** 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 Registration", 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, }, poaDelegationField(4), ]; /** 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, }, poaDelegationField(5), ]; /** 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, }, // Bulk output sets mirror the container output docs so customs+bulk bookings // can finalize (previously bulk had no output set and got stuck at finalize). { code: "clearance_output_import_bulk", label: "Customs output documents (import bulk)", entity: CLEARANCE_ENTITY, fields: IMPORT_CONTAINER_OUTPUT_FIELDS, }, { code: "clearance_output_export_bulk", label: "Customs output documents (export bulk)", entity: CLEARANCE_ENTITY, fields: EXPORT_CONTAINER_OUTPUT_FIELDS, }, ]; // ── Contract pre-booking clearance settings (Path B) ──────────────────────── // For customs-clearance contracts the customer uploads clearance documents on // the CONTRACT (before any booking exists). Same customer doc sets as the legacy // booking clearance, plus the GL output sets, keyed on the contract. Resolved by // contract-clearance.util.ts (codes: contract_clearance_{op}_{freight} and // contract_clearance_output_{op}_container). const CONTRACT_CLEARANCE_ENTITY = "contract_clearance"; const CONTRACT_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ { code: "contract_clearance_import_container", label: "Contract clearance documents (import container)", entity: CONTRACT_CLEARANCE_ENTITY, fields: IMPORT_CONTAINER_FIELDS, }, { code: "contract_clearance_export_container", label: "Contract clearance documents (export container)", entity: CONTRACT_CLEARANCE_ENTITY, fields: EXPORT_CONTAINER_FIELDS, }, { code: "contract_clearance_import_bulk", label: "Contract clearance documents (import bulk)", entity: CONTRACT_CLEARANCE_ENTITY, fields: IMPORT_BULK_FIELDS, }, { code: "contract_clearance_export_bulk", label: "Contract clearance documents (export bulk)", entity: CONTRACT_CLEARANCE_ENTITY, fields: EXPORT_BULK_FIELDS, }, // GL ET output sets uploaded during pre-booking clearance. { code: "contract_clearance_output_import_container", label: "Contract customs output documents (import container)", entity: CONTRACT_CLEARANCE_ENTITY, fields: IMPORT_CONTAINER_OUTPUT_FIELDS, }, { code: "contract_clearance_output_export_container", label: "Contract customs output documents (export container)", entity: CONTRACT_CLEARANCE_ENTITY, fields: EXPORT_CONTAINER_OUTPUT_FIELDS, }, // Bulk output sets mirror the container output docs so customs+bulk contracts // can finalize (previously bulk had no output set and got stuck at finalize). { code: "contract_clearance_output_import_bulk", label: "Contract customs output documents (import bulk)", entity: CONTRACT_CLEARANCE_ENTITY, fields: IMPORT_CONTAINER_OUTPUT_FIELDS, }, { code: "contract_clearance_output_export_bulk", label: "Contract customs output documents (export bulk)", entity: CONTRACT_CLEARANCE_ENTITY, fields: EXPORT_CONTAINER_OUTPUT_FIELDS, }, ]; // ── Path A self-clearance settings (no EDR customs service) ────────────────── // When the contract does NOT bundle customs clearance, the customer clears the // cargo himself and uploads his OWN clearance proof on the contract. Operations // (not GL) reviews this smaller set before the customer may create the booking. // Resolved by contract-clearance.util.ts as contract_clearance_selfclear_{op}_{freight}. const SELF_CLEARANCE_IMPORT_FIELDS: OnboardingField[] = [ clearanceField("customs_declaration", "Customs Declaration (IM4/IM5)", 1), clearanceField("import_release", "Import Release Permit", 2), clearanceField("duty_tax_receipt", "Duty & Tax Payment Receipt", 3, { required: false, }), clearanceField("delivery_order", "Delivery Order", 4, { required: false }), clearanceField("supporting_document", "Other Clearance Document", 5, { required: false, }), ]; const SELF_CLEARANCE_EXPORT_FIELDS: OnboardingField[] = [ clearanceField("customs_declaration", "Customs Declaration (EX3/EX8)", 1), clearanceField("export_release", "Export Release", 2), clearanceField("transit_document", "Transit Document (T1)", 3, { required: false, }), clearanceField("supporting_document", "Other Clearance Document", 4, { required: false, }), ]; const SELF_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ { code: "contract_clearance_selfclear_import_container", label: "Self-clearance documents (import container)", entity: CONTRACT_CLEARANCE_ENTITY, fields: SELF_CLEARANCE_IMPORT_FIELDS, }, { code: "contract_clearance_selfclear_export_container", label: "Self-clearance documents (export container)", entity: CONTRACT_CLEARANCE_ENTITY, fields: SELF_CLEARANCE_EXPORT_FIELDS, }, { code: "contract_clearance_selfclear_import_bulk", label: "Self-clearance documents (import bulk)", entity: CONTRACT_CLEARANCE_ENTITY, fields: SELF_CLEARANCE_IMPORT_FIELDS, }, { code: "contract_clearance_selfclear_export_bulk", label: "Self-clearance documents (export bulk)", entity: CONTRACT_CLEARANCE_ENTITY, fields: SELF_CLEARANCE_EXPORT_FIELDS, }, ]; // ── Contract intake settings ──────────────────────────────────────────────── // Commercial/framework documents attached at contract submission (wizard step 5), // distinct from the post-sign clearance docs above. const CONTRACT_INTAKE_ENTITY = "contract_intake"; const CONTRACT_INTAKE_FIELDS: OnboardingField[] = [ clearanceField( "commercial_framework", "Commercial Framework / Agreement", 1, { required: false, }, ), clearanceField("onboarding_attachment", "Onboarding Attachment", 2, { required: false, }), clearanceField("supporting_document", "Supporting Document", 3, { required: false, }), ]; const CONTRACT_INTAKE_SETTINGS: OnboardingDocumentSetting[] = [ { code: "contract_intake_documents", label: "Contract intake documents", entity: CONTRACT_INTAKE_ENTITY, fields: CONTRACT_INTAKE_FIELDS, }, ]; const CLEARANCE_DESCRIPTION = "Operation/clearance documents collected after contract execution, by operation, freight type and customs."; // ── Driver documents ──────────────────────────────────────────────────────── // Configurable upload area (code "driver_docs") attached to a driver profile — // license, national ID, contracts, training certificates, etc. const DRIVER_DOCUMENT_FIELDS: OnboardingField[] = [ { fileKey: "driver_docs", fileLabel: "Driver documents", helpText: "License, national ID, contracts, training certificates, etc.", isRequired: false, isMultiple: true, maxFiles: 20, allowedExtensions: ["pdf", "jpg", "jpeg", "png", "doc", "docx"], maxSizeMb: 10, displayOrder: 1, }, ]; const DRIVER_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ { code: "driver_docs", label: "Driver documents", entity: "driver", fields: DRIVER_DOCUMENT_FIELDS, }, ]; @Injectable() export class FileUploadSettingsSeeder { private readonly logger = new Logger(FileUploadSettingsSeeder.name); constructor(private readonly dataSource: DataSource) { } async run() { const settingRepository = this.dataSource.getRepository(FileUploadSetting); // Seed only into an empty table: any existing rows (including // soft-deleted ones, which would still conflict on the unique `code`) // mean the data is admin-managed, so leave it untouched. const existing = await settingRepository.count({ withDeleted: true }); if (existing > 0) { this.logger.log( `file_upload_settings already has ${existing} rows — skipping seed`, ); return; } 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, })), ...CONTRACT_CLEARANCE_SETTINGS.map((s) => ({ ...s, description: "Pre-booking clearance documents collected on the contract (Path B), by operation and freight type.", })), ...SELF_CLEARANCE_SETTINGS.map((s) => ({ ...s, description: "Customer self-clearance documents (Path A, no EDR customs service), reviewed by Operations.", })), ...CONTRACT_INTAKE_SETTINGS.map((s) => ({ ...s, description: "Commercial/framework documents attached at contract submission.", })), ...DRIVER_DOCUMENT_SETTINGS.map((s) => ({ ...s, description: "Documents uploaded against a driver profile (license, ID, contracts, etc.).", })), ]; // Insert setting rows only — no FileUploadField rows. Fields start empty // and are configured from the backoffice file-settings editor; the field // definitions above are kept as reference defaults. await settingRepository.insert( allSettings.map((documentSetting) => ({ code: documentSetting.code, label: documentSetting.label, description: documentSetting.description, entity: documentSetting.entity, })), ); this.logger.log( `Seeded ${allSettings.length} file upload settings with empty fields`, ); } }