import { ContractRateScheduleBuilder } from './contract-rate-schedule.builder'; import { Rate } from '../modules/rule-engine/entities/rate.entity'; /** Minimal Rate factory for the builder unit tests. */ function rate(partial: Partial): Rate { return { trigger: 'ALWAYS', appliesTo: 'CONTAINER', tradeDirection: 'IMPORT', rateType: 'CONTAINER_IMPORT', currency: 'USD', rateValue: 200, rateUnit: 'PER_CONTAINER', ...partial, } as Rate; } describe('ContractRateScheduleBuilder', () => { const LIVE: Rate[] = [ rate({ appliesTo: 'CONTAINER', tradeDirection: 'IMPORT', rateType: 'CONTAINER_IMPORT', rateValue: 200, rateUnit: 'PER_CONTAINER', originYard: { label: 'Negad' } as never, destinationYard: { label: 'Mojo Dry Port' } as never, containerType: { label: '40ft GP' } as never, }), rate({ appliesTo: 'CONTAINER', tradeDirection: 'EXPORT', // wrong direction — must be filtered out for import rateType: 'CONTAINER_EXPORT', rateValue: 819, originYard: { label: 'GMP' } as never, destinationYard: { label: 'SGTD' } as never, }), rate({ appliesTo: 'BULK', // wrong freight — filtered out for a container contract tradeDirection: 'IMPORT', rateType: 'BULK_IMPORT', rateUnit: 'PER_WAGON', rateValue: 100, }), rate({ appliesTo: 'FIRST_MILE', trigger: 'ALWAYS', tradeDirection: null, rateUnit: 'PER_CONTAINER', rateValue: 50, }), rate({ appliesTo: 'OTHER', trigger: 'CUSTOMS_CLEARANCE', tradeDirection: null, rateType: 'CUSTOMS_CLEARANCE', rateUnit: 'FLAT', rateValue: 120, }), ]; const build = (dir: 'IMP' | 'EXP' | 'DOM', freight: 'CON' | 'BULK') => { const service = { findLiveRatesDetailed: jest.fn().mockResolvedValue(LIVE) }; return new ContractRateScheduleBuilder(service as never).build(dir, freight); }; it('shows only import container lanes for an import container contract', async () => { const s = await build('IMP', 'CON'); expect(s.freightLanes).toHaveLength(1); expect(s.freightLanes[0]).toMatchObject({ route: 'Negad → Mojo Dry Port', cargo: '40ft GP', currency: 'USD', amount: '200', unit: 'per container', }); }); it('always lists route-agnostic services and surcharges', async () => { const s = await build('IMP', 'CON'); expect(s.additionalServices).toHaveLength(1); expect(s.additionalServices[0].route).toBe('First-mile pickup by truck'); expect(s.surcharges).toHaveLength(1); expect(s.surcharges[0].route).toBe('Customs clearance service'); }); it('excludes container lanes from a bulk contract', async () => { const s = await build('IMP', 'BULK'); expect(s.freightLanes).toHaveLength(1); expect(s.freightLanes[0]).toMatchObject({ amount: '100', unit: 'per wagon' }); }); it('flags an empty schedule when nothing priced matches', async () => { const service = { findLiveRatesDetailed: jest.fn().mockResolvedValue([]) }; const s = await new ContractRateScheduleBuilder(service as never).build('DOM', 'CON'); expect(s.isEmpty).toBe(true); }); });