import { BookingOrdersService } from './booking-orders.service'; /** * Phase-0 spine: a drawdown order spawns a PRICED, UNPAID child booking that * waits for Marketing review (or the customs clearance gate first) — it does * NOT auto-enter the train batch pool, and the contract is not charged. */ describe('BookingOrdersService — child spawn on order create', () => { function makeService(opts: { includesCustoms: boolean; roadKm?: number | null }) { const contract = { id: 'c-1', bookingType: 'GENERAL_CONTRACT', status: 'CONTRACT_ACTIVE', expiresAt: new Date('2030-01-01T00:00:00.000Z'), freightType: 'BULK', originYardId: 'o-1', destinationYardId: 'd-1', companyId: null, paymentCurrency: 'ETB', serviceType: { includesCustoms: opts.includesCustoms, code: 'RAIL_BULK' }, bookingContainers: [], }; // Capture what status the child is created with. const created: Record[] = []; const managerUpdates: Record[] = []; const fakeManager = { create: (_entity: unknown, data: Record) => { created.push(data); return { id: 'child-1', ...data }; }, save: async (row: Record) => ({ id: 'child-1', ...row }), getRepository: () => ({ findOne: async () => ({ id: 'child-1', paymentCurrency: 'ETB', bookingContainers: [] }), update: async (_id: string, data: Record) => { managerUpdates.push(data); }, }), }; const dataSource = { transaction: async (cb: (m: unknown) => Promise) => cb(fakeManager), getRepository: () => ({ update: jest.fn() }), }; const ordersRepository = { countByYear: jest.fn().mockResolvedValue(0), findById: jest.fn().mockResolvedValue({ id: 'order-1', lines: [] }), }; const bookingsRepository = { findById: jest.fn().mockResolvedValue(contract), countByYear: jest.fn().mockResolvedValue(0), }; const generalContractService = { isGeneralContract: () => true, getRouteLines: jest.fn().mockResolvedValue([]), getQuantityLines: jest .fn() .mockResolvedValue([ { containerTypeId: null, remainingQuantity: 100, containerTypeName: null }, ]), isExhausted: jest.fn().mockResolvedValue(false), }; const pricingService = { computePriceForBooking: jest.fn().mockResolvedValue({ totalAmount: 500, priorityScore: 10, lineItems: [], currency: 'ETB', }), }; const ratesService = { findLiveRates: jest.fn().mockResolvedValue([]) }; const trainSchedulingService = { existsOpenScheduleOnRouteDay: jest.fn().mockResolvedValue(true), }; const companiesService = {}; const service = new BookingOrdersService( dataSource as never, ordersRepository as never, bookingsRepository as never, companiesService as never, generalContractService as never, pricingService as never, ratesService as never, trainSchedulingService as never, ); return { service, created, managerUpdates, pricingService }; } const dto = { contractBookingId: 'c-1', scheduledDate: '2026-07-01T00:00:00.000Z', lines: [{ quantity: 10, hazardousQuantity: 4, reeferQuantity: 0 }], }; it('spawns the child at OPERATION_REQUEST_PENDING (no customs), priced + unpaid', async () => { const { service, created, managerUpdates, pricingService } = makeService({ includesCustoms: false, }); await service.create(dto as never); const child = created.find((c) => c.bookingType === 'ONE_TIME')!; expect(child.status).toBe('OPERATION_REQUEST_PENDING'); expect(child.paymentStatus).toBe('PENDING'); expect(child.isHazardous).toBe(true); // line has hazardousQuantity > 0 expect(pricingService.computePriceForBooking).toHaveBeenCalled(); // The computed price is persisted onto the child. expect(managerUpdates.some((u) => u.totalAmount === 500)).toBe(true); }); it('spawns the child at AWAITING_DOCUMENTS when the service includes customs', async () => { const { service, created } = makeService({ includesCustoms: true }); await service.create(dto as never); const child = created.find((c) => c.bookingType === 'ONE_TIME')!; expect(child.status).toBe('AWAITING_DOCUMENTS'); }); it('rejects when hazardous quantity exceeds the line quantity', async () => { const { service } = makeService({ includesCustoms: false }); await expect( service.create({ ...dto, lines: [{ quantity: 5, hazardousQuantity: 9, reeferQuantity: 0 }], } as never), ).rejects.toThrow(/exceed the line quantity/); }); });