booking cancellation

This commit is contained in:
Marshal
2026-08-15 08:53:24 +00:00
parent 086c4fe8c9
commit c9c2d4dcb3
26 changed files with 751 additions and 163 deletions

View File

@@ -187,7 +187,7 @@ const RATE_TRIGGERS = [
{ label: "Shipping line mapped", value: "SHIPPING_LINE" },
{ label: "Penalty", value: "CONSOLIDATION" },
{ label: "Lashing (bulk, per cargo type)", value: "LASHING" },
{ label: "Cancellation", value: "CANCELLATION" },
{ label: "Cancellation (per wagon, per direction + cargo type)", value: "CANCELLATION" },
{ label: "Shipping line extra fee (PIL)", value: "PIL_EXTRA_FEE" },
{ label: "Customs clearance service fee (billed with the booking)", value: "CUSTOMS_CLEARANCE" },
{ label: "Fuel (per lane + cargo type)", value: "FUEL" },
@@ -265,6 +265,13 @@ const isRouteScopedRate = (values: Record<string, unknown>) =>
(String(values.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN", "FUEL"].includes(String(values.trigger ?? "")));
/**
* Surcharges sold per cargo kind: the admin says container or bulk, then names
* the container type or bulk commodity the fee covers.
*/
const isCargoKindTrigger = (values: Record<string, unknown>) =>
["CUSTOMS_CLEARANCE", "CANCELLATION"].includes(String(values.trigger ?? ""));
const unitOption = (value: string) => ({ label: value.replace(/_/g, " "), value });
/**
@@ -304,7 +311,8 @@ const unitsForShape = (
// Container-only service — per returned container, per wagon, or flat.
return ["PER_CONTAINER", "PER_WAGON", "FLAT"];
case "CANCELLATION":
return ["FLAT", "PER_INVOICE"];
// Wagon cancellation fee — scales with the cancelled wagons only.
return ["PER_WAGON"];
case "CUSTOMS_CLEARANCE":
// Per cargo kind: container fees per box/wagon, bulk per ton/wagon.
return cargoKind === "BULK"
@@ -1054,9 +1062,10 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
showIf: (v) =>
hasShippingLine(v) && v.shippingLineRateKind === "SURCHARGE",
},
// ── Trade direction — Bulk & Container base freight, plus the route-
// scoped surcharges (customs clearance; empty-container return, which is
// import-only for now so export is not offered) ────────────────────────
// ── Trade direction — Bulk & Container base freight, plus the directed
// surcharges (customs clearance, cancellation, lashing, fuel; empty-
// container return, which is import-only for now so export is not
// offered) ─────────────────────────────────────────────────────────────
{
name: "tradeDirection",
label: "Trade direction",
@@ -1074,9 +1083,13 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
!isShippingLineRate(v) &&
(["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
(String(v.appliesTo ?? "") === "OTHER" &&
["CUSTOMS_CLEARANCE", "WITH_RETURN", "LASHING", "FUEL"].includes(
String(v.trigger ?? ""),
))),
[
"CUSTOMS_CLEARANCE",
"CANCELLATION",
"WITH_RETURN",
"LASHING",
"FUEL",
].includes(String(v.trigger ?? "")))),
},
// Shipping lines only ever ship import — the export leg is sold through
// the customer's contract — so the direction is stated, not asked. Shown
@@ -1097,8 +1110,9 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
computeValue: () => "IMPORT",
showIf: hasShippingLine,
},
// ── Cargo kind — customs clearance is priced separately for containers
// (one rate per container type) and bulk ───────────────────────────────
// ── Cargo kind — customs clearance and the cancellation fee are priced
// separately for containers (one rate per container type) and bulk (one
// rate per commodity) ──────────────────────────────────────────────────
{
name: "cargoKind",
label: "Cargo kind",
@@ -1107,11 +1121,10 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
options: INTERCITY_KINDS,
placeholder: "Is this fee for containers or bulk?",
description:
"Container fees bill per box or wagon (one rate per container type); bulk fees bill per ton or wagon.",
showIf: (v) =>
v.appliesTo === "OTHER" && v.trigger === "CUSTOMS_CLEARANCE",
"Container fees are set per container type; bulk fees per commodity. Customs: container per box or wagon, bulk per ton or wagon. Cancellation: per wagon.",
showIf: (v) => v.appliesTo === "OTHER" && isCargoKindTrigger(v),
// Not a stored column: a container fee carries its containerTypeId, a
// bulk fee carries none.
// bulk fee its cargoTypeId.
getInitialValue: (record) =>
record.containerTypeId ? "CONTAINER" : "BULK",
},
@@ -1124,10 +1137,10 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
placeholder: "Which container type this fee covers",
showIf: (v) =>
v.appliesTo === "OTHER" &&
v.trigger === "CUSTOMS_CLEARANCE" &&
isCargoKindTrigger(v) &&
v.cargoKind === "CONTAINER",
},
// ── Bulk cargo type — the bulk customs fee names its commodity ────────
// ── Bulk cargo type — the bulk fee names its commodity ────────────────
{
name: "cargoTypeId",
label: "Bulk cargo type",
@@ -1136,7 +1149,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
placeholder: "Which bulk commodity this fee covers",
showIf: (v) =>
v.appliesTo === "OTHER" &&
v.trigger === "CUSTOMS_CLEARANCE" &&
isCargoKindTrigger(v) &&
v.cargoKind === "BULK",
},
// ── Cargo type — a fuel rate names the commodity it covers (different