mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
feat(billing): USD offline bank-transfer payments
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { Booking } from '../../bookings/entities/booking.entity';
|
||||
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||
import {
|
||||
computeFleetAvailability,
|
||||
selectBookingsWithinFleetCap,
|
||||
sortBookingsForScheduling,
|
||||
summarizeFleetWarnings,
|
||||
wagonsRequiredForBooking,
|
||||
} from './fleet-plan.util';
|
||||
import { buildContainerWagonPlan, type WagonPlanSlot } from './wagon-plan.util';
|
||||
|
||||
const nw5: WagonType = {
|
||||
id: 'wt-nw5',
|
||||
code: 'NW5',
|
||||
name: 'Flat Wagon',
|
||||
capacityTons: 70,
|
||||
lengthMeters: 14,
|
||||
supportedLoadTypes: ['CONTAINER'],
|
||||
isActive: true,
|
||||
supportsContainer: true,
|
||||
} as WagonType;
|
||||
|
||||
const makeBooking = (
|
||||
id: string,
|
||||
extra: Partial<Booking> = {},
|
||||
): Booking =>
|
||||
({
|
||||
id,
|
||||
reference: id,
|
||||
freightType: 'CONTAINER',
|
||||
isGovernment: false,
|
||||
priorityScore: 0,
|
||||
scheduledDate: new Date('2026-06-20T08:00:00.000Z'),
|
||||
cargoTotalWeightVgm: 50,
|
||||
bookingContainers: [{ id: `${id}-line`, quantity: 2, wagonsRequired: 1, vgmPerUnitTons: 25 }],
|
||||
...extra,
|
||||
}) as Booking;
|
||||
|
||||
describe('fleet-plan.util', () => {
|
||||
it('sorts bookings government first, then priority, then date', () => {
|
||||
const bookings = [
|
||||
makeBooking('late', { scheduledDate: new Date('2026-06-22T08:00:00.000Z') }),
|
||||
makeBooking('gov', { isGovernment: true, priorityScore: 0 }),
|
||||
makeBooking('prio', { priorityScore: 10 }),
|
||||
];
|
||||
|
||||
const sorted = sortBookingsForScheduling(bookings);
|
||||
expect(sorted.map((b) => b.id)).toEqual(['gov', 'prio', 'late']);
|
||||
});
|
||||
|
||||
it('computes fleet availability with shortfall', () => {
|
||||
const plan: WagonPlanSlot[] = buildContainerWagonPlan(
|
||||
[
|
||||
makeBooking('b1', {
|
||||
bookingContainers: [
|
||||
{ id: 'b1-line', quantity: 4, wagonsRequired: 2, vgmPerUnitTons: 25 } as never,
|
||||
],
|
||||
}),
|
||||
],
|
||||
nw5,
|
||||
);
|
||||
const fleetByTypeId = new Map([[nw5.id, 1]]);
|
||||
|
||||
const rows = computeFleetAvailability(plan, fleetByTypeId, new Map([[nw5.id, 'NW5']]));
|
||||
const nw5Row = rows.find((r) => r.wagonTypeCode === 'NW5');
|
||||
|
||||
expect(nw5Row?.needed).toBe(2);
|
||||
expect(nw5Row?.available).toBe(1);
|
||||
expect(nw5Row?.shortfall).toBe(1);
|
||||
});
|
||||
|
||||
it('defers lower-priority bookings when fleet is insufficient', () => {
|
||||
const high = makeBooking('high', {
|
||||
priorityScore: 100,
|
||||
bookingContainers: [
|
||||
{ id: 'high-line', quantity: 2, wagonsRequired: 2, vgmPerUnitTons: 25 } as never,
|
||||
],
|
||||
});
|
||||
const low = makeBooking('low', {
|
||||
priorityScore: 1,
|
||||
bookingContainers: [
|
||||
{ id: 'low-line', quantity: 2, wagonsRequired: 2, vgmPerUnitTons: 25 } as never,
|
||||
],
|
||||
});
|
||||
const fleet = new Map([[nw5.id, 2]]);
|
||||
|
||||
const { fitting, deferred } = selectBookingsWithinFleetCap(
|
||||
[low, high],
|
||||
fleet,
|
||||
() => nw5.id,
|
||||
);
|
||||
|
||||
expect(fitting.map((b) => b.id)).toEqual(['high']);
|
||||
expect(deferred).toHaveLength(1);
|
||||
expect(deferred[0]?.id).toBe('low');
|
||||
expect(deferred[0]?.reason).toContain('2');
|
||||
});
|
||||
|
||||
it('summarizes fleet shortage warnings', () => {
|
||||
const warnings = summarizeFleetWarnings(
|
||||
[
|
||||
{
|
||||
wagonTypeId: nw5.id,
|
||||
wagonTypeCode: 'NW5',
|
||||
needed: 5,
|
||||
available: 2,
|
||||
shortfall: 3,
|
||||
},
|
||||
],
|
||||
[{ id: 'b1', reference: 'BKG-1', reason: 'No wagons' }],
|
||||
);
|
||||
|
||||
expect(warnings.some((w) => w.includes('Fleet shortage'))).toBe(true);
|
||||
expect(warnings.some((w) => w.includes('deferred'))).toBe(true);
|
||||
});
|
||||
|
||||
it('names the booking and its per-type shortfall when the deferral carries a shortage', () => {
|
||||
const warnings = summarizeFleetWarnings(
|
||||
[],
|
||||
[
|
||||
{
|
||||
id: 'b1',
|
||||
reference: 'BKG-1',
|
||||
reason: 'No available NW6 wagon at the yard',
|
||||
shortage: {
|
||||
wagonTypeCodes: 'NW6',
|
||||
wagonsNeeded: 2,
|
||||
wagonsAvailable: 1,
|
||||
wagonsShort: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
warnings.some(
|
||||
(w) =>
|
||||
w.includes('BKG-1') &&
|
||||
w.includes('2 × NW6') &&
|
||||
w.includes('only 1 available') &&
|
||||
w.includes('short 1'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('counts wagons required per booking from container lines', () => {
|
||||
const booking = makeBooking('b1', {
|
||||
bookingContainers: [
|
||||
{ id: 'b1-line-0', quantity: 2, wagonsRequired: 1, vgmPerUnitTons: 25 } as never,
|
||||
{ id: 'b1-line-1', quantity: 1, wagonsRequired: 1, vgmPerUnitTons: 25 } as never,
|
||||
],
|
||||
});
|
||||
expect(wagonsRequiredForBooking(booking)).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user