mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 14:38:12 +00:00
fix train build issue
This commit is contained in:
@@ -508,12 +508,12 @@ export class RuleEngineService {
|
||||
|
||||
if (input.tradeDirection === 'IMPORT') {
|
||||
appliedModifiers.push(
|
||||
...this.derivedImportOverweight(
|
||||
...(await this.derivedImportOverweight(
|
||||
input,
|
||||
containerWeightResults,
|
||||
lineMaxVgmTons,
|
||||
liveRates,
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -540,23 +540,37 @@ export class RuleEngineService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Import overweight — derived, never configured. Each overweight container
|
||||
* line bills its excess tons at (its own base import freight on the booking's
|
||||
* route) ÷ (2 × its weight limit): 20ft at 1000 USD with a 20 t limit →
|
||||
* 25 USD per excess ton. Export keeps the configured OVERWEIGHT rate.
|
||||
* Note: derives from the LIVE route rate even for frozen-rate contract
|
||||
* bookings — the frozen snapshot has no route-scoped container price to
|
||||
* divide.
|
||||
* Import overweight — derived, never configured. Excess tons are billed on a
|
||||
* PER-WAGON basis: (the wagon's base import freight on the booking's route)
|
||||
* ÷ (2 × the container's weight limit).
|
||||
*
|
||||
* The rate is normalised to a wagon before dividing, because a 20ft rate
|
||||
* quoted PER_CONTAINER prices only HALF a wagon — two 20ft ride one wagon —
|
||||
* while a 40ft container IS the whole wagon. So a PER_CONTAINER 20ft rate is
|
||||
* doubled first; 40ft (and any rate already quoted PER_WAGON) is taken as is:
|
||||
* - 20ft PER_CONTAINER 845 USD, 20 t limit → (845 × 2) / (2 × 20) = 42.25
|
||||
* - 40ft PER_CONTAINER 1676 USD, 40 t limit → 1676 / (2 × 40) = 20.95
|
||||
* Halving over 2 × the limit keeps the original meaning: filling one wagon's
|
||||
* worth of excess costs one extra wagon of freight.
|
||||
*
|
||||
* Export keeps the configured OVERWEIGHT rate. Note: derives from the LIVE
|
||||
* route rate even for frozen-rate contract bookings — the frozen snapshot has
|
||||
* no route-scoped container price to divide.
|
||||
*/
|
||||
private derivedImportOverweight(
|
||||
private async derivedImportOverweight(
|
||||
input: BookingEvaluationInput,
|
||||
weightResults: ContainerWeightResult[],
|
||||
lineMaxVgmTons: Array<number | null>,
|
||||
liveRates: Rate[],
|
||||
): AppliedCargoModifier[] {
|
||||
): Promise<AppliedCargoModifier[]> {
|
||||
const modifiers: AppliedCargoModifier[] = [];
|
||||
if (!input.originYardId || !input.destinationYardId) return modifiers;
|
||||
|
||||
// How many of each container type ride one wagon: a 40ft fills a wagon,
|
||||
// two 20ft share one. Keyed by container type so a PER_CONTAINER rate can
|
||||
// be scaled up to the wagon the overweight formula prices against.
|
||||
const sizeByTypeId = await this.containersPerWagonByTypeId(weightResults);
|
||||
|
||||
for (let i = 0; i < weightResults.length; i++) {
|
||||
const wr = weightResults[i];
|
||||
const excess = Number(wr?.overweightExcessTons ?? 0);
|
||||
@@ -578,7 +592,16 @@ export class RuleEngineService {
|
||||
// No base rate → the base-freight line hard-blocks this booking anyway.
|
||||
if (!base) continue;
|
||||
|
||||
const perTon = Number(base.rateValue) / (2 * maxVgm);
|
||||
// Normalise the rate to ONE WAGON before dividing. A PER_CONTAINER 20ft
|
||||
// rate covers half a wagon, so it is scaled by the 2 containers that ride
|
||||
// one; 40ft scales by 1. A rate already quoted PER_WAGON is the wagon
|
||||
// price already — never scale it again.
|
||||
const perWagonRate =
|
||||
base.rateUnit === 'PER_CONTAINER'
|
||||
? Number(base.rateValue) * (sizeByTypeId.get(wr.containerTypeId) ?? 1)
|
||||
: Number(base.rateValue);
|
||||
|
||||
const perTon = perWagonRate / (2 * maxVgm);
|
||||
const amount = excess * perTon;
|
||||
if (!(amount > 0)) continue;
|
||||
|
||||
@@ -595,6 +618,42 @@ export class RuleEngineService {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Containers of each type that ride ONE wagon, derived from the type's
|
||||
* size_ft against a 40ft wagon slot: 20ft → 2, 40ft → 1. Only the types the
|
||||
* caller actually needs are looked up. Unknown or non-positive sizes fall
|
||||
* back to 1, which leaves a PER_CONTAINER rate unscaled — the pre-existing
|
||||
* behaviour, so a missing size can never inflate a bill.
|
||||
*/
|
||||
private async containersPerWagonByTypeId(
|
||||
weightResults: ContainerWeightResult[],
|
||||
): Promise<Map<string, number>> {
|
||||
const perWagon = new Map<string, number>();
|
||||
const ids = [...new Set(weightResults.map((w) => w.containerTypeId).filter(Boolean))];
|
||||
if (ids.length === 0) return perWagon;
|
||||
|
||||
let rows: Array<{ id: string; size_ft: string | number | null }> = [];
|
||||
try {
|
||||
rows = await this.dataSource.query(
|
||||
'SELECT id, size_ft FROM freight.container_types WHERE id = ANY($1)',
|
||||
[ids],
|
||||
);
|
||||
} catch {
|
||||
// Size lookup unavailable — fall back to an unscaled (×1) rate, the
|
||||
// behaviour before per-wagon normalisation. Never fail pricing over it.
|
||||
return perWagon;
|
||||
}
|
||||
const WAGON_SLOT_FT = 40;
|
||||
for (const row of rows) {
|
||||
const sizeFt = Number(row.size_ft ?? 0);
|
||||
perWagon.set(
|
||||
row.id,
|
||||
sizeFt > 0 ? Math.max(1, Math.floor(WAGON_SLOT_FT / sizeFt)) : 1,
|
||||
);
|
||||
}
|
||||
return perWagon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty-container return — sold per direction + route + container type, like
|
||||
* base freight. Each container line that opted in (returnQuantity, or every
|
||||
|
||||
Reference in New Issue
Block a user