Files
edr-platform/apps/edr-freight-api/src/modules/contracts/contract-pricing.lane-scope.spec.ts
2026-07-30 12:18:50 +00:00

123 lines
3.9 KiB
TypeScript

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>): 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>): 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<Rate>);
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 }),
]);
});
});