import { ConflictException } from '@nestjs/common'; import { BookingBatchService } from './booking-batch.service'; import { Booking } from '../bookings/entities/booking.entity'; /** * Export whole-booking single-train gate: an export booking never splits — it * rides one train whole or is rejected. The report must try every fillable * train on the day (first full → use the second), and when none fits, say how * much space is still bookable so the customer knows what he CAN book. */ describe('BookingBatchService — exportSpaceReport (whole-booking, single train)', () => { const DAY = new Date('2026-07-20T10:00:00Z'); const schedule = (id: string) => ({ id, status: 'SCHEDULED', direction: 'EXPORT', scheduledDepartureDate: DAY, bookingWindowStatus: 'OPEN', windowPhase: null, // legacy gate: OPEN alone makes it fillable }); const fullGraph = (id: string) => ({ id, originStationId: 'yard-a', destinationStationId: 'yard-b', routeId: null, // legacy two-stop pseudo-route — no milestone query scheduleBookings: [], trainSet: { locomotive: { maxPullWeightTons: 500, maxTrainLengthMeters: 140, overageToleranceTons: 0, overageToleranceMeters: 0, }, }, }); const exportBooking = (cargoTons: number) => ({ id: 'bk-exp', freightType: 'BULK', tradeDirection: 'EXPORT', scheduledDate: DAY, originYardId: 'yard-a', destinationYardId: 'yard-b', cargoTotalWeightVgm: cargoTons, bookingContainers: [], }) as unknown as Booking; // A reserved bulk booking heavy enough to exhaust the 500t pull budget. const heavyReserved = { id: 'bk-heavy', freightType: 'BULK', originYardId: 'yard-a', destinationYardId: 'yard-b', cargoTotalWeightVgm: 476, bookingContainers: [], } as unknown as Booking; let service: BookingBatchService; let trainSchedulesRepository: { findAll: jest.Mock; findByIdWithFullGraph: jest.Mock; findById: jest.Mock; }; let bookingsRepository: { findReservedForSchedule: jest.Mock }; beforeEach(() => { trainSchedulesRepository = { findAll: jest.fn().mockResolvedValue([]), findByIdWithFullGraph: jest .fn() .mockImplementation(async (id: string) => fullGraph(id)), findById: jest.fn(), }; bookingsRepository = { findReservedForSchedule: jest.fn().mockResolvedValue([]) }; // WagonType.find() → [] so representative default dims apply (bulk 60t // payload / 23.4t tare / 14m); RouteMilestone is never queried (routeId null). const genericRepo = { find: jest.fn().mockResolvedValue([]) }; const dataSource = { getRepository: jest.fn().mockReturnValue(genericRepo) }; service = new BookingBatchService( dataSource as never, bookingsRepository as never, trainSchedulesRepository as never, {} as never, // trainScheduleBookingsRepository {} as never, // notifier {} as never, // scheduler {} as never, // trainSchedulingService {} as never, // billing {} as never, // bookingWindowGateway {} as never, // pricingService ); }); it('uses the other train when the first one is full', async () => { trainSchedulesRepository.findAll.mockResolvedValue([ schedule('train-1'), schedule('train-2'), ]); bookingsRepository.findReservedForSchedule.mockImplementation( async (id: string) => (id === 'train-1' ? [heavyReserved] : []), ); const report = await service.exportSpaceReport(exportBooking(60)); expect(report.scheduleId).toBe('train-2'); }); it('rejects a booking no single train fits and reports the bookable space', async () => { trainSchedulesRepository.findAll.mockResolvedValue([schedule('train-1')]); const report = await service.exportSpaceReport(exportBooking(900)); expect(report.scheduleId).toBeNull(); expect(report.bestAvailable).not.toBeNull(); expect(report.bestAvailable!.cargoTons).toBeGreaterThan(0); expect(report.bestAvailable!.cargoTons).toBeLessThan(900); expect(report.fullMessage).toMatch(/largest remaining space is about .* tons/); expect(report.fullMessage).toMatch(/single train whole/); await expect(service.pickExportSchedule(exportBooking(900))).rejects.toThrow( ConflictException, ); }); it('says no train is accepting bookings when the day has none', async () => { trainSchedulesRepository.findAll.mockResolvedValue([]); const report = await service.exportSpaceReport(exportBooking(60)); expect(report.scheduleId).toBeNull(); expect(report.fullMessage).toBe( 'No export train is accepting bookings for this day', ); }); });