import { ContractBookingService } from './contract-booking.service'; /** * OPERATION_CHANGES_REQUESTED resubmit with restated cargo. Operations can ask * for the cargo itself to change, so a completion payload that restates * containers must cancel the unpaid invoice, wipe the persisted cargo and * re-run the fresh-completion path (re-persist, re-price, re-invoice). A * payload without cargo keeps the day-only resubmit behavior. */ describe('ContractBookingService — changes-requested resubmit restating cargo', () => { const CONTRACT = { id: 'c-1', reference: 'CTR-1', contractKind: 'GENERAL', freightType: 'CONTAINER', tradeDirection: 'IMPORT', customsClearingEnabled: false, contractValidUntil: null, cargoScope: [], }; const bookingWithCargo = () => ({ id: 'b-1', contractId: 'c-1', reference: 'BKG-1', status: 'OPERATION_CHANGES_REQUESTED', bookingContainers: [{ containerSize: '20FT', quantity: 4 }], cargoTotalWeightVgm: 80, originYardId: 'y-o', destinationYardId: 'y-d', }); function makeService() { const bookingsRepository = { findByIdWithFiles: jest.fn().mockResolvedValue(bookingWithCargo()), deleteContainers: jest.fn().mockResolvedValue(undefined), update: jest.fn().mockResolvedValue(undefined), }; const invoiceService = { cancelUnpaidInvoiceForBooking: jest.fn().mockResolvedValue(undefined), }; const trainSchedulingService = { assertBookingWindowOpen: jest.fn().mockResolvedValue(undefined), }; const contractsRepository = { findByIdWithRelations: jest.fn().mockResolvedValue(CONTRACT), }; const service = new ContractBookingService( contractsRepository as never, bookingsRepository as never, {} as never, // bookingPricingService {} as never, // consolidationService {} as never, // containerTypesService {} as never, // ruleEngineService {} as never, // milestoneService invoiceService as never, {} as never, // bookingNotifier {} as never, // dataSource trainSchedulingService as never, {} as never, // bookingBatchService {} as never, // bookingTransitionService {} as never, // consolidationApprovalService ); return { service, bookingsRepository, invoiceService }; } // Both paths dead-end into a downstream private assert we replace with a // sentinel — which path threw tells us which branch the resubmit took. const SENTINEL = new Error('reached-branch'); it('restated cargo cancels the invoice, wipes cargo and re-runs fresh completion', async () => { const { service, bookingsRepository, invoiceService } = makeService(); // First gate inside the fresh-completion (!hasCargo) path. jest .spyOn( service as never as { assertWithinQuantityCap: () => Promise }, 'assertWithinQuantityCap', ) .mockRejectedValue(SENTINEL); await expect( service.completeUnderContract('c-1', 'b-1', { scheduledDate: new Date().toISOString(), containers: [{ containerSize: '20FT', quantity: 2 }], } as never), ).rejects.toBe(SENTINEL); expect(invoiceService.cancelUnpaidInvoiceForBooking).toHaveBeenCalledWith('b-1'); expect(bookingsRepository.deleteContainers).toHaveBeenCalledWith('b-1'); expect(bookingsRepository.update).toHaveBeenCalledWith('b-1', { cargoTotalWeightVgm: 0, }); }); it('a day-only resubmit keeps the persisted cargo and invoice untouched', async () => { const { service, bookingsRepository, invoiceService } = makeService(); // First call inside the day-only (hasCargo) resubmit path. jest .spyOn( service as never as { assertPersistedContainersAvailable: () => Promise; }, 'assertPersistedContainersAvailable', ) .mockRejectedValue(SENTINEL); await expect( service.completeUnderContract('c-1', 'b-1', { scheduledDate: new Date().toISOString(), } as never), ).rejects.toBe(SENTINEL); expect(invoiceService.cancelUnpaidInvoiceForBooking).not.toHaveBeenCalled(); expect(bookingsRepository.deleteContainers).not.toHaveBeenCalled(); }); });