import { BadRequestException } from '@nestjs/common'; import { BookingTransitionService } from './booking-transition.service'; /** * Operation-request review for general-contract drawdown orders: * - ACCEPT a train order → FULLY_EXECUTED with the invoice ensured; import/ * domestic bookings wait for their booking-day window cycle (no immediate * batch enqueue at accept time). * - ACCEPT a road order → ROAD_DISPATCH_PENDING, never enters the train batch. * - REQUEST_CHANGES requires a note → OPERATION_CHANGES_REQUESTED. */ describe('BookingTransitionService — operation review', () => { function makeService(serviceTypeCode: string) { const booking = { id: 'b-1', reference: 'BKG-1', status: 'OPERATION_REQUEST_PENDING', originYardId: 'o-1', destinationYardId: 'd-1', scheduledDate: new Date('2026-07-01T00:00:00.000Z'), serviceType: { code: serviceTypeCode }, }; const bookingsRepository = { update: jest.fn().mockResolvedValue({ id: 'b-1' }), createReviewNote: jest.fn().mockResolvedValue(undefined), }; const bookingsService = { findById: jest.fn().mockResolvedValue(booking), }; const bookingBatchService = { enqueueRouteDayProcessing: jest.fn(), pickExportSchedule: jest.fn(), acceptExportBooking: jest.fn(), }; const invoiceService = { ensureInvoiceForBooking: jest .fn() .mockResolvedValue({ id: 'inv-1', invoiceNumber: 'INV-0001' }), updateStatus: jest.fn().mockResolvedValue(undefined), }; const service = new BookingTransitionService( bookingsRepository as never, {} as never, // ruleEngineService {} as never, // pricingService {} as never, // contractService {} as never, // filesService {} as never, // fileUploadSettingsService bookingBatchService as never, bookingsService as never, { isPhasedGeneralCustomsBooking: () => false } as never, {} as never, // workflowService invoiceService as never, { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never, { accepted: jest.fn(), approved: jest.fn(), rejected: jest.fn(), changesRequested: jest.fn(), documentQueried: jest.fn(), clearanceReady: jest.fn(), operationChangesRequested: jest.fn(), operationAccepted: jest.fn(), inTransit: jest.fn(), completed: jest.fn(), cancelled: jest.fn(), submittedToStaff: jest.fn(), customerSignedToStaff: jest.fn(), operationRequestedToStaff: jest.fn(), clearanceDocsUploadedToStaff: jest.fn(), dutySlipUploadedToStaff: jest.fn(), } as never, // notifier ); return { service, bookingsRepository, bookingBatchService, invoiceService }; } it('ACCEPT of a train order → FULLY_EXECUTED, invoice ensured, batch waits for window cycle', async () => { const { service, bookingsRepository, bookingBatchService, invoiceService } = makeService('RAIL_CONTAINER'); await service.reviewOperationRequest('b-1', 'ACCEPT', 'staff-1'); expect(bookingsRepository.update).toHaveBeenCalledWith( 'b-1', expect.objectContaining({ status: 'FULLY_EXECUTED' }), ); expect(invoiceService.ensureInvoiceForBooking).toHaveBeenCalledTimes(1); // Import/domestic train bookings are batched by the window cycle later — // never enqueued directly at accept time. expect(bookingBatchService.enqueueRouteDayProcessing).not.toHaveBeenCalled(); expect(bookingBatchService.acceptExportBooking).not.toHaveBeenCalled(); }); it('ACCEPT of a road order → ROAD_DISPATCH_PENDING and does NOT enter the batch', async () => { const { service, bookingsRepository, bookingBatchService, invoiceService } = makeService('ROAD_CONTAINER'); await service.reviewOperationRequest('b-1', 'ACCEPT', 'staff-1'); expect(bookingsRepository.update).toHaveBeenCalledWith( 'b-1', expect.objectContaining({ status: 'ROAD_DISPATCH_PENDING' }), ); expect(invoiceService.ensureInvoiceForBooking).toHaveBeenCalledTimes(1); expect(bookingBatchService.enqueueRouteDayProcessing).not.toHaveBeenCalled(); }); it('REQUEST_CHANGES requires a note → OPERATION_CHANGES_REQUESTED', async () => { const { service, bookingsRepository } = makeService('RAIL_CONTAINER'); await expect( service.reviewOperationRequest('b-1', 'REQUEST_CHANGES', 'staff-1', {}), ).rejects.toBeInstanceOf(BadRequestException); await service.reviewOperationRequest('b-1', 'REQUEST_CHANGES', 'staff-1', { note: 'Fix the schedule', }); expect(bookingsRepository.update).toHaveBeenCalledWith( 'b-1', expect.objectContaining({ status: 'OPERATION_CHANGES_REQUESTED' }), ); }); });