mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
import { Booking } from './entities/booking.entity';
|
||
|
||
/**
|
||
* Resolves which seeded clearance FileUploadSetting applies to a booking, from
|
||
* its trade direction, freight type and whether its service includes customs.
|
||
* Mirrors the codes seeded in file-upload-settings.seeder.ts.
|
||
*/
|
||
|
||
type Op = 'import' | 'export';
|
||
type Freight = 'container' | 'bulk';
|
||
|
||
/**
|
||
* The single (admin-configured) document set intercity shipments upload.
|
||
* DOMESTIC has no customs, so one shared set serves every intercity booking —
|
||
* ONE_TIME and GENERAL alike, collected per booking and reviewed by Operations.
|
||
*/
|
||
export const INTERCITY_DOCUMENTS_SETTING_CODE = 'intercity_documents';
|
||
|
||
/**
|
||
* The document set a shipping line uploads on a booking it initiated.
|
||
*
|
||
* Shipping lines book without a contract, so none of the trade-direction /
|
||
* freight / customs matrix below applies to them — this one admin-configured
|
||
* set is what Operations reviews before the booking may be completed.
|
||
*/
|
||
export const SHIPPING_LINE_DOCUMENTS_SETTING_CODE =
|
||
'shipping_line_booking_documents';
|
||
|
||
/** 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 customs operation
|
||
}
|
||
|
||
function freightFor(freightType: string): Freight {
|
||
return freightType === 'BULK' ? 'bulk' : 'container';
|
||
}
|
||
|
||
/** The customer-input clearance setting code, or null when no gate applies. */
|
||
export function clearanceSettingCode(
|
||
tradeDirection: string,
|
||
freightType: string,
|
||
includesCustoms: boolean,
|
||
): string | null {
|
||
// Intercity: no customs, but the admin-configured intercity document set is
|
||
// still collected and ops-reviewed before the shipment may board a train.
|
||
if (tradeDirection === 'DOMESTIC') return INTERCITY_DOCUMENTS_SETTING_CODE;
|
||
const op = operationFor(tradeDirection);
|
||
if (!op) return null;
|
||
const freight = freightFor(freightType);
|
||
// 4 import + 4 export cases (bulk/container × with/without customs) — each
|
||
// booking resolves to its own clearance_{op}_{freight}_{with|without}_customs
|
||
// set, independent of any contract-level clearance codes.
|
||
if (!includesCustoms) {
|
||
return `clearance_${op}_${freight}_without_customs`;
|
||
}
|
||
return `clearance_${op}_${freight}_with_customs`;
|
||
}
|
||
|
||
/** The GL-output (customs output) setting code, keyed on op + freight. */
|
||
export function clearanceOutputSettingCode(
|
||
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 `clearance_output_${op}_${freight}`;
|
||
}
|
||
|
||
/** Convenience: resolve both codes for a loaded booking (with its serviceType). */
|
||
export function clearanceCodesForBooking(booking: Booking): {
|
||
inputCode: string | null;
|
||
outputCode: string | null;
|
||
includesCustoms: boolean;
|
||
} {
|
||
// Shipping-line bookings resolve to their own single set and never reach the
|
||
// matrix below: they have no contract, and their trade direction / freight
|
||
// type are placeholders until the booking is completed, so the customer codes
|
||
// would resolve to a set that was never meant for them. Keyed off the owner
|
||
// column, which is NULL on every customer booking — so no customer booking
|
||
// can take this branch.
|
||
if (booking.shippingLineCompanyId) {
|
||
return {
|
||
inputCode: SHIPPING_LINE_DOCUMENTS_SETTING_CODE,
|
||
outputCode: null,
|
||
includesCustoms: false,
|
||
};
|
||
}
|
||
|
||
// Customs applies when EITHER the service type bundles it OR the booking was
|
||
// created with customsClearingEnabled (copied from the contract). Contract
|
||
// bookings carry customsClearingEnabled even when the serviceType relation
|
||
// isn't loaded / has includesCustoms=false — without this the per-booking
|
||
// clearance grid would resolve empty.
|
||
const includesCustoms =
|
||
Boolean(booking.serviceType?.includesCustoms) ||
|
||
Boolean(booking.customsClearingEnabled);
|
||
return {
|
||
inputCode: clearanceSettingCode(
|
||
booking.tradeDirection,
|
||
booking.freightType,
|
||
includesCustoms,
|
||
),
|
||
outputCode: clearanceOutputSettingCode(
|
||
booking.tradeDirection,
|
||
booking.freightType,
|
||
includesCustoms,
|
||
),
|
||
includesCustoms,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Statuses after which clearance documents are closed: the shipment is paid
|
||
* and moving. Everything before that — review, clearance ready, operation
|
||
* request, batch selection, PNR, payment verification — still accepts new
|
||
* customer documents and still lets GL review them.
|
||
*/
|
||
const CLEARANCE_DOCS_CLOSED_STATUSES = new Set<string>([
|
||
'PAID',
|
||
'IN_TRANSIT',
|
||
'ARRIVED',
|
||
'COMPLETED',
|
||
'REJECTED',
|
||
'CANCELLED',
|
||
'EXPIRED',
|
||
]);
|
||
|
||
/**
|
||
* True while the customer may still attach clearance documents and GL may
|
||
* still approve or query them.
|
||
*
|
||
* Clearance finalization is NOT the cut-off: a customs shipment keeps
|
||
* collecting paperwork (amended invoices, revised packing lists, port
|
||
* documents) right up to the final invoice being settled. Both the customer's
|
||
* upload endpoint and GL's review endpoint gate on this one predicate, so the
|
||
* two sides can never drift apart.
|
||
*/
|
||
export function clearanceDocumentsOpen(booking: Booking): boolean {
|
||
if (CLEARANCE_DOCS_CLOSED_STATUSES.has(booking.status)) return false;
|
||
// Payment settled ahead of the status transition (webhook ordering).
|
||
if (booking.paymentStatus === 'PAID') return false;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* The label the customer typed for an ad-hoc clearance document, recovered from
|
||
* its file code. The portal encodes it as `custom_<slug>_<n>`; a plain
|
||
* `custom_<n>` (older uploads, or an unnamed row) yields null so callers fall
|
||
* back to the filename.
|
||
*/
|
||
export function adHocLabel(fileKey: string): string | null {
|
||
const m = /^custom_(.+)_\d+$/.exec(fileKey);
|
||
if (!m) return null;
|
||
// Legacy keys are `custom_<timestamp>_<n>`, which this regex reads as a label
|
||
// of digits. Those carry no name — reject them so the caller falls back to
|
||
// the filename instead of showing "1755780000000".
|
||
if (/^\d+$/.test(m[1])) return null;
|
||
const label = m[1].replace(/-/g, ' ').trim();
|
||
return label ? label.charAt(0).toUpperCase() + label.slice(1) : null;
|
||
}
|