mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
162 lines
6.6 KiB
TypeScript
162 lines
6.6 KiB
TypeScript
import { WagonStockLedger } from './wagon-stock-ledger.util';
|
|
|
|
const WHOLE = { fromEdge: 0, toEdge: 1 };
|
|
|
|
describe('WagonStockLedger', () => {
|
|
it('reports the wagons of a booking\'s OWN types, not the train total', () => {
|
|
// The reported case: 20 free wagons on the train, but only 16 of them NW5.
|
|
const ledger = new WagonStockLedger(
|
|
new Map([
|
|
['nw5', 16],
|
|
['pw2', 4],
|
|
]),
|
|
1,
|
|
);
|
|
expect(ledger.availableFor(['nw5'], WHOLE)).toBe(16);
|
|
expect(ledger.availableFor(['pw2'], WHOLE)).toBe(4);
|
|
// A cargo type mapped to both may ride either, so they add up.
|
|
expect(ledger.availableFor(['nw5', 'pw2'], WHOLE)).toBe(20);
|
|
// Duplicates must not double-count.
|
|
expect(ledger.availableFor(['nw5', 'nw5'], WHOLE)).toBe(16);
|
|
// An unconfigured type has no stock.
|
|
expect(ledger.availableFor(['unknown'], WHOLE)).toBe(0);
|
|
});
|
|
|
|
it('consumes what it can and reports the shortfall', () => {
|
|
const ledger = new WagonStockLedger(new Map([['nw5', 16]]), 1);
|
|
// A 20-wagon booking can only take 16 — the caller splits on that number.
|
|
expect(ledger.consume(['nw5'], 20, WHOLE)).toBe(16);
|
|
expect(ledger.availableFor(['nw5'], WHOLE)).toBe(0);
|
|
expect(ledger.consume(['nw5'], 1, WHOLE)).toBe(0);
|
|
});
|
|
|
|
it('drains the deepest stock first across candidate types', () => {
|
|
const ledger = new WagonStockLedger(
|
|
new Map([
|
|
['nw5', 10],
|
|
['nw7', 3],
|
|
]),
|
|
1,
|
|
);
|
|
expect(ledger.consume(['nw5', 'nw7'], 12, WHOLE)).toBe(12);
|
|
// 10 from NW5 then 2 from NW7 — one NW7 left.
|
|
expect(ledger.availableFor(['nw7'], WHOLE)).toBe(1);
|
|
expect(ledger.availableFor(['nw5'], WHOLE)).toBe(0);
|
|
});
|
|
|
|
it('frees stock past an alight yard — disjoint legs never compete', () => {
|
|
// Three stops (A→B→C) = two edges. An intercity booking riding A→B must
|
|
// not consume the wagon on B→C.
|
|
const ledger = new WagonStockLedger(new Map([['nw5', 5]]), 2);
|
|
const firstLeg = { fromEdge: 0, toEdge: 1 };
|
|
const secondLeg = { fromEdge: 1, toEdge: 2 };
|
|
|
|
ledger.consume(['nw5'], 5, firstLeg);
|
|
expect(ledger.availableFor(['nw5'], firstLeg)).toBe(0);
|
|
expect(ledger.availableFor(['nw5'], secondLeg)).toBe(5);
|
|
|
|
// A whole-route booking sees the busiest edge it crosses, so it is blocked.
|
|
expect(ledger.availableFor(['nw5'], { fromEdge: 0, toEdge: 2 })).toBe(0);
|
|
});
|
|
|
|
it('counts the busiest edge within a leg, not the sum of edges', () => {
|
|
const ledger = new WagonStockLedger(new Map([['nw5', 10]]), 3);
|
|
ledger.consume(['nw5'], 4, { fromEdge: 0, toEdge: 1 });
|
|
ledger.consume(['nw5'], 6, { fromEdge: 1, toEdge: 2 });
|
|
// Edge 0 uses 4, edge 1 uses 6 — a booking over both needs 10 free at once.
|
|
expect(ledger.availableFor(['nw5'], { fromEdge: 0, toEdge: 2 })).toBe(4);
|
|
expect(ledger.availableFor(['nw5'], { fromEdge: 2, toEdge: 3 })).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('WagonStockLedger — multi-yard consist', () => {
|
|
// The reported case: a built train of 53 wagons, 20 standing in Dire and 33
|
|
// in Mojo. Each yard may only sell the wagons physically standing there.
|
|
const DIRE = 'yard-dire';
|
|
const MOJO = 'yard-mojo';
|
|
const ADDIS = 'yard-addis';
|
|
const STOPS = [DIRE, MOJO, ADDIS];
|
|
const EDGES = STOPS.length - 1;
|
|
const splitStock = () =>
|
|
new Map([
|
|
[DIRE, new Map([['nw5', 20]])],
|
|
[MOJO, new Map([['nw5', 33]])],
|
|
]);
|
|
// Legs along Dire → Mojo → Addis.
|
|
const DIRE_TO_ADDIS = { fromEdge: 0, toEdge: 2 };
|
|
const MOJO_TO_ADDIS = { fromEdge: 1, toEdge: 2 };
|
|
|
|
const splitLedger = () =>
|
|
new WagonStockLedger(new Map([['nw5', 53]]), EDGES, splitStock(), STOPS);
|
|
|
|
it('offers each yard only the wagons standing there', () => {
|
|
const ledger = splitLedger();
|
|
expect(ledger.availableFor(['nw5'], DIRE_TO_ADDIS)).toBe(20);
|
|
expect(ledger.availableFor(['nw5'], MOJO_TO_ADDIS)).toBe(33);
|
|
});
|
|
|
|
it('keeps the yards independent — Dire bookings never eat Mojo stock', () => {
|
|
const ledger = splitLedger();
|
|
// A Dire booking rides the whole corridor, occupying the Mojo→Addis edge…
|
|
expect(ledger.consume(['nw5'], 20, DIRE_TO_ADDIS)).toBe(20);
|
|
expect(ledger.availableFor(['nw5'], DIRE_TO_ADDIS)).toBe(0);
|
|
// …but those are Dire's steel, so Mojo still has its own 33 to sell.
|
|
expect(ledger.availableFor(['nw5'], MOJO_TO_ADDIS)).toBe(33);
|
|
expect(ledger.consume(['nw5'], 33, MOJO_TO_ADDIS)).toBe(33);
|
|
expect(ledger.availableFor(['nw5'], MOJO_TO_ADDIS)).toBe(0);
|
|
});
|
|
|
|
it('never lends a free Dire wagon to a Mojo customer', () => {
|
|
const ledger = splitLedger();
|
|
// Only 5 of Dire's 20 sell; the other 15 ride past Mojo empty.
|
|
expect(ledger.consume(['nw5'], 5, DIRE_TO_ADDIS)).toBe(5);
|
|
// Mojo is still capped at its own 33 — the 15 empty Dire wagons are not
|
|
// offered here, exactly as the operator requires.
|
|
expect(ledger.availableFor(['nw5'], MOJO_TO_ADDIS)).toBe(33);
|
|
expect(ledger.consume(['nw5'], 40, MOJO_TO_ADDIS)).toBe(33);
|
|
});
|
|
|
|
it('offers nothing at the destination — there is nothing to pick up there', () => {
|
|
const ledger = splitLedger();
|
|
// A leg boarding at the last stop has no pool of its own.
|
|
expect(ledger.availableFor(['nw5'], { fromEdge: 2, toEdge: 2 })).toBe(0);
|
|
});
|
|
|
|
it('second example: Addis → Dire → Indode → Mojo → Djibouti', () => {
|
|
const [ADD, DIRE_2, INDODE, MOJO_2, DJIBOUTI] = [
|
|
'yard-add',
|
|
'yard-dire',
|
|
'yard-indode',
|
|
'yard-mojo',
|
|
'yard-djibouti',
|
|
];
|
|
const stops = [ADD, DIRE_2, INDODE, MOJO_2, DJIBOUTI];
|
|
const ledger = new WagonStockLedger(
|
|
new Map([['nw5', 53]]),
|
|
stops.length - 1,
|
|
new Map([
|
|
[DIRE_2, new Map([['nw5', 20]])],
|
|
[MOJO_2, new Map([['nw5', 33]])],
|
|
]),
|
|
stops,
|
|
);
|
|
const to = (fromEdge: number) => ({ fromEdge, toEdge: stops.length - 1 });
|
|
// Addis: the train starts empty — nothing to sell.
|
|
expect(ledger.availableFor(['nw5'], to(0))).toBe(0);
|
|
// Dire: the 20 wagons waiting there.
|
|
expect(ledger.availableFor(['nw5'], to(1))).toBe(20);
|
|
// Indode: the same 20 wagons, which have moved with the train.
|
|
expect(ledger.availableFor(['nw5'], to(2))).toBe(0);
|
|
// Mojo: its own 33 only.
|
|
expect(ledger.availableFor(['nw5'], to(3))).toBe(33);
|
|
});
|
|
|
|
it('single-yard consist keeps the original whole-train behaviour', () => {
|
|
// No byYardId (the train is not split) — every leg sees the whole train,
|
|
// exactly as before this feature.
|
|
const ledger = new WagonStockLedger(new Map([['nw5', 53]]), EDGES);
|
|
expect(ledger.availableFor(['nw5'], DIRE_TO_ADDIS)).toBe(53);
|
|
expect(ledger.availableFor(['nw5'], MOJO_TO_ADDIS)).toBe(53);
|
|
});
|
|
});
|