import { BadRequestException } from '@nestjs/common'; import { Contract } from './entities/contract.entity'; import { INTERCITY_DOCUMENTS_SETTING_CODE } from '../bookings/clearance.util'; /** * Resolves which seeded clearance FileUploadSetting applies to a contract during * the CONTRACT pre-booking phase (Path B). Mirrors clearance.util.ts but emits * `contract_clearance_*` codes keyed on (tradeDirection, freightType, customs). */ type Op = 'import' | 'export'; type Freight = 'container' | 'bulk'; /** Trade direction → clearance operation. DOMESTIC has no customs clearance. */ function operationFor(tradeDirection: string): Op | null { if (tradeDirection === 'IMPORT') return 'import'; if (tradeDirection === 'EXPORT') return 'export'; return null; // DOMESTIC / intercity — no clearance gate } function freightFor(freightType: string): Freight { return freightType === 'BULK' ? 'bulk' : 'container'; } /** * The customer-input clearance setting code, or null when no gate applies. * * Contract-level IMPORT/EXPORT clearance has been removed — clearance is * collected per booking instead (see bookings/clearance.util.ts), so this * always returns null for IMPORT/EXPORT now. * * DOMESTIC/intercity has no border, but a ONE_TIME intercity contract still * collects the admin-configured intercity document set after both signatures * (ops-reviewed). GENERAL intercity contracts skip the contract gate and * collect the same set per booking instead. */ export function contractClearanceSettingCode( tradeDirection: string, _freightType: string, _includesCustoms: boolean, ): string | null { if (tradeDirection === 'DOMESTIC') return INTERCITY_DOCUMENTS_SETTING_CODE; return null; } /** The GL-output (customs output) setting code, keyed on op + freight. */ export function contractClearanceOutputSettingCode( tradeDirection: string, freightType: string, includesCustoms: boolean, ): string | null { if (!includesCustoms) return null; const op = operationFor(tradeDirection); if (!op) return null; const freight = freightFor(freightType); return `contract_clearance_output_${op}_${freight}`; } /** Convenience: resolve both codes for a loaded contract. */ export function contractClearanceCodes(contract: Contract): { inputCode: string | null; outputCode: string | null; includesCustoms: boolean; } { const includesCustoms = contract.customsClearingEnabled ?? false; return { inputCode: contractClearanceSettingCode( contract.tradeDirection, contract.freightType, includesCustoms, ), outputCode: contractClearanceOutputSettingCode( contract.tradeDirection, contract.freightType, includesCustoms, ), includesCustoms, }; } /** * Djibouti GL cannot record a Delivery Order without saying WHEN the vessel * arrived and WHEN the DO was collected — the file alone leaves the import * timeline unauditable. Shared by the contract and per-booking DO uploads so * one endpoint can never be laxer than the other. * * Returns the normalized `YYYY-MM-DD` pair; throws if either is missing, * unparseable, or the DO predates the vessel's arrival. */ export function assertDoCollectionDates(dates?: { vesselArrivalDate?: string; doCollectedDate?: string; }): { vesselArrivalDate: string; doCollectedDate: string } { const vesselArrivalDate = normalizeDoDate( dates?.vesselArrivalDate, 'Vessel arrival date', ); const doCollectedDate = normalizeDoDate( dates?.doCollectedDate, 'DO collected date', ); if (doCollectedDate < vesselArrivalDate) { throw new BadRequestException( 'DO collected date cannot be earlier than the vessel arrival date.', ); } return { vesselArrivalDate, doCollectedDate }; } /** `YYYY-MM-DD` or throw — the column is a DATE, so time zones never enter. */ function normalizeDoDate(value: string | undefined, label: string): string { const trimmed = value?.trim(); if (!trimmed) { throw new BadRequestException(`${label} is required to upload a Delivery Order.`); } const date = trimmed.slice(0, 10); if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || Number.isNaN(Date.parse(date))) { throw new BadRequestException(`${label} is not a valid date.`); } return date; }