mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 21:50:57 +00:00
fix issue and add consolidation
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
sumWagonsRequired,
|
||||
validate20ftContainerRules,
|
||||
validateContainerPlacements,
|
||||
validateWagonCargoExclusivity,
|
||||
} from './wagon-plan.util';
|
||||
|
||||
const nw5: WagonType = {
|
||||
@@ -222,6 +223,85 @@ describe('wagon-plan.util', () => {
|
||||
expect(plan[0]?.slotLoadType).toBe('BULK');
|
||||
expect(buildBulkWagonPlan([bulkBooking], cw3)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('never pools two bulk bookings on one wagon', () => {
|
||||
// 5T + 40T both fit a single 60T CW3 by tonnage — but a wagon with bulk
|
||||
// takes that one load only, so each booking gets its own wagon.
|
||||
const small = {
|
||||
id: 'bulk-5',
|
||||
reference: 'bulk-5',
|
||||
freightType: 'BULK',
|
||||
cargoTotalWeightVgm: 5,
|
||||
bookingContainers: [],
|
||||
} as unknown as Booking;
|
||||
const other = {
|
||||
id: 'bulk-40',
|
||||
reference: 'bulk-40',
|
||||
freightType: 'BULK',
|
||||
cargoTotalWeightVgm: 40,
|
||||
bookingContainers: [],
|
||||
} as unknown as Booking;
|
||||
const plan = buildBulkWagonPlan([small, other], cw3);
|
||||
expect(plan).toHaveLength(2);
|
||||
for (const slot of plan) {
|
||||
expect(slot.allocations).toHaveLength(1);
|
||||
}
|
||||
expect(plan[0]?.allocations[0]?.bookingId).toBe('bulk-5');
|
||||
expect(plan[1]?.allocations[0]?.bookingId).toBe('bulk-40');
|
||||
expect(validateWagonCargoExclusivity(plan)).toEqual([]);
|
||||
});
|
||||
|
||||
it('a multi-wagon bulk booking still spreads over its own wagons', () => {
|
||||
const big = {
|
||||
id: 'bulk-130',
|
||||
reference: 'bulk-130',
|
||||
freightType: 'BULK',
|
||||
cargoTotalWeightVgm: 130,
|
||||
bookingContainers: [],
|
||||
} as unknown as Booking;
|
||||
const plan = buildBulkWagonPlan([big], cw3);
|
||||
expect(plan).toHaveLength(3);
|
||||
expect(plan.map((s) => s.allocations[0]?.allocatedWeightTons)).toEqual([60, 60, 10]);
|
||||
});
|
||||
|
||||
it('flags a wagon mixing bulk with anything else', () => {
|
||||
const bulkAlloc = {
|
||||
bookingId: 'b',
|
||||
bookingReference: 'b',
|
||||
allocatedWeightTons: 5,
|
||||
loadType: AllocationLoadType.Bulk,
|
||||
};
|
||||
const containerAlloc = {
|
||||
bookingId: 'c',
|
||||
bookingReference: 'c',
|
||||
allocatedWeightTons: 25,
|
||||
loadType: AllocationLoadType.Container,
|
||||
};
|
||||
const slot = (allocations: (typeof bulkAlloc)[]) => ({
|
||||
sequenceNo: 1,
|
||||
wagonTypeId: cw3.id,
|
||||
wagonTypeCode: cw3.code,
|
||||
capacityTons: 60,
|
||||
lengthMeters: 14,
|
||||
tareWeightTons: 24,
|
||||
assignedWeightTons: 0,
|
||||
allocations,
|
||||
});
|
||||
// bulk + container on one wagon
|
||||
expect(validateWagonCargoExclusivity([slot([bulkAlloc, containerAlloc])]))
|
||||
.toHaveLength(1);
|
||||
// bulk + bulk on one wagon
|
||||
expect(
|
||||
validateWagonCargoExclusivity([slot([bulkAlloc, { ...bulkAlloc, bookingId: 'b2' }])]),
|
||||
).toHaveLength(1);
|
||||
// bulk alone, and containers sharing, are fine
|
||||
expect(validateWagonCargoExclusivity([slot([bulkAlloc])])).toEqual([]);
|
||||
expect(
|
||||
validateWagonCargoExclusivity([
|
||||
slot([containerAlloc, { ...containerAlloc, bookingId: 'c2' }]),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('containerWagonsForLines — TEU-aware, ceil booking total once', () => {
|
||||
|
||||
@@ -200,16 +200,14 @@ export function buildBulkWagonPlan(
|
||||
);
|
||||
const cappedTonSlots = cappedTonSlotsByBooking.reduce((sum, n) => sum + n, 0);
|
||||
|
||||
const totalWeight = roundTons(
|
||||
bookings.reduce(
|
||||
(sum, b, i) =>
|
||||
itemSlotsByBooking[i] > 0 || cappedTonSlotsByBooking[i] > 0
|
||||
? sum
|
||||
: sum + Number(b.cargoTotalWeightVgm ?? 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
const tonSlots = totalWeight > 0 ? Math.ceil(totalWeight / capacity) : 0;
|
||||
// One bulk booking per wagon — bookings never pool tonnage on a shared
|
||||
// wagon, so each uncapped booking sizes its own wagons (ceil per booking,
|
||||
// not over the pooled total).
|
||||
const tonSlots = bookings.reduce((sum, b, i) => {
|
||||
if (itemSlotsByBooking[i] > 0 || cappedTonSlotsByBooking[i] > 0) return sum;
|
||||
const weight = roundTons(Number(b.cargoTotalWeightVgm ?? 0));
|
||||
return weight > 0 ? sum + Math.ceil(weight / capacity) : sum;
|
||||
}, 0);
|
||||
const slots = Math.max(1, tonSlots + itemSlots + cappedTonSlots);
|
||||
|
||||
const basePlan: WagonPlanSlot[] = Array.from({ length: slots }, (_, index) => ({
|
||||
@@ -374,13 +372,12 @@ function allocateBookingsToSlots(
|
||||
|
||||
if (booking.remainingWeightTons <= 0) {
|
||||
bookingIndex += 1;
|
||||
} else if (allocatedWeightTons >= takeCap) {
|
||||
// The cap stopped this wagon short of its rating and the booking has
|
||||
// more to load. The leftover room is NOT free: `buildBulkWagonPlan`
|
||||
// already reserved a wagon for the rest, so backfilling another booking
|
||||
// here would double-book the consist. Close the wagon.
|
||||
break;
|
||||
}
|
||||
// One bulk booking per wagon: a wagon carrying bulk takes nothing else —
|
||||
// never a second booking's cargo. `buildBulkWagonPlan` sized the slots
|
||||
// per booking, so leftover room on this wagon is not free capacity.
|
||||
// Close the wagon after its single allocation.
|
||||
break;
|
||||
}
|
||||
|
||||
return { ...slot, assignedWeightTons, allocations };
|
||||
@@ -504,6 +501,26 @@ export function sumWagonsRequired(booking: Booking, wagonPlan?: WagonPlanSlot[])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One wagon carries one kind of cargo: a slot with a BULK allocation holds
|
||||
* nothing else — no container beside it and no second bulk booking. Container
|
||||
* allocations may still share a wagon with each other (TEU rules apply).
|
||||
*/
|
||||
export function validateWagonCargoExclusivity(wagonPlan: WagonPlanSlot[]): string[] {
|
||||
const violations: string[] = [];
|
||||
for (const slot of wagonPlan) {
|
||||
const hasBulk = slot.allocations.some(
|
||||
(a) => a.loadType === AllocationLoadType.Bulk,
|
||||
);
|
||||
if (hasBulk && slot.allocations.length > 1) {
|
||||
violations.push(
|
||||
`Wagon #${slot.sequenceNo} mixes bulk with other cargo — a wagon carrying bulk takes that one load only`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function validateBulkWagonSlotWeights(wagonPlan: WagonPlanSlot[]): string[] {
|
||||
const violations: string[] = [];
|
||||
for (const slot of wagonPlan.filter((s) => s.slotLoadType === 'BULK')) {
|
||||
@@ -547,6 +564,7 @@ export function validateTrainLimits(
|
||||
);
|
||||
|
||||
violations.push(...validateBulkWagonSlotWeights(wagonPlan));
|
||||
violations.push(...validateWagonCargoExclusivity(wagonPlan));
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user