import { BookingBatchService } from './booking-batch.service'; import { Booking } from '../bookings/entities/booking.entity'; describe('BookingBatchService — PAID reconcile', () => { const scheduleId = 'schedule-1'; const bookingId = 'booking-1'; const paidBooking = { id: bookingId, reference: 'BK-2026-000034', trainScheduleId: scheduleId, status: 'PAID', paymentStatus: 'PAID', isGovernment: false, cargoTotalWeightVgm: 20, bookingContainers: [], } as unknown as Booking; let service: BookingBatchService; let bookingsRepository: { findPaidUnlinkedForSchedule: jest.Mock; findBatchPool: jest.Mock; findReservedForSchedule: jest.Mock; update: jest.Mock; }; let trainScheduleBookingsRepository: { existsForBooking: jest.Mock; createMany: jest.Mock; }; let trainSchedulesRepository: { findByIdWithFullGraph: jest.Mock; findAll: jest.Mock; }; let trainSchedulingService: { tryAutoWagonAllocation: jest.Mock; }; let dataSource: { getRepository: jest.Mock; transaction: jest.Mock; }; beforeEach(() => { bookingsRepository = { findPaidUnlinkedForSchedule: jest.fn().mockResolvedValue([]), findBatchPool: jest.fn().mockResolvedValue([]), findReservedForSchedule: jest.fn().mockResolvedValue([]), update: jest.fn().mockResolvedValue(undefined), }; trainScheduleBookingsRepository = { existsForBooking: jest.fn().mockResolvedValue(false), createMany: jest.fn().mockResolvedValue(undefined), }; trainSchedulesRepository = { findByIdWithFullGraph: jest.fn().mockResolvedValue({ id: scheduleId, maxWagons: 10, bookingWindowStatus: 'OPEN', trainSet: { locomotive: { maxPullWeightTons: 3500, maxTrainLengthMeters: 760 } }, scheduleBookings: [], }), findAll: jest.fn().mockResolvedValue([]), }; trainSchedulingService = { tryAutoWagonAllocation: jest.fn().mockResolvedValue({ assignedBookingIds: [], deferred: [], issues: [], violations: [], }), }; const bookingRepo = { findOne: jest.fn().mockResolvedValue(paidBooking), update: jest.fn().mockResolvedValue(undefined), }; dataSource = { getRepository: jest.fn().mockReturnValue(bookingRepo), transaction: jest.fn(async (fn: (m: unknown) => Promise) => { const manager = { getRepository: () => bookingRepo, }; await fn(manager); }), }; service = new BookingBatchService( dataSource as never, bookingsRepository as never, trainSchedulesRepository as never, trainScheduleBookingsRepository as never, { payNow: jest.fn(), secured: jest.fn(), expired: jest.fn() } as never, { addTimeout: jest.fn(), deleteTimeout: jest.fn(), doesExist: jest.fn() } as never, trainSchedulingService as never, ); }); it('reconcilePaidUnlinked links PAID bookings without a schedule row', async () => { bookingsRepository.findPaidUnlinkedForSchedule.mockResolvedValue([paidBooking]); await service.reconcilePaidUnlinked(scheduleId); expect(bookingsRepository.findPaidUnlinkedForSchedule).toHaveBeenCalledWith(scheduleId); expect(trainScheduleBookingsRepository.createMany).toHaveBeenCalledWith( [{ trainScheduleId: scheduleId, bookingId }], expect.anything(), ); }); it('ensurePaidBookingAllocated links PAID booking when not yet linked', async () => { await service.ensurePaidBookingAllocated(bookingId); expect(trainScheduleBookingsRepository.createMany).toHaveBeenCalledTimes(1); expect(trainSchedulingService.tryAutoWagonAllocation).toHaveBeenCalledWith(scheduleId); }); it('ensurePaidBookingAllocated is idempotent when already linked', async () => { trainScheduleBookingsRepository.existsForBooking.mockResolvedValue(true); await service.ensurePaidBookingAllocated(bookingId); await service.ensurePaidBookingAllocated(bookingId); expect(trainScheduleBookingsRepository.createMany).not.toHaveBeenCalled(); expect(trainSchedulingService.tryAutoWagonAllocation).toHaveBeenCalledTimes(2); }); it('processSchedule reconciles PAID-unlinked before wagon allocation', async () => { const fillSpy = jest.spyOn(service, 'fillSchedule').mockResolvedValue(undefined); const settleSpy = jest.spyOn(service, 'settleDueReservations').mockResolvedValue(undefined); const reconcileSpy = jest.spyOn(service, 'reconcilePaidUnlinked').mockResolvedValue(undefined); await service.processSchedule(scheduleId); expect(fillSpy).toHaveBeenCalledWith(scheduleId); expect(settleSpy).toHaveBeenCalledWith(scheduleId); expect(reconcileSpy).toHaveBeenCalledWith(scheduleId); expect(trainSchedulingService.tryAutoWagonAllocation).toHaveBeenCalledWith(scheduleId); const fillOrder = fillSpy.mock.invocationCallOrder[0]; const reconcileOrder = reconcileSpy.mock.invocationCallOrder[0]; const wagonOrder = trainSchedulingService.tryAutoWagonAllocation.mock.invocationCallOrder[0]; expect(fillOrder).toBeLessThan(reconcileOrder); expect(reconcileOrder).toBeLessThan(wagonOrder); }); });