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"; import { poaDelegationField } from "../modules/file-upload-settings/poa-delegation.constants"; 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"]; /** * Listed in the sets below only so the reference defaults stay a complete * picture of a company onboarding form. Unlike every other field here, the DARS * delegation paper is not admin-managed: `FileUploadSettingsService.getByCode` * injects it from poa-delegation.constants.ts whether or not a row exists. */ const poaDelegationDefault = (displayOrder: number): OnboardingField => poaDelegationField(displayOrder) as unknown as OnboardingField; /** 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: 50, 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: 50, 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: 50, displayOrder: 3, }, poaDelegationDefault(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: 50, 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: 50, 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: 50, displayOrder: 3, }, { fileKey: "passport", fileLabel: "Passport", helpText: "Passport of the company's authorized representative.", isRequired: true, isMultiple: false, maxFiles: 1, allowedExtensions: DOC_EXTENSIONS, maxSizeMb: 50, displayOrder: 4, }, poaDelegationDefault(5), ]; // Legacy combined set for the removed per-company-type codes // (company_onboarding_documents_customer/forwarder/transporter/forwarder_dj). // 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: 50, // 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: 50, // 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: 50, // displayOrder: 3, // }, // ]; /** * Documents required from a co-operative union or farm — the third alternative * to the two nationality sets, not an addition to them. A co-op is always * registered in Ethiopia and has a TIN but no business licence, so its * registration certificate stands in for the commercial registration every * other Ethiopian company uploads. * * Admin-managed like every other onboarding set: what these members must * actually produce is a backoffice decision, edited in the file-settings editor. */ const COOPERATIVE_ONBOARDING_FIELDS: OnboardingField[] = [ // First on purpose: only the first field of a new set is seeded, and this is // the paper that distinguishes a co-operative from every other company. { fileKey: "cooperative_registration_certificate", fileLabel: "Co-operative Union / Farm Registration Certificate", helpText: "Certificate issued by the co-operative promotion agency that registered the union or farm.", isRequired: true, isMultiple: false, maxFiles: 1, allowedExtensions: DOC_EXTENSIONS, maxSizeMb: 50, 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: 50, 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: 50, displayOrder: 3, }, poaDelegationDefault(4), ]; 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, }, // The third set: a union or farm resolves here INSTEAD of a nationality set // (it is always Ethiopian, and holds no business licence). { code: "company_onboarding_documents_cooperative", label: "Co-operative union / farm onboarding documents", entity: "customer", fields: COOPERATIVE_ONBOARDING_FIELDS, }, // Legacy per-company-type codes — removed, unused by any resolver or portal // lookup (only `company_onboarding_documents_ethiopian`/`_foreign` are live). // { // 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"]; // only used by the commented-out output field sets below 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: 50, 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 — unused now that all // clearance_output_*/contract_clearance_output_* settings are commented out. // 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, // }), // ]; // 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) ── // Commented out per request. NOTE: these codes are still resolved and // looked up at runtime by clearanceOutputSettingCode() in clearance.util.ts, // called from booking-transition.service.ts (finalizeClearance, // uploadClearanceOutputDocuments) and booking-clearance.service.ts — // reachable from bookings.controller.ts's finalizeClearance/ // uploadClearanceOutputDocuments endpoints. finalizeClearance does not // catch getByCode()'s NotFoundException, so finalizing a customs-enabled // booking will 500 once these rows are gone from the DB too. // { // 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 ───────────────────────────────── // Removed — clearance is now collected once, per booking, instead of also on // the contract. See bookings/clearance.util.ts + CLEARANCE_DOCUMENT_SETTINGS. // const CONTRACT_CLEARANCE_ENTITY = "contract_clearance"; // Contract-level IMPORT/EXPORT clearance input codes — removed. Clearance is // now collected once, per booking, via bookings/clearance.util.ts's 4+4 // clearance_{op}_{freight}_{with|without}_customs codes. Kept here as // reference in case a contract-level step is reinstated. // const CONTRACT_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ // { // code: "contract_clearance_import_container_with_customs", // label: "Contract clearance documents (import container, with customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: IMPORT_CONTAINER_FIELDS, // }, // { // code: "contract_clearance_export_container_with_customs", // label: "Contract clearance documents (export container, with customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: EXPORT_CONTAINER_FIELDS, // }, // { // code: "contract_clearance_import_bulk_with_customs", // label: "Contract clearance documents (import bulk, with customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: IMPORT_BULK_FIELDS, // }, // { // code: "contract_clearance_export_bulk_with_customs", // label: "Contract clearance documents (export bulk, with customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: EXPORT_BULK_FIELDS, // }, // ]; // GL ET output sets uploaded during pre-booking clearance — commented out // per request. NOTE: still resolved/looked up at runtime by // contractClearanceOutputSettingCode() in contract-clearance.util.ts, // called from contract-clearance.service.ts (finalize, uploadOutputDocuments) // — reachable from contracts.controller.ts. `finalize` does not catch // getByCode()'s NotFoundException, so finalizing a customs-enabled contract // will 500 once these rows are gone from the DB too. // const CONTRACT_CLEARANCE_OUTPUT_SETTINGS: OnboardingDocumentSetting[] = [ // { // 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, // }, // { // 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 — removed (contract-level clearance input // codes no longer exist; see CONTRACT_CLEARANCE_SETTINGS above). Kept as // reference in case a contract-level step is reinstated. // 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_import_container_without_customs", // label: "Contract clearance documents (import container, without customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: SELF_CLEARANCE_IMPORT_FIELDS, // }, // { // code: "contract_clearance_export_container_without_customs", // label: "Contract clearance documents (export container, without customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: SELF_CLEARANCE_EXPORT_FIELDS, // }, // { // code: "contract_clearance_import_bulk_without_customs", // label: "Contract clearance documents (import bulk, without customs)", // entity: CONTRACT_CLEARANCE_ENTITY, // fields: SELF_CLEARANCE_IMPORT_FIELDS, // }, // { // code: "contract_clearance_export_bulk_without_customs", // label: "Contract clearance documents (export bulk, without customs)", // 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"; // Removed — unused. Nothing resolves/looks up `contract_intake_documents` by // code anywhere in the API or web apps. // 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: 50, displayOrder: 1, }, ]; const DRIVER_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ { code: "driver_docs", label: "Driver documents", entity: "driver", fields: DRIVER_DOCUMENT_FIELDS, }, ]; // ── Intercity documents ───────────────────────────────────────────────────── // One shared set for DOMESTIC (intercity) shipments, reviewed by Operations. // ONE_TIME contracts collect it at contract level after both signatures; // GENERAL contracts collect it per booking right after the booking is created. // Fields start empty and are configured in the backoffice file-settings editor. const INTERCITY_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ { code: "intercity_documents", label: "Intercity documents", entity: "booking", fields: [], }, ]; // ── Shipping line booking documents ───────────────────────────────────────── // Collected on a shipping line's booking right after it is initiated. Shipping // lines book without a contract, so this set — not a contract — is what // Operations reviews before the booking may be completed. Fields start empty // and are configured in the backoffice file-settings editor, like the sets // above. `entity: "booking"` puts it alongside the other per-booking sets. const SHIPPING_LINE_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ { code: "shipping_line_booking_documents", label: "Shipping line booking documents", entity: "booking", fields: [], }, ]; // ── Hazardous cargo documents ─────────────────────────────────────────────── // Asked for in the contract wizard the moment the customer flags the cargo as // hazardous (ONE_TIME contracts only). Fields start empty and are configured in // the backoffice file-settings editor. const HAZARDOUS_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ { code: "hazardous_documents", label: "Hazardous cargo documents", entity: CONTRACT_INTAKE_ENTITY, 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 per CODE, not "only into an empty table". An existing row is // admin-managed and never touched — including a soft-deleted one, which // means the set was removed on purpose (and would still conflict on the // unique `code`). What the table-wide check got wrong is the other half: a // set added to this file after the first boot could never reach a database // that already held the others, so it existed in code and nowhere else. const existingCodes = new Set( (await settingRepository.find({ withDeleted: true })).map((s) => s.code), ); 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, })), ...DRIVER_DOCUMENT_SETTINGS.map((s) => ({ ...s, description: "Documents uploaded against a driver profile (license, ID, contracts, etc.).", })), ...HAZARDOUS_DOCUMENT_SETTINGS.map((s) => ({ ...s, description: "Documents required when a one-time contract's cargo is flagged hazardous.", })), ...INTERCITY_DOCUMENT_SETTINGS.map((s) => ({ ...s, description: "Intercity shipment documents — contract-level for ONE_TIME (after both signatures), per booking for GENERAL; reviewed by Operations.", })), ...SHIPPING_LINE_DOCUMENT_SETTINGS.map((s) => ({ ...s, description: "Documents a shipping line uploads on a booking it initiated. Reviewed by Operations; the booking can only be completed once they are approved.", })), ]; const missing = allSettings.filter((s) => !existingCodes.has(s.code)); if (missing.length === 0) { this.logger.log("file upload settings up to date — nothing to seed"); return; } const inserted = await settingRepository.save( missing.map((documentSetting) => settingRepository.create({ code: documentSetting.code, label: documentSetting.label, description: documentSetting.description, entity: documentSetting.entity, }), ), ); // A brand-new set gets exactly ONE field: its first reference default. The // rest of the list above stays documentation — what a set actually asks for // is a backoffice decision, edited in the file-settings editor. Seeding one // means a set is never born empty (an empty set silently requires nothing), // while leaving the admin a single row to extend rather than a list to prune. const fieldRepository = this.dataSource.getRepository(FileUploadField); const firstFields = inserted.flatMap((setting) => { const reference = missing.find((s) => s.code === setting.code)?.fields[0]; return reference ? [fieldRepository.create({ ...reference, settingId: setting.id })] : []; }); if (firstFields.length > 0) await fieldRepository.save(firstFields); this.logger.log( `Seeded ${missing.length} file upload settings (${firstFields.length} with a default field): ${missing .map((s) => s.code) .join(", ")}`, ); } }