mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
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:
@@ -85,6 +85,48 @@ describe('computeLastMileCharge', () => {
|
||||
expect(charge).toMatchObject({ mode: 'BULK', total: 60 * 26 * 25, currency: 'ETB' });
|
||||
});
|
||||
|
||||
it('picks the bulk rate whose distance band holds the km (half-open boundary)', () => {
|
||||
const bulkNear = rate({ rateUnit: 'PER_TON_KM', rateValue: 30, minKm: 0, maxKm: 30 });
|
||||
const bulkFar = rate({ rateUnit: 'PER_TON_KM', rateValue: 22, minKm: 30, maxKm: null });
|
||||
const near = computeLastMileCharge({
|
||||
freightType: 'BULK',
|
||||
tons: 10,
|
||||
km: 12,
|
||||
containers: [],
|
||||
liveRates: [bulkNear, bulkFar],
|
||||
});
|
||||
expect(near).toMatchObject({ mode: 'BULK', total: 10 * 12 * 30 });
|
||||
const boundary = computeLastMileCharge({
|
||||
freightType: 'BULK',
|
||||
tons: 10,
|
||||
km: 30,
|
||||
containers: [],
|
||||
liveRates: [bulkNear, bulkFar],
|
||||
});
|
||||
expect(boundary).toMatchObject({ total: 10 * 30 * 22 });
|
||||
});
|
||||
|
||||
it('bulk falls back to the legacy bandless rate when no band holds the km, null when nothing covers it', () => {
|
||||
const bulkNear = rate({ rateUnit: 'PER_TON_KM', rateValue: 30, minKm: 0, maxKm: 30 });
|
||||
const fallback = computeLastMileCharge({
|
||||
freightType: 'BULK',
|
||||
tons: 10,
|
||||
km: 50,
|
||||
containers: [],
|
||||
liveRates: [bulkNear, bulkRate], // bulkRate has no band
|
||||
});
|
||||
expect(fallback).toMatchObject({ total: 10 * 50 * 25 });
|
||||
expect(
|
||||
computeLastMileCharge({
|
||||
freightType: 'BULK',
|
||||
tons: 10,
|
||||
km: 50,
|
||||
containers: [],
|
||||
liveRates: [bulkNear],
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null on mixed currencies, unknown km, and uncovered freight types', () => {
|
||||
const usd40 = rate({ ...band40a, currency: 'USD' });
|
||||
expect(
|
||||
|
||||
@@ -30,10 +30,12 @@ const round2 = (n: number): number => Math.round(n * 100) / 100;
|
||||
/**
|
||||
* Price a last-mile leg off the LIVE rate rules. Pure — pass the live rates in.
|
||||
*
|
||||
* BULK: one PER_TON_KM rate → price = tons × km × rate.
|
||||
* BULK: the PER_TON_KM rate whose distance band holds the km (a legacy
|
||||
* bandless row — NULL minKm — is the fallback and prices every distance) →
|
||||
* price = tons × km × rate.
|
||||
* CONTAINER: per container size, the PER_KM rate whose distance band holds the
|
||||
* km (bands are half-open [minKm, maxKm), NULL maxKm = open-ended) → price =
|
||||
* km × rate × quantity, summed across sizes.
|
||||
* km → price = km × rate × quantity, summed across sizes.
|
||||
* Bands are half-open [minKm, maxKm), NULL maxKm = open-ended.
|
||||
*
|
||||
* Returns null whenever the rules don't fully cover the shipment (no rate, a
|
||||
* container size without a matching band, mixed currencies, km/tons unknown) —
|
||||
@@ -56,7 +58,15 @@ export function computeLastMileCharge(input: {
|
||||
|
||||
if (freightType === 'BULK') {
|
||||
if (!tons || tons <= 0) return null;
|
||||
const rate = candidates.find((r) => r.rateUnit === 'PER_TON_KM');
|
||||
const bulkRates = candidates.filter((r) => r.rateUnit === 'PER_TON_KM');
|
||||
const rate =
|
||||
bulkRates.find(
|
||||
(r) =>
|
||||
r.minKm !== null &&
|
||||
r.minKm !== undefined &&
|
||||
Number(r.minKm) <= km &&
|
||||
(r.maxKm === null || r.maxKm === undefined || km < Number(r.maxKm)),
|
||||
) ?? bulkRates.find((r) => r.minKm === null || r.minKm === undefined);
|
||||
if (!rate) return null;
|
||||
const unitRate = Number(rate.rateValue);
|
||||
const amount = round2(tons * km * unitRate);
|
||||
|
||||
Reference in New Issue
Block a user