revert back the clerance payment

This commit is contained in:
Marshal
2026-07-23 13:54:09 +00:00
parent 13609f8d59
commit 15a6bab5d0
54 changed files with 1222 additions and 712 deletions

View File

@@ -249,6 +249,49 @@ describe('RuleEngineService — empty-container return per route + container typ
expect(result.hardBlocked.some((m) => m.includes('return'))).toBe(true);
});
it('PER_WAGON bills the wagons the empties ride back on, not the boxes', async () => {
// Same service, but the return rate is sold per wagon: 4× 20ft return =
// 2 wagons (two 20ft share a wagon) × 20 USD, not 4 × 20.
service = new RuleEngineService(
{ findById: jest.fn().mockResolvedValue(null) } as never,
{ findById: jest.fn().mockResolvedValue(null) } as never,
{
findActiveByContainerTypeId: jest
.fn()
.mockResolvedValue([{ id: 'wlr-20', maxVgmTons: 20, maxCapacityTons: null }]),
} as never,
{ findAllActive: jest.fn().mockResolvedValue([]) } as never,
{
findLiveRates: jest
.fn()
.mockResolvedValue([{ ...returnRate20, rateUnit: 'PER_WAGON' } as Rate]),
} as never,
{ findById: jest.fn().mockResolvedValue(null) } as never,
{} as never,
);
const result = await service.evaluate(
returnInput({
containers: [
{
containerTypeId: 'ct-20',
quantity: 4,
vgmPerUnitTons: 10,
totalVgmTons: 40,
returnQuantity: 4,
wagonsPerUnit: 0.5,
},
],
}),
);
const ret = result.appliedModifiers.filter((m) => m.surchargeCode === 'RETURN_SURCHARGE');
expect(ret).toHaveLength(1);
expect(ret[0].triggerValue).toBe(2);
expect(ret[0].calculatedAmount).toBe(40);
expect(ret[0].billingUnit).toBe('PER_WAGON');
});
it('legacy booking-level flag bills every container at its type rate', async () => {
const result = await service.evaluate(
returnInput({
@@ -264,3 +307,128 @@ describe('RuleEngineService — empty-container return per route + container typ
expect(ret[0].calculatedAmount).toBe(80);
});
});
describe('RuleEngineService — lashing per cargo kind', () => {
const lashing20: Rate = {
id: 'rate-lash-20',
rateType: 'LASHING',
trigger: 'LASHING',
rateValue: 30,
rateUnit: 'PER_CONTAINER',
currency: 'USD',
status: 'LIVE',
containerTypeId: 'ct-20',
cargoTypeId: null,
tradeDirection: null,
originYardId: null,
destinationYardId: null,
} as Rate;
const lashingBulk: Rate = {
...lashing20,
id: 'rate-lash-bulk',
rateValue: 2,
rateUnit: 'PER_TON',
containerTypeId: null,
} as Rate;
const buildService = (rates: Rate[]): RuleEngineService =>
new RuleEngineService(
{ findById: jest.fn().mockResolvedValue(null) } as never,
{ findById: jest.fn().mockResolvedValue(null) } as never,
{
findActiveByContainerTypeId: jest
.fn()
.mockResolvedValue([{ id: 'wlr-20', maxVgmTons: 20, maxCapacityTons: null }]),
} as never,
{ findAllActive: jest.fn().mockResolvedValue([]) } as never,
{ findLiveRates: jest.fn().mockResolvedValue(rates) } as never,
{ findById: jest.fn().mockResolvedValue(null) } as never,
{} as never,
);
const containerInput = (overrides: Partial<BookingEvaluationInput> = {}): BookingEvaluationInput => ({
serviceTypeId: 'svc-1',
paymentCurrency: 'USD',
tradeDirection: 'IMPORT',
isHazardous: false,
hasLashing: true,
totalWagons: 2,
containers: [
{
containerTypeId: 'ct-20',
quantity: 4,
vgmPerUnitTons: 10,
totalVgmTons: 40,
wagonsPerUnit: 0.5,
},
],
...overrides,
});
const bulkInput = (overrides: Partial<BookingEvaluationInput> = {}): BookingEvaluationInput => ({
serviceTypeId: 'svc-1',
paymentCurrency: 'USD',
tradeDirection: 'IMPORT',
isHazardous: false,
hasLashing: true,
totalWagons: 0,
bulkTons: 100,
bulkWagons: 3,
containers: [],
...overrides,
});
const lashingMods = (result: Awaited<ReturnType<RuleEngineService['evaluate']>>) =>
result.appliedModifiers.filter((m) => m.surchargeCode === 'LASHING');
it('bills each container line at its own container type rate', async () => {
const result = await buildService([lashing20]).evaluate(containerInput());
const mods = lashingMods(result);
expect(mods).toHaveLength(1);
expect(mods[0].triggerValue).toBe(4);
expect(mods[0].calculatedAmount).toBe(120);
expect(mods[0].billingUnit).toBe('PER_CONTAINER');
});
it('PER_WAGON container lashing bills the wagons the line occupies', async () => {
const result = await buildService([
{ ...lashing20, rateUnit: 'PER_WAGON' } as Rate,
]).evaluate(containerInput());
const mods = lashingMods(result);
expect(mods[0].triggerValue).toBe(2);
expect(mods[0].calculatedAmount).toBe(60);
expect(mods[0].billingUnit).toBe('PER_WAGON');
});
it('an unmatched container type simply bills nothing (lenient)', async () => {
const result = await buildService([
{ ...lashing20, containerTypeId: 'ct-40' } as Rate,
]).evaluate(containerInput());
expect(lashingMods(result)).toHaveLength(0);
});
it('bulk lashing bills per ton on the tonnage', async () => {
const result = await buildService([lashingBulk]).evaluate(bulkInput());
const mods = lashingMods(result);
expect(mods[0].triggerValue).toBe(100);
expect(mods[0].calculatedAmount).toBe(200);
expect(mods[0].billingUnit).toBe('PER_TON');
});
it('PER_WAGON bulk lashing bills the wagons the bulk occupies', async () => {
const result = await buildService([
{ ...lashingBulk, rateUnit: 'PER_WAGON', rateValue: 25 } as Rate,
]).evaluate(bulkInput());
const mods = lashingMods(result);
expect(mods[0].triggerValue).toBe(3);
expect(mods[0].calculatedAmount).toBe(75);
});
it('no lashing charge when the cargo does not need lashing', async () => {
const result = await buildService([lashing20]).evaluate(
containerInput({ hasLashing: false }),
);
expect(lashingMods(result)).toHaveLength(0);
});
});