mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-05 23:33:38 +00:00
fix train build issue
This commit is contained in:
@@ -508,12 +508,12 @@ export class RuleEngineService {
|
|||||||
|
|
||||||
if (input.tradeDirection === 'IMPORT') {
|
if (input.tradeDirection === 'IMPORT') {
|
||||||
appliedModifiers.push(
|
appliedModifiers.push(
|
||||||
...this.derivedImportOverweight(
|
...(await this.derivedImportOverweight(
|
||||||
input,
|
input,
|
||||||
containerWeightResults,
|
containerWeightResults,
|
||||||
lineMaxVgmTons,
|
lineMaxVgmTons,
|
||||||
liveRates,
|
liveRates,
|
||||||
),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,23 +540,37 @@ export class RuleEngineService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import overweight — derived, never configured. Each overweight container
|
* Import overweight — derived, never configured. Excess tons are billed on a
|
||||||
* line bills its excess tons at (its own base import freight on the booking's
|
* PER-WAGON basis: (the wagon's base import freight on the booking's route)
|
||||||
* route) ÷ (2 × its weight limit): 20ft at 1000 USD with a 20 t limit →
|
* ÷ (2 × the container's weight limit).
|
||||||
* 25 USD per excess ton. Export keeps the configured OVERWEIGHT rate.
|
*
|
||||||
* Note: derives from the LIVE route rate even for frozen-rate contract
|
* The rate is normalised to a wagon before dividing, because a 20ft rate
|
||||||
* bookings — the frozen snapshot has no route-scoped container price to
|
* quoted PER_CONTAINER prices only HALF a wagon — two 20ft ride one wagon —
|
||||||
* divide.
|
* 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,
|
input: BookingEvaluationInput,
|
||||||
weightResults: ContainerWeightResult[],
|
weightResults: ContainerWeightResult[],
|
||||||
lineMaxVgmTons: Array<number | null>,
|
lineMaxVgmTons: Array<number | null>,
|
||||||
liveRates: Rate[],
|
liveRates: Rate[],
|
||||||
): AppliedCargoModifier[] {
|
): Promise<AppliedCargoModifier[]> {
|
||||||
const modifiers: AppliedCargoModifier[] = [];
|
const modifiers: AppliedCargoModifier[] = [];
|
||||||
if (!input.originYardId || !input.destinationYardId) return modifiers;
|
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++) {
|
for (let i = 0; i < weightResults.length; i++) {
|
||||||
const wr = weightResults[i];
|
const wr = weightResults[i];
|
||||||
const excess = Number(wr?.overweightExcessTons ?? 0);
|
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.
|
// No base rate → the base-freight line hard-blocks this booking anyway.
|
||||||
if (!base) continue;
|
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;
|
const amount = excess * perTon;
|
||||||
if (!(amount > 0)) continue;
|
if (!(amount > 0)) continue;
|
||||||
|
|
||||||
@@ -595,6 +618,42 @@ export class RuleEngineService {
|
|||||||
return modifiers;
|
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
|
* Empty-container return — sold per direction + route + container type, like
|
||||||
* base freight. Each container line that opted in (returnQuantity, or every
|
* base freight. Each container line that opted in (returnQuantity, or every
|
||||||
|
|||||||
Reference in New Issue
Block a user