Bulk (per ton per km) last-mile rates now carry From/To km bands like

container mode: the Add Rate dialog offers the multi-tier editor in both
modes, each tier is created as its own rate row, and overlapping bulk
bands are rejected. Pricing picks the tier whose half-open band holds
the trip km, falling back to the legacy bandless bulk rate.
This commit is contained in:
Hagernesh
2026-08-06 08:42:13 +00:00
parent 69f6fd36a9
commit 95fad6f095
5 changed files with 125 additions and 60 deletions

View File

@@ -306,34 +306,27 @@ const RuleEngineResourcePage = () => {
const formFields = useMemo(() => {
if (!config) return [];
// Last-mile container bands: creating uses the multi-row tier list (one
// rate per tier); editing an existing band row keeps the single
// From/To/value fields (a rate row IS one band).
// Last-mile bands (both modes): creating uses the multi-row tier list (one
// rate per tier, each tier carrying its own rate value); editing an
// existing band row keeps the single From/To/value fields (a rate row IS
// one band).
const bandFields = config.formFields.filter((field) => {
if (config.slug !== "rates") return true;
if (field.type === "tierList") return !editing;
if (editing) return true;
return field.name !== "minKm" && field.name !== "maxKm";
});
return bandFields.map((field) => {
// On create, the tier rows carry the per-band rate values — the single
// last-mile "Rate value" field then only applies to bulk mode.
// On create the tier rows carry From/To/value — drop the single fields,
// including the last-mile "Rate value" (the non-last-mile one keeps its
// own showIf).
if (
config.slug === "rates" &&
!editing &&
field.name === "rateValue" &&
field.showWhen?.field === "appliesTo" &&
field.showWhen.equals.includes("LAST_MILE")
) {
return {
...field,
showWhen: undefined,
showIf: (values: Record<string, unknown>) =>
values.appliesTo === "LAST_MILE" && values.lastMileMode === "BULK",
};
return false;
}
return field;
}).map((field) => {
return field.name !== "minKm" && field.name !== "maxKm";
});
return bandFields.map((field) => {
if (isPriorityRules && field.name === "minWagonCount") {
return {
...field,
@@ -621,19 +614,15 @@ const RuleEngineResourcePage = () => {
currency: isLastMile ? (values.currency ?? "ETB") : "USD",
trigger: isSurcharge ? values.trigger : "ALWAYS",
...(isLastMile
? lastMileMode === "BULK"
? {
rateUnit: "PER_TON_KM",
containerTypeId: undefined,
minKm: undefined,
maxKm: undefined,
}
: {
rateUnit: "PER_KM",
// Empty "To km" means an open-ended band — send null so an
// edit can clear a previously-set ceiling.
maxKm: values.maxKm ?? null,
}
? {
// Empty "To km" means an open-ended band — send null so an
// edit can clear a previously-set ceiling. On create the tier
// spread below overrides the band fields per tier.
maxKm: values.maxKm ?? null,
...(lastMileMode === "BULK"
? { rateUnit: "PER_TON_KM", containerTypeId: undefined }
: { rateUnit: "PER_KM" }),
}
: {}),
};
// Editing a LIVE rate files a change request — the rate keeps charging

View File

@@ -1003,7 +1003,8 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
placeholder: "0",
description: "Band start (inclusive). Use 0 for the first band.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" && v.lastMileMode === "CONTAINER",
v.appliesTo === "LAST_MILE" &&
(v.lastMileMode === "CONTAINER" || v.lastMileMode === "BULK"),
},
{
name: "maxKm",
@@ -1013,7 +1014,8 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
placeholder: "Leave empty for no upper limit",
description: "Band end (exclusive) — a 030 band covers up to but not including 30 km.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" && v.lastMileMode === "CONTAINER",
v.appliesTo === "LAST_MILE" &&
(v.lastMileMode === "CONTAINER" || v.lastMileMode === "BULK"),
},
{
name: "currency",
@@ -1035,9 +1037,10 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
type: "tierList",
required: true,
description:
"One rate per distance range. To km is exclusive (030 then 30+); leave the last tier's To km empty for no upper limit.",
"One rate per distance range — the rate value is per km (container mode) or per ton per km (bulk mode). To km is exclusive (030 then 30+); leave the last tier's To km empty for no upper limit.",
showIf: (v) =>
v.appliesTo === "LAST_MILE" && v.lastMileMode === "CONTAINER",
v.appliesTo === "LAST_MILE" &&
(v.lastMileMode === "CONTAINER" || v.lastMileMode === "BULK"),
},
// ── Container type — Container freight, container-kind intercity, and
// the empty-container return surcharge (20ft vs 40ft price differently) ─