import { Booking } from '../bookings/entities/booking.entity'; import { BookingBatchService } from './booking-batch.service'; import { WagonStockLedger } from './wagon-stock-ledger.util'; /** * smartBulkNeed math in isolation: the private helpers it touches * (allowedDimsWithTypes) read only their arguments, so a bare prototype * instance is enough — no Nest wiring. */ describe('BookingBatchService.smartBulkNeed', () => { const service = Object.create(BookingBatchService.prototype) as BookingBatchService; const call = ( booking: Booking, stock: WagonStockLedger, rank: Map, ) => ( service as unknown as { smartBulkNeed: ( b: Booking, d: unknown, s: WagonStockLedger, l: { fromEdge: number; toEdge: number }, r: Map, ) => { need: { wagons: number }; perType: Array<{ wagonTypeId: string; wagons: number }> } | null; } ).smartBulkNeed(booking, wagonDims, stock, { fromEdge: 0, toEdge: 1 }, rank); const nw5 = { id: 'wt-nw5', capacityTons: 70 }; const pw2 = { id: 'wt-pw2', capacityTons: 70 }; const perishable = { id: 'cargo-perishable', wagonTypes: [nw5, pw2], tonsPerWagonMap: { [nw5.id]: 30, [pw2.id]: 20 }, }; const wagonDims = { container: { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }, bulk: { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }, byWagonTypeId: new Map([ [nw5.id, { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }], [pw2.id, { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }], ]), }; const booking = (tons: number): Booking => ({ id: 'b1', reference: 'b1', freightType: 'BULK', cargoTotalWeightVgm: tons, cargoTypeId: perishable.id, cargoType: perishable, bookingContainers: [], }) as unknown as Booking; // Containers compete for NW5 → NW5 rank 2, PW2 rank 1. const contested = new Map([ [nw5.id, 2], [pw2.id, 1], ]); it('seats 695T as 10 PW2 (20T) + 17 NW5 (30T) = 27 wagons, PW2 first', () => { const stock = new WagonStockLedger( new Map([ [nw5.id, 18], [pw2.id, 10], ]), 1, ); const smart = call(booking(695), stock, contested); expect(smart).not.toBeNull(); expect(smart!.need.wagons).toBe(27); expect(smart!.perType).toEqual([ { wagonTypeId: pw2.id, wagons: 10 }, { wagonTypeId: nw5.id, wagons: 17 }, ]); }); it('returns null when the free stock cannot seat the whole booking', () => { const stock = new WagonStockLedger( new Map([ [nw5.id, 5], [pw2.id, 10], ]), 1, ); // 10×20 + 5×30 = 350T < 695T. expect(call(booking(695), stock, contested)).toBeNull(); }); it('uncontested types fall back to biggest per-cargo take (fewest wagons)', () => { const stock = new WagonStockLedger( new Map([ [nw5.id, 10], [pw2.id, 10], ]), 1, ); const even = new Map([ [nw5.id, 1], [pw2.id, 1], ]); const smart = call(booking(60), stock, even); expect(smart!.perType).toEqual([{ wagonTypeId: nw5.id, wagons: 2 }]); }); });