Files
edr-platform/apps/edr-freight-api/src/common/last-mile-charge.util.spec.ts
Hagernesh 5c8c68e990 feat(rates): last-mile rate rules with bulk per-ton-km and container distance bands
- rates: min_km/max_km columns, PER_TON_KM unit, ETB|USD currency for last mile
- extend UQ_rates_pattern with band start; overlap + shape validation
- shared last-mile charge resolver; approve-dialog price estimate endpoint
- delivery-fee invoice prices via rules, falls back to vehicle price/km
- backoffice: last-mile rate form (mode, container type, band, currency)
2026-08-05 22:13:42 +00:00

122 lines
3.2 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('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();
});
});