mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: fuel surcharge per lane and cargo type
This commit is contained in:
@@ -193,17 +193,26 @@ export class ContractRateScheduleBuilder {
|
|||||||
return rate.tradeDirection === want;
|
return rate.tradeDirection === want;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fuel row — the lane matters, so it rides along in the charge label. */
|
/**
|
||||||
|
* Fuel row — the lane matters, so it rides along in the charge label.
|
||||||
|
* Per-liter collapses to one flat total (base liters × rate value); the
|
||||||
|
* customer only ever sees the final price.
|
||||||
|
*/
|
||||||
private fuelRow(rate: Rate): RateScheduleRow {
|
private fuelRow(rate: Rate): RateScheduleRow {
|
||||||
const origin = rate.originYard?.label ?? rate.originYard?.code ?? '—';
|
const origin = rate.originYard?.label ?? rate.originYard?.code ?? '—';
|
||||||
const destination =
|
const destination =
|
||||||
rate.destinationYard?.label ?? rate.destinationYard?.code ?? '—';
|
rate.destinationYard?.label ?? rate.destinationYard?.code ?? '—';
|
||||||
|
const perLiter = rate.rateUnit === 'PER_LITER';
|
||||||
return {
|
return {
|
||||||
route: `Fuel surcharge (${origin} → ${destination})`,
|
route: `Fuel surcharge (${origin} → ${destination})`,
|
||||||
cargo: this.cargoLabel(rate),
|
cargo: this.cargoLabel(rate),
|
||||||
currency: rate.currency,
|
currency: rate.currency,
|
||||||
amount: this.formatAmount(rate.rateValue),
|
amount: this.formatAmount(
|
||||||
unit: this.unitLabel(rate.rateUnit),
|
perLiter
|
||||||
|
? Number(rate.baseLiters ?? 0) * Number(rate.rateValue)
|
||||||
|
: rate.rateValue,
|
||||||
|
),
|
||||||
|
unit: perLiter ? 'flat' : this.unitLabel(rate.rateUnit),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,15 +302,18 @@ export class ContractPricingService {
|
|||||||
r.cargoTypeId === scope.cargoTypeId,
|
r.cargoTypeId === scope.cargoTypeId,
|
||||||
);
|
);
|
||||||
if (fuel && Number(fuel.rateValue) > 0) {
|
if (fuel && Number(fuel.rateValue) > 0) {
|
||||||
const base = Number(fuel.baseLiters ?? 0);
|
// Per-liter collapses to one flat total (base liters × rate value) —
|
||||||
|
// the customer only sees the final price, and booking pricing bills
|
||||||
|
// the same flat figure once (see RuleEngineService.fuelCharges).
|
||||||
|
const perLiter = fuel.rateUnit === 'PER_LITER';
|
||||||
|
const total = perLiter
|
||||||
|
? Number(fuel.baseLiters ?? 0) * Number(fuel.rateValue)
|
||||||
|
: Number(fuel.rateValue);
|
||||||
lineItems.push({
|
lineItems.push({
|
||||||
code: 'FUEL_SURCHARGE',
|
code: 'FUEL_SURCHARGE',
|
||||||
label:
|
label: `Fuel surcharge (${scope.cargoType.cargoTypeName})`,
|
||||||
fuel.rateUnit === 'PER_LITER'
|
unit: perLiter ? 'flat' : toContractUnit(fuel.rateUnit),
|
||||||
? `Fuel surcharge (${scope.cargoType.cargoTypeName}, ${base} liters)`
|
unitPrice: convert(total),
|
||||||
: `Fuel surcharge (${scope.cargoType.cargoTypeName})`,
|
|
||||||
unit: toContractUnit(fuel.rateUnit),
|
|
||||||
unitPrice: convert(Number(fuel.rateValue)),
|
|
||||||
cargoTypeCode: scope.cargoType.code ?? null,
|
cargoTypeCode: scope.cargoType.code ?? null,
|
||||||
conditionalOn: 'has_fuel',
|
conditionalOn: 'has_fuel',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -475,13 +475,16 @@ describe('RuleEngineService — fuel surcharge (per lane + cargo type)', () => {
|
|||||||
const fuelMods = (result: Awaited<ReturnType<RuleEngineService['evaluate']>>) =>
|
const fuelMods = (result: Awaited<ReturnType<RuleEngineService['evaluate']>>) =>
|
||||||
result.appliedModifiers.filter((m) => m.surchargeCode === 'FUEL_SURCHARGE');
|
result.appliedModifiers.filter((m) => m.surchargeCode === 'FUEL_SURCHARGE');
|
||||||
|
|
||||||
it('PER_LITER bills base liters × rate value once, regardless of wagons', async () => {
|
it('PER_LITER collapses to one flat total (base liters × rate value), regardless of wagons', async () => {
|
||||||
const result = await buildService([fuelPerLiter]).evaluate(fuelInput());
|
const result = await buildService([fuelPerLiter]).evaluate(fuelInput());
|
||||||
const mods = fuelMods(result);
|
const mods = fuelMods(result);
|
||||||
expect(mods).toHaveLength(1);
|
expect(mods).toHaveLength(1);
|
||||||
expect(mods[0].triggerValue).toBe(100);
|
// Flat: the customer sees only the total, and a frozen contract snapshot
|
||||||
|
// (also stored flat) multiplies it by quantity 1 — never by the liters.
|
||||||
|
expect(mods[0].triggerValue).toBe(1);
|
||||||
|
expect(mods[0].unitPriceUsd).toBe(200);
|
||||||
expect(mods[0].calculatedAmount).toBe(200);
|
expect(mods[0].calculatedAmount).toBe(200);
|
||||||
expect(mods[0].billingUnit).toBe('PER_LITER');
|
expect(mods[0].billingUnit).toBe('FLAT');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('PER_WAGON bills the wagons the cargo occupies', async () => {
|
it('PER_WAGON bills the wagons the cargo occupies', async () => {
|
||||||
|
|||||||
@@ -648,9 +648,12 @@ export class RuleEngineService {
|
|||||||
/**
|
/**
|
||||||
* Fuel surcharge — fires when the booking's cargo type has hasFuel = true,
|
* Fuel surcharge — fires when the booking's cargo type has hasFuel = true,
|
||||||
* billed off the FUEL rate matching the booking's lane (trade direction +
|
* billed off the FUEL rate matching the booking's lane (trade direction +
|
||||||
* origin + destination) and cargo type. PER_LITER bills baseLiters ×
|
* origin + destination) and cargo type. PER_LITER collapses to one FLAT
|
||||||
* rateValue once per booking; PER_WAGON bills the wagons the cargo occupies.
|
* amount (baseLiters × rateValue, once per booking) — the customer only ever
|
||||||
* No matching lane rate simply bills nothing — same leniency as lashing.
|
* sees the total, and the frozen contract snapshot stores that same flat
|
||||||
|
* figure so the snapshot-override math bills it exactly once. PER_WAGON
|
||||||
|
* bills the wagons the cargo occupies. No matching lane rate simply bills
|
||||||
|
* nothing — same leniency as lashing.
|
||||||
*/
|
*/
|
||||||
private fuelCharges(
|
private fuelCharges(
|
||||||
input: BookingEvaluationInput,
|
input: BookingEvaluationInput,
|
||||||
@@ -673,9 +676,12 @@ export class RuleEngineService {
|
|||||||
0,
|
0,
|
||||||
Number(input.bulkWagons ?? 0) || Number(input.totalWagons ?? 0),
|
Number(input.bulkWagons ?? 0) || Number(input.totalWagons ?? 0),
|
||||||
);
|
);
|
||||||
const billedQty =
|
const perLiter = rate.rateUnit === 'PER_LITER';
|
||||||
rate.rateUnit === 'PER_LITER' ? Number(rate.baseLiters ?? 0) : wagons;
|
const billedQty = perLiter ? 1 : wagons;
|
||||||
const amount = billedQty * rateValue;
|
const unitPrice = perLiter
|
||||||
|
? Number(rate.baseLiters ?? 0) * rateValue
|
||||||
|
: rateValue;
|
||||||
|
const amount = billedQty * unitPrice;
|
||||||
if (!(amount > 0)) return modifiers;
|
if (!(amount > 0)) return modifiers;
|
||||||
modifiers.push({
|
modifiers.push({
|
||||||
rateId: rate.id,
|
rateId: rate.id,
|
||||||
@@ -683,8 +689,8 @@ export class RuleEngineService {
|
|||||||
triggerValue: billedQty,
|
triggerValue: billedQty,
|
||||||
calculatedAmount: amount,
|
calculatedAmount: amount,
|
||||||
currency: rate.currency,
|
currency: rate.currency,
|
||||||
unitPriceUsd: rateValue,
|
unitPriceUsd: unitPrice,
|
||||||
billingUnit: rate.rateUnit,
|
billingUnit: perLiter ? 'FLAT' : rate.rateUnit,
|
||||||
});
|
});
|
||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user