import { autoFillPlacements, findMissingContainerNumberIssues, occupiedTeuPerEdgeBySlot, type ContainerUnitForPlacement, } from './container-placement.util'; describe('container-placement.util', () => { const units: ContainerUnitForPlacement[] = [ { bookingId: 'b1', bookingContainerId: 'c1', unitIndex: 0, label: 'REF · 1/1 · 20GP', teuSlots: 1, sizeFt: 20, containerNumber: 'ABCD1234567', }, { bookingId: 'b2', bookingContainerId: 'c2', unitIndex: 0, label: 'REF2 · 1/1 · 40GP', teuSlots: 2, sizeFt: 40, containerNumber: null, }, ]; it('auto-fills placements across slots', () => { const { placements, overflow } = autoFillPlacements(units, [1, 2]); expect(placements).toHaveLength(2); expect(overflow).toHaveLength(0); expect(placements[0].sequenceNo).toBe(1); expect(placements[1].sequenceNo).toBe(2); }); it('reports missing container numbers only when placement is empty', () => { const { placements } = autoFillPlacements(units, [1, 2]); const issues = findMissingContainerNumberIssues(units, placements); expect(issues).toHaveLength(0); expect(placements[1].containerNumber).toMatch(/^TBD-/); }); it('generates TBD placeholder for missing container numbers', () => { const single: ContainerUnitForPlacement[] = [ { bookingId: 'b2', bookingReference: 'BK-2026-000033', bookingContainerId: 'c2', unitIndex: 0, label: 'REF2 · 1/1 · 40GP', teuSlots: 2, sizeFt: 40, containerNumber: null, }, ]; const { placements } = autoFillPlacements(single, [1]); expect(placements[0].containerNumber).toBe('TBD-BK-2026-000033-1'); }); const ft40 = ( bookingId: string, i: number, leg?: { from: number; to: number }, ): ContainerUnitForPlacement => ({ bookingId, bookingReference: bookingId, bookingContainerId: `${bookingId}-line`, unitIndex: i, label: `${bookingId} · ${i + 1} · 40GP`, teuSlots: 2, sizeFt: 40, containerNumber: `CNT${bookingId}${i}`, leg, }); it('never clamps overflow onto the last slot — returns it instead', () => { // 3 × 40ft, 2 slots. The old walk piled unit 3 onto slot #2 and let the // validator reject it once per container ("Wagon #42…", the reported bug). const three = [ft40('A', 0), ft40('A', 1), ft40('A', 2)]; const { placements, overflow } = autoFillPlacements(three, [1, 2]); expect(placements).toHaveLength(2); expect(overflow).toHaveLength(1); expect(placements.every((p) => p.sequenceNo === 1 || p.sequenceNo === 2)).toBe(true); }); it('leg-aware: disjoint-leg 40fts share one wagon (the staging case)', () => { // 2 slots riding the whole 2-edge route. Leg-blind fill fits only two of // these four 40fts; per-edge TEU fits all four — two per wagon, one per leg. const slots = [ { sequenceNo: 1, from: 0, to: 2 }, { sequenceNo: 2, from: 0, to: 2 }, ]; const four = [ ft40('LEG1', 0, { from: 0, to: 1 }), ft40('LEG1', 1, { from: 0, to: 1 }), ft40('LEG2', 0, { from: 1, to: 2 }), ft40('LEG2', 1, { from: 1, to: 2 }), ]; const { placements, overflow } = autoFillPlacements(four, slots, new Map(), 2); expect(overflow).toHaveLength(0); expect(placements).toHaveLength(4); }); it('same-leg 40fts still never share a wagon', () => { const slots = [{ sequenceNo: 1, from: 0, to: 2 }]; const two = [ft40('X', 0, { from: 0, to: 1 }), ft40('X', 1, { from: 0, to: 1 })]; const { placements, overflow } = autoFillPlacements(two, slots, new Map(), 2); expect(placements).toHaveLength(1); expect(overflow).toHaveLength(1); }); it('respects per-edge occupied TEU from caller-provided placements', () => { const slots = [{ sequenceNo: 1, from: 0, to: 2 }]; const provided = [{ bookingContainerId: 'P-line', unitIndex: 0, sequenceNo: 1 }]; const providedUnit = ft40('P', 0, { from: 0, to: 1 }); const occupied = occupiedTeuPerEdgeBySlot(provided, [providedUnit], 2); // Edge 0 is full on slot 1; an edge-0 unit overflows, an edge-1 unit fits. const edge0 = autoFillPlacements([ft40('Q', 0, { from: 0, to: 1 })], slots, occupied, 2); expect(edge0.overflow).toHaveLength(1); const edge1 = autoFillPlacements([ft40('Q', 0, { from: 1, to: 2 })], slots, occupied, 2); expect(edge1.overflow).toHaveLength(0); expect(edge1.placements[0].sequenceNo).toBe(1); }); it('a unit never lands on a slot that does not ride its leg', () => { const slots = [{ sequenceNo: 1, from: 0, to: 1 }]; // alights at stop 1 const { placements, overflow } = autoFillPlacements( [ft40('Y', 0, { from: 1, to: 2 })], slots, new Map(), 2, ); expect(placements).toHaveLength(0); expect(overflow).toHaveLength(1); }); });