import { Booking } from '../../bookings/entities/booking.entity'; import { WagonType } from '../../wagon-types/entities/wagon-type.entity'; import { computeFleetAvailability, selectBookingsWithinFleetCap, sortBookingsForScheduling, summarizeFleetWarnings, wagonsRequiredForBooking, } from './fleet-plan.util'; import { buildContainerWagonPlan, type WagonPlanSlot } from './wagon-plan.util'; const nw5: WagonType = { id: 'wt-nw5', code: 'NW5', name: 'Flat Wagon', capacityTons: 70, lengthMeters: 14, supportedLoadTypes: ['CONTAINER'], isActive: true, supportsContainer: true, } as WagonType; const makeBooking = ( id: string, extra: Partial = {}, ): Booking => ({ id, reference: id, freightType: 'CONTAINER', isGovernment: false, priorityScore: 0, scheduledDate: new Date('2026-06-20T08:00:00.000Z'), cargoTotalWeightVgm: 50, bookingContainers: [{ id: `${id}-line`, quantity: 2, wagonsRequired: 1, vgmPerUnitTons: 25 }], ...extra, }) as Booking; describe('fleet-plan.util', () => { it('sorts bookings government first, then priority, then date', () => { const bookings = [ makeBooking('late', { scheduledDate: new Date('2026-06-22T08:00:00.000Z') }), makeBooking('gov', { isGovernment: true, priorityScore: 0 }), makeBooking('prio', { priorityScore: 10 }), ]; const sorted = sortBookingsForScheduling(bookings); expect(sorted.map((b) => b.id)).toEqual(['gov', 'prio', 'late']); }); it('computes fleet availability with shortfall', () => { const plan: WagonPlanSlot[] = buildContainerWagonPlan( [ makeBooking('b1', { bookingContainers: [ { id: 'b1-line', quantity: 4, wagonsRequired: 2, vgmPerUnitTons: 25 } as never, ], }), ], nw5, ); const fleetByTypeId = new Map([[nw5.id, 1]]); const rows = computeFleetAvailability(plan, fleetByTypeId, new Map([[nw5.id, 'NW5']])); const nw5Row = rows.find((r) => r.wagonTypeCode === 'NW5'); expect(nw5Row?.needed).toBe(2); expect(nw5Row?.available).toBe(1); expect(nw5Row?.shortfall).toBe(1); }); it('defers lower-priority bookings when fleet is insufficient', () => { const high = makeBooking('high', { priorityScore: 100, bookingContainers: [ { id: 'high-line', quantity: 2, wagonsRequired: 2, vgmPerUnitTons: 25 } as never, ], }); const low = makeBooking('low', { priorityScore: 1, bookingContainers: [ { id: 'low-line', quantity: 2, wagonsRequired: 2, vgmPerUnitTons: 25 } as never, ], }); const fleet = new Map([[nw5.id, 2]]); const { fitting, deferred } = selectBookingsWithinFleetCap( [low, high], fleet, () => nw5.id, ); expect(fitting.map((b) => b.id)).toEqual(['high']); expect(deferred).toHaveLength(1); expect(deferred[0]?.id).toBe('low'); expect(deferred[0]?.reason).toContain('2'); }); it('summarizes fleet shortage warnings', () => { const warnings = summarizeFleetWarnings( [ { wagonTypeId: nw5.id, wagonTypeCode: 'NW5', needed: 5, available: 2, shortfall: 3, }, ], [{ id: 'b1', reference: 'BKG-1', reason: 'No wagons' }], ); expect(warnings.some((w) => w.includes('Fleet shortage'))).toBe(true); expect(warnings.some((w) => w.includes('deferred'))).toBe(true); }); it('names the booking and its per-type shortfall when the deferral carries a shortage', () => { const warnings = summarizeFleetWarnings( [], [ { id: 'b1', reference: 'BKG-1', reason: 'No available NW6 wagon at the yard', shortage: { wagonTypeCodes: 'NW6', wagonsNeeded: 2, wagonsAvailable: 1, wagonsShort: 1, }, }, ], ); expect( warnings.some( (w) => w.includes('BKG-1') && w.includes('2 × NW6') && w.includes('only 1 available') && w.includes('short 1'), ), ).toBe(true); }); it('counts wagons required per booking from container lines', () => { const booking = makeBooking('b1', { bookingContainers: [ { id: 'b1-line-0', quantity: 2, wagonsRequired: 1, vgmPerUnitTons: 25 } as never, { id: 'b1-line-1', quantity: 1, wagonsRequired: 1, vgmPerUnitTons: 25 } as never, ], }); expect(wagonsRequiredForBooking(booking)).toBe(2); }); });