import { UnprocessableEntityException } from '@nestjs/common'; import { ContractPricingService } from './contract-pricing.service'; import type { Contract } from './entities/contract.entity'; import type { Rate } from '../rule-engine/entities/rate.entity'; const CT20 = 'ct-20'; const CT40 = 'ct-40'; const DCT = 'yard-dct'; const SEBETA = 'yard-sebeta'; const GMP = 'yard-gmp'; const rate = (over: Partial): Rate => ({ rateType: 'CONTAINER_IMPORT', currency: 'USD', rateValue: 1000, rateUnit: 'PER_CONTAINER', containerTypeId: null, cargoTypeId: null, originYardId: DCT, destinationYardId: SEBETA, ...over, }) as Rate; const contract = (over: Partial): Contract => ({ freightType: 'CONTAINER', tradeDirection: 'IMPORT', paymentCurrency: 'USD', customsClearingEnabled: false, isHazardous: false, isReefer: false, routes: [{ originYardId: DCT, destinationYardId: SEBETA, sortOrder: 0 }], cargoScope: [{ containerSize: '20ft' }], ...over, }) as Contract; const service = (liveRates: Rate[]): ContractPricingService => new ContractPricingService( {} as never, { findLiveRates: async () => liveRates } as never, { findAll: async () => ({ items: [ { id: CT20, sizeFt: 20 }, { id: CT40, sizeFt: 40 }, ], }), } as never, { getRate: async () => 1 } as never, ); describe('contract base freight is priced on the contract lane only', () => { it('prices from the contract route, never another lane (CTR-2026-00065)', async () => { const breakdown = await service([ // Same size, other lane — the leak that priced DCT → Sebeta at GMP rates. rate({ containerTypeId: CT20, destinationYardId: GMP, rateValue: 1690 }), rate({ containerTypeId: CT20, rateValue: 750 }), ]).buildBreakdown(contract({})); expect(breakdown.lineItems).toEqual([ expect.objectContaining({ code: 'CONTAINER_20FT', unitPrice: 750 }), ]); }); it('blocks the contract when its lane has no container rate', async () => { await expect( service([ rate({ containerTypeId: CT20, destinationYardId: GMP, rateValue: 1690 }), ]).buildBreakdown(contract({})), ).rejects.toThrow(UnprocessableEntityException); }); it('freezes the OVERWEIGHT_PER_TON surcharge on export contracts only', async () => { const overweight = rate({ rateType: 'OVERWEIGHT_PER_TON', trigger: 'OVERWEIGHT', rateUnit: 'PER_TON', rateValue: 25, originYardId: null, destinationYardId: null, } as Partial); const exported = await service([ rate({ rateType: 'CONTAINER_EXPORT', containerTypeId: CT20, rateValue: 900 }), overweight, ]).buildBreakdown(contract({ tradeDirection: 'EXPORT' })); expect(exported.lineItems).toEqual( expect.arrayContaining([ expect.objectContaining({ code: 'OVERWEIGHT_PER_TON', unitPrice: 25 }), ]), ); // Import derives overweight from the route's base freight — never frozen. const imported = await service([ rate({ containerTypeId: CT20, rateValue: 750 }), overweight, ]).buildBreakdown(contract({})); expect( imported.lineItems.some((li) => li.code === 'OVERWEIGHT_PER_TON'), ).toBe(false); }); it('blocks bulk contracts too instead of borrowing an arbitrary rate', async () => { const bulk = contract({ freightType: 'BULK', cargoScope: [] }); await expect( service([ rate({ rateType: 'BULK_IMPORT', rateUnit: 'PER_TON', destinationYardId: GMP, }), ]).buildBreakdown(bulk), ).rejects.toThrow(UnprocessableEntityException); const priced = await service([ rate({ rateType: 'BULK_IMPORT', rateUnit: 'PER_TON', rateValue: 32 }), ]).buildBreakdown(bulk); expect(priced.lineItems).toEqual([ expect.objectContaining({ code: 'BULK_FREIGHT', unitPrice: 32 }), ]); }); });