feat: add per-ton cargo loading limits and update related services

This commit is contained in:
Marshal
2026-08-03 19:16:28 +00:00
parent 9afc281d21
commit 488c2465be
14 changed files with 394 additions and 21 deletions

View File

@@ -4,6 +4,10 @@ import {
bookingTrainLengthMeters,
bulkItemWagonsForAllowedTypes,
bulkItemWagonsRequired,
bulkTonsPerWagon,
bulkTonWagonsForAllowedTypes,
bulkTonWagonsRequired,
bulkWagonsForAllowedTypes,
consistUsage,
consistViolations,
deriveTrainCapacityFromLocomotive,
@@ -135,6 +139,61 @@ describe('train-capacity.util', () => {
});
});
describe('bulkTonsPerWagon / bulkTonWagonsRequired (PER_TON loading cap)', () => {
// Sugar is loaded 50T per wagon even on a 70T wagon.
const sugar = { wagonTypes: [{ id: 'nw5', capacityTons: 70 }], tonsPerWagonMap: { nw5: 50 } };
const bulk = (tons: number) => ({ freightType: 'BULK', cargoTotalWeightVgm: tons });
it('uses the configured cap instead of the rated capacity', () => {
expect(bulkTonsPerWagon(sugar, 'nw5', 70)).toBe(50);
});
it('falls back to rated capacity when the cargo type caps nothing', () => {
expect(bulkTonsPerWagon(null, 'nw5', 70)).toBe(70);
expect(bulkTonsPerWagon({ wagonTypes: [] }, 'nw5', 70)).toBe(70);
expect(bulkTonsPerWagon({ tonsPerWagonMap: { other: 50 } }, 'nw5', 70)).toBe(70);
});
it('clamps a stale cap that now exceeds the rating (wagon type edited down)', () => {
// Saved when NW5 was rated 70T; the type was later re-rated to 45T.
expect(bulkTonsPerWagon(sugar, 'nw5', 45)).toBe(45);
});
it('sizes 200T of capped sugar at 4 wagons, not the 3 raw capacity implies', () => {
expect(bulkTonWagonsRequired(bulk(200), sugar, 'nw5', 70)).toBe(4);
// Same booking, no cap → the old 3-wagon answer.
expect(bulkTonWagonsRequired(bulk(200), null, 'nw5', 70)).toBe(3);
});
it('picks the fewest-wagon allowed type, each on its own cap', () => {
const cargoType = {
wagonTypes: [
{ id: 'nw5', capacityTons: 70 },
{ id: 'nw7', capacityTons: 80 },
],
tonsPerWagonMap: { nw5: 50 },
};
// NW5 capped 50 → 4 wagons; NW7 uncapped 80 → 3 wagons. Best = 3.
expect(bulkTonWagonsForAllowedTypes(bulk(200), cargoType, 70)).toBe(3);
});
it('routes PER_ITEM and PER_TON through one call', () => {
expect(bulkWagonsForAllowedTypes(bulk(200), sugar, 70)).toBe(4);
// PER_ITEM still wins where an item count is present.
const cars = {
wagonTypes: [{ id: 'nw5', capacityTons: 70 }],
itemsPerWagonMap: { nw5: 4 },
};
expect(
bulkWagonsForAllowedTypes(
{ freightType: 'BULK', cargoTotalWeightVgm: 50, bulkTotalWeightTons: 1000 },
cars,
70,
),
).toBe(17);
});
});
describe('bookingCargoTons (break-bulk weight preference)', () => {
it('prefers bulkTotalWeightTons over the item-count VGM column', () => {
expect(