feat: empty container Import

This commit is contained in:
hager
2026-09-04 22:43:20 +00:00
parent 2b6c78cf71
commit 3fb5cdf29f
36 changed files with 968 additions and 49 deletions

View File

@@ -53,24 +53,60 @@ describe('contractTemplateCodeFor', () => {
it('only ever resolves to a code that exists', () => {
const directions = ['IMPORT', 'EXPORT', 'DOMESTIC', null];
const freights = ['BULK', 'CONTAINER', 'BREAK_BULK', null];
const conditions = ['LADEN', 'EMPTY', null, undefined];
for (const d of directions) {
for (const f of freights) {
for (const c of [true, false]) {
for (const e of [true, false, undefined]) {
expect(CONTRACT_TEMPLATE_CODES).toContain(
contractTemplateCodeFor(d, f, c, e),
);
for (const cond of conditions) {
expect(CONTRACT_TEMPLATE_CODES).toContain(
contractTemplateCodeFor(d, f, c, e, cond),
);
}
}
}
}
}
});
// Empty equipment is a carriage agreement, not a cargo contract: no cargo
// liability, no VGM declaration, no commercial documents, no customs leg.
it('gives empty container import its own customs-free paper', () => {
for (const customs of [true, false]) {
for (const ethiopian of [true, false, undefined]) {
expect(
contractTemplateCodeFor('IMPORT', 'CONTAINER', customs, ethiopian, 'EMPTY'),
).toBe('IMPORT_EMPTY_CONTAINER');
}
}
});
it('leaves laden contracts on the laden codes', () => {
expect(
contractTemplateCodeFor('IMPORT', 'CONTAINER', false, false, 'LADEN'),
).toBe('IMPORT_CONTAINER_NO_CUSTOMS');
expect(contractTemplateCodeFor('IMPORT', 'CONTAINER', false, false)).toBe(
'IMPORT_CONTAINER_NO_CUSTOMS',
);
});
// Empty rates and empty bookings are import-only, so a stray EMPTY on any
// other direction must fall through rather than resolve a template that
// describes a Djibouti-to-Ethiopia movement.
it('ignores the empty condition outside import', () => {
expect(
contractTemplateCodeFor('EXPORT', 'CONTAINER', false, false, 'EMPTY'),
).toBe('EXPORT_CONTAINER_NO_CUSTOMS');
expect(
contractTemplateCodeFor('DOMESTIC', 'CONTAINER', false, false, 'EMPTY'),
).toBe('INTERCITY_CONTAINER');
});
});
describe('CONTRACT_TEMPLATE_DEFAULTS', () => {
it('seeds exactly the fourteen declared codes, once each', () => {
it('seeds exactly the fifteen declared codes, once each', () => {
const seeded = CONTRACT_TEMPLATE_DEFAULTS.map((t) => t.code).sort();
expect(seeded).toHaveLength(14);
expect(seeded).toHaveLength(15);
expect(seeded).toEqual([...CONTRACT_TEMPLATE_CODES].sort());
});

View File

@@ -54,6 +54,9 @@ const PREVIEW_TEMPLATE_KEYS: Record<ContractTemplateCode, string> = {
EXPORT_CONTAINER_ETHIOPIAN_CUSTOMS: "EXP_CON_USD_FORWARDING",
EXPORT_CONTAINER_NO_CUSTOMS: "EXP_CON_USD_TRANSPORT_ONLY",
INTERCITY_CONTAINER: "DOM_CON_USD_TRANSPORT_ONLY",
// Carriage of the equipment itself — no cargo, no clearing, so it previews
// against the transport-only scope like every other non-customs code.
IMPORT_EMPTY_CONTAINER: "IMP_CON_USD_TRANSPORT_ONLY",
};
@Injectable()
@@ -216,6 +219,7 @@ export class ContractTemplatesService {
customsClearingEnabled?: boolean | null,
cargoTypeId?: string | null,
ethiopianCustomsOnly?: boolean | null,
cargoCondition?: string | null,
): Promise<ContractTemplate | null> {
const isBulk = (freightType ?? "").toUpperCase().includes("BULK");
if (isBulk) {
@@ -235,6 +239,7 @@ export class ContractTemplatesService {
freightType,
customsClearingEnabled,
ethiopianCustomsOnly,
cargoCondition,
);
const template = await this.repository.findByCode(code);
return template?.isActive ? template : null;

View File

@@ -41,6 +41,14 @@ export const CONTRACT_TEMPLATE_CODES = [
"EXPORT_CONTAINER_ETHIOPIAN_CUSTOMS",
"EXPORT_CONTAINER_NO_CUSTOMS",
"INTERCITY_CONTAINER",
/**
* Empty container import — bare equipment railed north from Djibouti. No
* customs split: an empty box carries no declaration to clear, the same
* reason intercity has a single unsuffixed code. Import-only, matching the
* rate rule (southbound empties are served by the WITH_RETURN surcharge and
* empty_return_requests instead).
*/
"IMPORT_EMPTY_CONTAINER",
] as const;
export type ContractTemplateCode = (typeof CONTRACT_TEMPLATE_CODES)[number];
@@ -74,7 +82,14 @@ export function contractTemplateCodeFor(
freightType?: string | null,
customsClearingEnabled?: boolean | null,
ethiopianCustomsOnly?: boolean | null,
cargoCondition?: string | null,
): ContractTemplateCode {
// Empty equipment is its own paper: a straight carriage agreement with no
// cargo liability, no VGM declaration and no customs leg. Import-only, so
// anything else falls through to the laden codes below.
if (cargoCondition === "EMPTY" && tradeDirection === "IMPORT") {
return "IMPORT_EMPTY_CONTAINER";
}
const direction =
tradeDirection === "IMPORT"
? "IMPORT"