mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
feat(rule-engine): add fuel surcharge rate trigger and cargo flag
This commit is contained in:
@@ -55,6 +55,8 @@ interface CargoNode extends RuleEngineRecord {
|
||||
requiresDirectorApproval?: boolean;
|
||||
/** When true, bookings of this cargo type incur the flat LASHING surcharge. */
|
||||
hasLashing?: boolean;
|
||||
/** When true, bookings of this cargo type incur the lane-scoped FUEL surcharge. */
|
||||
hasFuel?: boolean;
|
||||
/** Staff may write bulk contract templates for this cargo type (parent XOR children). */
|
||||
hasContractTemplate?: boolean;
|
||||
/** How this cargo is measured (PER_TON / PER_ITEM); null for groups/unset. */
|
||||
@@ -114,6 +116,9 @@ const FORM_FIELDS: FormFieldDef[] = [
|
||||
// When on, every booking of this cargo type is charged the flat LASHING
|
||||
// surcharge (a rate with trigger = Lashing).
|
||||
{ name: "hasLashing", label: "Charge lashing fee", type: "boolean" },
|
||||
// When on, bookings of this cargo type incur the fuel surcharge, billed off
|
||||
// the lane-scoped FUEL rate (direction + route + cargo type) under Rates.
|
||||
{ name: "hasFuel", label: "Charge fuel fee", type: "boolean" },
|
||||
// Lets staff write bulk contract templates for this cargo type. The API
|
||||
// rejects the save when the parent group (or a child) already has it on —
|
||||
// the template must live on exactly one level.
|
||||
|
||||
@@ -100,19 +100,27 @@ const yardOptionsForLegEnd = (
|
||||
} else if (
|
||||
appliesTo === "CONTAINER" ||
|
||||
appliesTo === "BULK" ||
|
||||
// Customs clearance + empty-container return are sold per direction +
|
||||
// route, so their yard dropdowns narrow exactly like base freight.
|
||||
// Customs clearance, empty-container return and fuel are sold per
|
||||
// direction + route, so their yard dropdowns narrow exactly like base
|
||||
// freight.
|
||||
(appliesTo === "OTHER" &&
|
||||
["CUSTOMS_CLEARANCE", "WITH_RETURN"].includes(String(values.trigger ?? "")))
|
||||
["CUSTOMS_CLEARANCE", "WITH_RETURN", "FUEL"].includes(
|
||||
String(values.trigger ?? ""),
|
||||
))
|
||||
) {
|
||||
const direction = String(values.tradeDirection ?? "");
|
||||
// Direction is what decides the countries, so offer nothing until it is set
|
||||
// rather than defaulting to one and letting it read as a real choice.
|
||||
if (direction !== "IMPORT" && direction !== "EXPORT") return [];
|
||||
const startsInEthiopia = direction === "EXPORT";
|
||||
country = (end === "origin" ? startsInEthiopia : !startsInEthiopia)
|
||||
? "Ethiopia"
|
||||
: "Djibouti";
|
||||
if (direction === "DOMESTIC") {
|
||||
// A fuel rate's intercity lane — stays inside Ethiopia.
|
||||
country = "Ethiopia";
|
||||
} else {
|
||||
if (direction !== "IMPORT" && direction !== "EXPORT") return [];
|
||||
const startsInEthiopia = direction === "EXPORT";
|
||||
country = (end === "origin" ? startsInEthiopia : !startsInEthiopia)
|
||||
? "Ethiopia"
|
||||
: "Djibouti";
|
||||
}
|
||||
}
|
||||
if (!country) return [];
|
||||
return yards
|
||||
|
||||
@@ -181,6 +181,17 @@ const RATE_TRIGGERS = [
|
||||
{ label: "Cancellation", 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" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Fuel lanes: import/export like base freight, plus a domestic intercity lane
|
||||
* (stored as DOMESTIC — matches the booking's own trade direction).
|
||||
*/
|
||||
const FUEL_TRADE_DIRECTIONS = [
|
||||
{ label: "Import", value: "IMPORT" },
|
||||
{ label: "Export", value: "EXPORT" },
|
||||
{ label: "Intercity", value: "DOMESTIC" },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -205,7 +216,7 @@ const isBaseFreightRate = (values: Record<string, unknown>) =>
|
||||
const isRouteScopedRate = (values: Record<string, unknown>) =>
|
||||
isBaseFreightRate(values) ||
|
||||
(String(values.appliesTo ?? "") === "OTHER" &&
|
||||
["CUSTOMS_CLEARANCE", "WITH_RETURN"].includes(String(values.trigger ?? "")));
|
||||
["CUSTOMS_CLEARANCE", "WITH_RETURN", "FUEL"].includes(String(values.trigger ?? "")));
|
||||
|
||||
const unitOption = (value: string) => ({ label: value.replace(/_/g, " "), value });
|
||||
|
||||
@@ -255,6 +266,9 @@ const unitsForShape = (
|
||||
case "LASHING":
|
||||
// Bulk-only cargo securing — per ton or per wagon.
|
||||
return ["PER_TON", "PER_WAGON"];
|
||||
case "FUEL":
|
||||
// Per wagon (wagons × rate) or per liter (base liters × rate, once).
|
||||
return ["PER_WAGON", "PER_LITER"];
|
||||
case "CONSOLIDATION":
|
||||
case "SHIPPING_LINE":
|
||||
case "PIL_EXTRA_FEE":
|
||||
@@ -371,6 +385,13 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
},
|
||||
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
|
||||
{ name: "hasLashing", label: "Charge lashing fee", type: "boolean" },
|
||||
{
|
||||
name: "hasFuel",
|
||||
label: "Charge fuel fee",
|
||||
type: "boolean",
|
||||
description:
|
||||
"Bookings of this cargo incur the fuel surcharge (configure the FUEL rate per lane under Rates).",
|
||||
},
|
||||
{ name: "isActive", label: "Active", type: "boolean" },
|
||||
],
|
||||
},
|
||||
@@ -834,7 +855,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
filters: {
|
||||
appliesTo: "OTHER",
|
||||
trigger:
|
||||
"HAZARDOUS,OVERWEIGHT,REEFER,SHIPPING_LINE,CONSOLIDATION,LASHING,CANCELLATION,PIL_EXTRA_FEE",
|
||||
"HAZARDOUS,OVERWEIGHT,REEFER,SHIPPING_LINE,CONSOLIDATION,LASHING,CANCELLATION,PIL_EXTRA_FEE,FUEL",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -886,14 +907,17 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
type: "select",
|
||||
required: true,
|
||||
optionsFromValues: (v: Record<string, unknown>) =>
|
||||
String(v.trigger ?? "") === "WITH_RETURN" &&
|
||||
String(v.appliesTo ?? "") === "OTHER"
|
||||
String(v.appliesTo ?? "") === "OTHER" &&
|
||||
String(v.trigger ?? "") === "WITH_RETURN"
|
||||
? TRADE_DIRECTIONS.filter((d) => d.value === "IMPORT")
|
||||
: TRADE_DIRECTIONS.filter((d) => d.value !== "BOTH"),
|
||||
: String(v.appliesTo ?? "") === "OTHER" &&
|
||||
String(v.trigger ?? "") === "FUEL"
|
||||
? FUEL_TRADE_DIRECTIONS
|
||||
: TRADE_DIRECTIONS.filter((d) => d.value !== "BOTH"),
|
||||
showIf: (v) =>
|
||||
["BULK", "CONTAINER"].includes(String(v.appliesTo ?? "")) ||
|
||||
(String(v.appliesTo ?? "") === "OTHER" &&
|
||||
["CUSTOMS_CLEARANCE", "WITH_RETURN", "LASHING"].includes(
|
||||
["CUSTOMS_CLEARANCE", "WITH_RETURN", "LASHING", "FUEL"].includes(
|
||||
String(v.trigger ?? ""),
|
||||
)),
|
||||
},
|
||||
@@ -939,6 +963,18 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
v.trigger === "CUSTOMS_CLEARANCE" &&
|
||||
v.cargoKind === "BULK",
|
||||
},
|
||||
// ── Cargo type — a fuel rate names the commodity it covers (different
|
||||
// commodities price differently on the same lane) ──────────────────────
|
||||
{
|
||||
name: "cargoTypeId",
|
||||
label: "Cargo type",
|
||||
type: "select",
|
||||
required: true,
|
||||
placeholder: "Which cargo type this fuel rate covers",
|
||||
description:
|
||||
"Fuel is charged for bookings of this cargo type (needs “Charge fuel fee” enabled on the cargo type).",
|
||||
showIf: (v) => v.appliesTo === "OTHER" && v.trigger === "FUEL",
|
||||
},
|
||||
// ── Bulk cargo type — lashing is bulk-only; may narrow to one leaf
|
||||
// commodity (specific wins over the commodity-wide catch-all) ──────────
|
||||
{
|
||||
@@ -1123,6 +1159,20 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
String(v.trigger ?? "") !== "OVERWEIGHT" &&
|
||||
String(v.appliesTo ?? "") !== "LAST_MILE",
|
||||
},
|
||||
// ── Base liters — per-liter fuel rates only ────────────────────────────
|
||||
{
|
||||
name: "baseLiters",
|
||||
label: "Base (liters)",
|
||||
type: "number",
|
||||
required: true,
|
||||
placeholder: "e.g. 100",
|
||||
description:
|
||||
"Liters the surcharge covers — price = base liters × rate value, charged once per booking.",
|
||||
showIf: (v) =>
|
||||
v.appliesTo === "OTHER" &&
|
||||
v.trigger === "FUEL" &&
|
||||
v.rateUnit === "PER_LITER",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface Rate {
|
||||
currency: string;
|
||||
rateValue: string;
|
||||
rateUnit: string;
|
||||
/** PER_LITER fuel rates only: price = baseLiters × rateValue, once per booking. */
|
||||
baseLiters?: string | null;
|
||||
status: string;
|
||||
proposedByStaffId: string;
|
||||
approvedByCeoId: string | null;
|
||||
|
||||
Reference in New Issue
Block a user