Files
edr-platform/apps/edr-freight-api/src/common/last-mile-charge.util.spec.ts
Hagernesh 95fad6f095 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.
2026-08-06 08:56:35 +00:00

164 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { computeLastMileCharge } from './last-mile-charge.util';
import type { Rate } from '../modules/rule-engine/entities/rate.entity';
const rate = (over: Partial<Rate>): Rate =>
({
appliesTo: 'LAST_MILE',
status: 'LIVE',
currency: 'ETB',
trigger: 'ALWAYS',
rateType: 'LAST_MILE',
...over,
}) as Rate;
const band20a = rate({
rateUnit: 'PER_KM',
rateValue: 1800,
minKm: 0,
maxKm: 30,
containerType: { sizeFt: 20 } as Rate['containerType'],
});
const band20b = rate({
rateUnit: 'PER_KM',
rateValue: 1500,
minKm: 30,
maxKm: null,
containerType: { sizeFt: 20 } as Rate['containerType'],
});
const band40a = rate({
rateUnit: 'PER_KM',
rateValue: 2200,
minKm: 0,
maxKm: 30,
containerType: { sizeFt: 40 } as Rate['containerType'],
});
const bulkRate = rate({ rateUnit: 'PER_TON_KM', rateValue: 25 });
describe('computeLastMileCharge', () => {
it('prices containers per band × size × quantity', () => {
const charge = computeLastMileCharge({
freightType: 'CONTAINER',
tons: 0,
km: 13,
containers: [
{ sizeLabel: '20DC', qty: 5 },
{ sizeLabel: '40HC', qty: 1 },
],
liveRates: [band20a, band20b, band40a],
});
// 13 × 1800 × 5 + 13 × 2200 × 1
expect(charge).toMatchObject({ mode: 'CONTAINER', total: 117000 + 28600, currency: 'ETB' });
expect(charge!.lines).toHaveLength(2);
});
it('band boundary is half-open: km = 30 falls in the 30+ band', () => {
const charge = computeLastMileCharge({
freightType: 'CONTAINER',
tons: 0,
km: 30,
containers: [{ sizeLabel: '20DC', qty: 1 }],
liveRates: [band20a, band20b],
});
expect(charge!.lines[0].unitRate).toBe(1500);
expect(charge!.total).toBe(30 * 1500);
});
it('returns null when a size has no matching band', () => {
const charge = computeLastMileCharge({
freightType: 'CONTAINER',
tons: 0,
km: 50,
containers: [{ sizeLabel: '40HC', qty: 2 }],
liveRates: [band40a], // 40ft only covers 030
});
expect(charge).toBeNull();
});
it('prices bulk as tons × km × rate', () => {
const charge = computeLastMileCharge({
freightType: 'BULK',
tons: 60,
km: 26,
containers: [],
liveRates: [bulkRate],
});
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(
computeLastMileCharge({
freightType: 'CONTAINER',
tons: 0,
km: 10,
containers: [
{ sizeLabel: '20DC', qty: 1 },
{ sizeLabel: '40HC', qty: 1 },
],
liveRates: [band20a, usd40],
}),
).toBeNull();
expect(
computeLastMileCharge({
freightType: 'BULK',
tons: 10,
km: 0,
containers: [],
liveRates: [bulkRate],
}),
).toBeNull();
expect(
computeLastMileCharge({
freightType: 'BREAK_BULK',
tons: 10,
km: 10,
containers: [],
liveRates: [bulkRate],
}),
).toBeNull();
});
});