Files
edr-platform/apps/edr-freight-api/src/modules/contracts/contract-clearance.util.ts
Marshal a8788eb549 enhance contract and train scheduling features
- Added pricing service integration to ContractsService for pre-persistence contract pricing.
- Updated LegCapacityPanel to display cargo weight instead of gross weight for better clarity on booked cargo.
- Enhanced train scheduling service to include cargo weight in booking details.
- Modified ClearanceDocumentsPage to include FULLY_EXECUTED status in booking status options.
- Refactored FileUploadSettingsPage to categorize file upload settings into tabs for better organization.
- Removed legacy onboarding fields and settings from file upload settings seeder.
- Improved type definitions for train scheduling to include cargo weight without wagon tare.
2026-08-01 05:37:46 +00:00

125 lines
4.2 KiB
TypeScript

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;
}