mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
feat(rule-engine): add fuel surcharge rate trigger and cargo flag
This commit is contained in:
@@ -121,15 +121,16 @@ export class RatesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rates sold per direction + route. Base freight always; customs clearance
|
||||
* and empty-container return are the surcharges that are too — their fee
|
||||
* depends on the lane (and, for returns, the container type).
|
||||
* Rates sold per direction + route. Base freight always; customs clearance,
|
||||
* empty-container return and fuel are the surcharges that are too — their
|
||||
* fee depends on the lane (and, for returns, the container type).
|
||||
*/
|
||||
private isRouteScoped(appliesTo: Rate['appliesTo'], trigger: Rate['trigger']): boolean {
|
||||
return (
|
||||
this.isBaseFreight(appliesTo, trigger) ||
|
||||
trigger === 'CUSTOMS_CLEARANCE' ||
|
||||
trigger === 'WITH_RETURN'
|
||||
trigger === 'WITH_RETURN' ||
|
||||
trigger === 'FUEL'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,7 +161,9 @@ export class RatesService {
|
||||
appliesTo: Rate['appliesTo'],
|
||||
tradeDirection: string | null,
|
||||
): { origin: YardCountry; destination: YardCountry } {
|
||||
if (appliesTo === 'INTERCITY') {
|
||||
// DOMESTIC only reaches here on a FUEL rate's intercity lane — it stays
|
||||
// inside Ethiopia exactly like intercity base freight.
|
||||
if (appliesTo === 'INTERCITY' || tradeDirection === 'DOMESTIC') {
|
||||
return { origin: YardCountry.ETHIOPIA, destination: YardCountry.ETHIOPIA };
|
||||
}
|
||||
return tradeDirection === 'EXPORT'
|
||||
@@ -291,6 +294,31 @@ export class RatesService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (trigger === 'FUEL') {
|
||||
// Fuel is sold per lane + commodity: the direction says which countries
|
||||
// the leg spans (DOMESTIC = intercity, inside Ethiopia) and the cargo
|
||||
// type names the commodity — different commodities price differently.
|
||||
if (
|
||||
tradeDirection !== 'IMPORT' &&
|
||||
tradeDirection !== 'EXPORT' &&
|
||||
tradeDirection !== 'DOMESTIC'
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'A fuel rate must say whether it covers IMPORT, EXPORT or DOMESTIC (intercity).',
|
||||
);
|
||||
}
|
||||
if (containerTypeId) {
|
||||
throw new BadRequestException(
|
||||
'A fuel rate cannot be scoped to a container type.',
|
||||
);
|
||||
}
|
||||
if (!cargoTypeId) {
|
||||
throw new BadRequestException(
|
||||
'A fuel rate must name the cargo type it covers.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (trigger === 'WITH_RETURN') {
|
||||
// Returning the empty box only exists on imports (the box goes back to
|
||||
// the port) — export return rates are rejected until the business sells
|
||||
@@ -502,15 +530,21 @@ export class RatesService {
|
||||
: (dto.containerTypeId ?? null);
|
||||
const cargoTypeId =
|
||||
(trigger === 'CUSTOMS_CLEARANCE' && cargoKind === 'BULK') ||
|
||||
trigger === 'LASHING'
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL'
|
||||
? (dto.cargoTypeId ?? null)
|
||||
: isSurcharge
|
||||
? null
|
||||
: (dto.cargoTypeId ?? null);
|
||||
// Intercity never leaves Ethiopia, so it has no trade direction to store —
|
||||
// its yard pair already says where it runs.
|
||||
// its yard pair already says where it runs. (Fuel is the exception: its
|
||||
// intercity lane is stored as DOMESTIC, since appliesTo = OTHER says
|
||||
// nothing about the direction.)
|
||||
const tradeDirection =
|
||||
trigger === 'CUSTOMS_CLEARANCE' || trigger === 'WITH_RETURN' || trigger === 'LASHING'
|
||||
trigger === 'CUSTOMS_CLEARANCE' ||
|
||||
trigger === 'WITH_RETURN' ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL'
|
||||
? (dto.tradeDirection ?? null)
|
||||
: isSurcharge || appliesTo === 'INTERCITY'
|
||||
? null
|
||||
@@ -563,6 +597,8 @@ export class RatesService {
|
||||
await this.assertNoBandOverlap({ rateUnit, containerTypeId, minKm, maxKm });
|
||||
}
|
||||
|
||||
const baseLiters = this.resolveBaseLiters(rateUnit, dto.baseLiters);
|
||||
|
||||
await this.assertNoDuplicatePattern({
|
||||
rateType,
|
||||
...(this.resolvesSingleRate(appliesTo, trigger) ? {} : { rateUnit }),
|
||||
@@ -588,6 +624,7 @@ export class RatesService {
|
||||
currency: appliesTo === 'LAST_MILE' ? (dto.currency ?? 'ETB') : 'USD',
|
||||
rateValue: dto.rateValue,
|
||||
rateUnit,
|
||||
baseLiters,
|
||||
minKm,
|
||||
maxKm,
|
||||
status: 'DRAFT',
|
||||
@@ -595,6 +632,25 @@ export class RatesService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The liters a PER_LITER fuel rate bills (price = baseLiters × rateValue,
|
||||
* once per booking). Required there; cleared on every other rate shape —
|
||||
* a PER_WAGON fuel rate bills wagons × rateValue and carries none.
|
||||
*/
|
||||
private resolveBaseLiters(
|
||||
rateUnit: Rate['rateUnit'],
|
||||
baseLiters?: number | null,
|
||||
): number | null {
|
||||
if (rateUnit !== 'PER_LITER') return null;
|
||||
const liters = Number(baseLiters);
|
||||
if (!(liters > 0)) {
|
||||
throw new BadRequestException(
|
||||
'A per-liter fuel rate needs a base liters amount — the price is base liters × rate value.',
|
||||
);
|
||||
}
|
||||
return liters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a DRAFT rate in place. Nothing prices off a draft, so a direct edit
|
||||
* is safe. A LIVE rate cannot take this path — see `applyApprovedUpdate`.
|
||||
@@ -680,14 +736,18 @@ export class RatesService {
|
||||
const keepsCargoType =
|
||||
!isSurcharge ||
|
||||
(trigger === 'CUSTOMS_CLEARANCE' && cargoKind === 'BULK') ||
|
||||
trigger === 'LASHING';
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL';
|
||||
const cargoTypeId = !keepsCargoType
|
||||
? null
|
||||
: dto.cargoTypeId !== undefined
|
||||
? dto.cargoTypeId
|
||||
: existing.cargoTypeId;
|
||||
const tradeDirection =
|
||||
trigger === 'CUSTOMS_CLEARANCE' || trigger === 'WITH_RETURN' || trigger === 'LASHING'
|
||||
trigger === 'CUSTOMS_CLEARANCE' ||
|
||||
trigger === 'WITH_RETURN' ||
|
||||
trigger === 'LASHING' ||
|
||||
trigger === 'FUEL'
|
||||
? dto.tradeDirection !== undefined
|
||||
? dto.tradeDirection
|
||||
: existing.tradeDirection
|
||||
@@ -788,6 +848,11 @@ export class RatesService {
|
||||
ignoreId: id,
|
||||
});
|
||||
|
||||
updates.baseLiters = this.resolveBaseLiters(
|
||||
rateUnit,
|
||||
dto.baseLiters !== undefined ? dto.baseLiters : existing.baseLiters,
|
||||
);
|
||||
|
||||
updates.currency =
|
||||
appliesTo === 'LAST_MILE'
|
||||
? (dto.currency ?? existing.currency ?? 'ETB')
|
||||
|
||||
Reference in New Issue
Block a user