mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 15:25:45 +00:00
feat(train-scheduling): mid-route consist changes, audit history, safer workspace
- planned couples: loose wagons join the train at a route stop, added from the schedule yards tab; capacity credits them per corridor edge and coupling validates locomotive weight/length caps per leg - real-cut toggle: a cut wagon permanently leaves the train build at its cut yard (soft cut still sits out one trip only) - fix heaviest-leg display counting a shared slot's full cargo on every spanned edge (phantom pull-weight overload on S-2026-00045) - confirmation dialogs for workspace add/load/unload/remove actions - train-builder History and Detached-wagons tabs, backed by paginated endpoints; builder detaches now always write adjustment-log rows Migrations 3660 (planned_wagon_couples, planned_wagon_real_cuts) and 3670 (adjustment log train_schedule_id nullable) — both applied to the dev DB by hand; watch mode does not run migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
sumWagonsRequired,
|
||||
validate20ftContainerRules,
|
||||
validateContainerPlacements,
|
||||
validateMixedTrainLimitsPerEdge,
|
||||
validateWagonCargoExclusivity,
|
||||
} from './wagon-plan.util';
|
||||
|
||||
@@ -412,4 +413,91 @@ describe('maxEdgeConsistUsage — the binding edge, not the whole-route sum', ()
|
||||
loadedWagonCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('with a legs map, shared-slot cargo weighs only its own edges (the S-2026-00045 shape)', () => {
|
||||
// One wagon reused across legs: booking X rides a→b (40T), booking Y
|
||||
// boards at b with 30T. The slot spans the whole route, but edge a→b
|
||||
// must weigh 24 + 40 = 64T — not 24 + 70. Tare rides both edges.
|
||||
const shared = {
|
||||
tareWeightTons: 24,
|
||||
assignedWeightTons: 70,
|
||||
lengthMeters: 14,
|
||||
boardYardId: null,
|
||||
alightYardId: null,
|
||||
allocations: [
|
||||
{ bookingId: 'X', allocatedWeightTons: 40 },
|
||||
{ bookingId: 'Y', allocatedWeightTons: 30 },
|
||||
],
|
||||
} as never;
|
||||
const legs = new Map([
|
||||
['X', { from: 0, to: 1 }],
|
||||
['Y', { from: 1, to: 2 }],
|
||||
]);
|
||||
// Without legs: whole-span scalar on both edges (94T binding edge).
|
||||
expect(maxEdgeConsistUsage([shared], stops).grossWeightTons).toBe(94);
|
||||
// With legs: heaviest edge is a→b at 64T (b→c is 54T).
|
||||
expect(maxEdgeConsistUsage([shared], stops, legs).grossWeightTons).toBe(64);
|
||||
});
|
||||
|
||||
it('falls back to the whole-span scalar when an allocation has no readable weight', () => {
|
||||
const shared = {
|
||||
tareWeightTons: 24,
|
||||
assignedWeightTons: 70,
|
||||
lengthMeters: 14,
|
||||
boardYardId: null,
|
||||
alightYardId: null,
|
||||
allocations: [{ bookingId: 'X' }],
|
||||
} as never;
|
||||
const legs = new Map([['X', { from: 0, to: 1 }]]);
|
||||
expect(maxEdgeConsistUsage([shared], stops, legs).grossWeightTons).toBe(94);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMixedTrainLimitsPerEdge — leg-aware cargo weighing', () => {
|
||||
it('does not flag a leg whose overweight is only later-boarding cargo (S-2026-00045)', () => {
|
||||
// 2 shared wagons, 100T cap. Booking X rides a→b with 30T/wagon, booking Y
|
||||
// boards at b with 25T/wagon. Whole-span scalars read every edge as
|
||||
// 2×(20 + 55) = 150T > 100T; the cargo actually aboard is 100T (a→b) and
|
||||
// 90T (b→c) — both fit.
|
||||
const slot = (seq: number) => ({
|
||||
sequenceNo: seq,
|
||||
wagonTypeId: 'wt-nw5',
|
||||
wagonTypeCode: 'NW5',
|
||||
capacityTons: 70,
|
||||
lengthMeters: 14,
|
||||
tareWeightTons: 20,
|
||||
assignedWeightTons: 55,
|
||||
boardYardId: null,
|
||||
alightYardId: null,
|
||||
allocations: [
|
||||
{
|
||||
bookingId: 'X',
|
||||
bookingReference: 'X',
|
||||
allocatedWeightTons: 30,
|
||||
loadType: AllocationLoadType.Container,
|
||||
},
|
||||
{
|
||||
bookingId: 'Y',
|
||||
bookingReference: 'Y',
|
||||
allocatedWeightTons: 25,
|
||||
loadType: AllocationLoadType.Container,
|
||||
},
|
||||
],
|
||||
});
|
||||
const legs = new Map([
|
||||
['X', { from: 0, to: 1 }],
|
||||
['Y', { from: 1, to: 2 }],
|
||||
]);
|
||||
const run = (withLegs?: typeof legs) =>
|
||||
validateMixedTrainLimitsPerEdge(
|
||||
[slot(1), slot(2)] as never,
|
||||
[{ lengthMeters: 14 }],
|
||||
{ maxWeightTons: 100 },
|
||||
['a', 'b', 'c'],
|
||||
undefined,
|
||||
withLegs,
|
||||
);
|
||||
expect(run()).toHaveLength(2); // both edges falsely overweight without legs
|
||||
expect(run(legs)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user