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

@@ -48,6 +48,12 @@ export interface BookingContainerEvalInput {
hazardousQuantity?: number;
reeferQuantity?: number;
returnQuantity?: number;
/**
* Wagon fraction one container of this line occupies (40ft = 1, 20ft = 0.5).
* Lets a PER_WAGON empty-return rate bill the wagons the returned empties
* ride back on. Missing ⇒ one wagon per container.
*/
wagonsPerUnit?: number;
}
export interface BookingEvaluationInput {
@@ -86,6 +92,12 @@ export interface BookingEvaluationInput {
* container freight, which is scaled by container count instead.
*/
bulkTons?: number;
/**
* Wagons a BULK booking occupies (ceil(tons ÷ wagon capacity)), resolved by
* the pricing service. Scales PER_WAGON kind-scoped surcharges (lashing);
* 0/undefined when unknown — those charges then bill nothing.
*/
bulkWagons?: number;
containers: BookingContainerEvalInput[];
}
@@ -312,6 +324,9 @@ export class RuleEngineService {
// Empty-container return is sold per route + container type — billed by
// the route-matched block below, never by this route-agnostic loop.
if (rate.trigger === 'WITH_RETURN') continue;
// Lashing is sold per cargo kind + container type — billed by the
// kind-aware block below, never by this generic loop.
if (rate.trigger === 'LASHING') continue;
const triggered = this.matchesTrigger(rate.trigger, {
isHazardous: input.isHazardous,
hasReefer,
@@ -419,6 +434,10 @@ export class RuleEngineService {
appliedModifiers.push(...withReturn.modifiers);
hardBlocked.push(...withReturn.blocked);
if (hasLashing) {
appliedModifiers.push(...this.lashingCharges(input, liveRates));
}
return {
priorityScore,
appliedModifiers,
@@ -538,12 +557,18 @@ export class RuleEngineService {
}
const rateValue = Number(rate.rateValue);
const amount = rate.rateUnit === 'FLAT' ? rateValue : qty * rateValue;
// PER_WAGON bills the wagons the returned empties occupy (two 20ft share
// one wagon), PER_CONTAINER the boxes themselves, FLAT once per line.
const billed =
rate.rateUnit === 'PER_WAGON'
? Math.ceil(qty * (container.wagonsPerUnit ?? 1))
: qty;
const amount = rate.rateUnit === 'FLAT' ? rateValue : billed * rateValue;
if (!(amount > 0)) continue;
modifiers.push({
rateId: rate.id,
surchargeCode: this.surchargeCode(rate),
triggerValue: qty,
triggerValue: rate.rateUnit === 'FLAT' ? qty : billed,
calculatedAmount: amount,
currency: rate.currency,
unitPriceUsd: rateValue,
@@ -555,6 +580,70 @@ export class RuleEngineService {
return { modifiers, blocked: [...new Set(blocked)] };
}
/**
* Cargo securing / lashing — sold per cargo kind, like the customs clearance
* fee. Container bookings bill each container line at its own container
* type's lashing rate (PER_CONTAINER × boxes, or PER_WAGON × the wagons the
* line occupies); bulk bookings bill the type-less rate (PER_TON × tonnage,
* or PER_WAGON × the wagons the bulk occupies). An unconfigured rate simply
* bills nothing — same leniency as hazard/reefer.
*/
private lashingCharges(
input: BookingEvaluationInput,
liveRates: Rate[],
): AppliedCargoModifier[] {
const modifiers: AppliedCargoModifier[] = [];
const lashingRates = liveRates.filter(
(r) => r.trigger === 'LASHING' && r.currency === 'USD',
);
if (lashingRates.length === 0) return modifiers;
const push = (rate: Rate, billedQty: number): void => {
const rateValue = Number(rate.rateValue);
const amount =
rate.rateUnit === 'FLAT' ? rateValue : billedQty * rateValue;
if (!(amount > 0)) return;
modifiers.push({
rateId: rate.id,
surchargeCode: this.surchargeCode(rate),
triggerValue: rate.rateUnit === 'FLAT' ? 1 : billedQty,
calculatedAmount: amount,
currency: rate.currency,
unitPriceUsd: rateValue,
billingUnit: rate.rateUnit,
});
};
if (input.containers.length > 0) {
for (const container of input.containers) {
const qty = Number(container.quantity || 0);
if (!(qty > 0)) continue;
const rate = lashingRates.find(
(r) => r.containerTypeId === container.containerTypeId,
);
if (!rate) continue;
const billedQty =
rate.rateUnit === 'PER_WAGON'
? Math.ceil(qty * (container.wagonsPerUnit ?? 1))
: qty;
push(rate, billedQty);
}
return modifiers;
}
// Bulk — the type-less rate covers the whole booking.
const rate = lashingRates.find((r) => !r.containerTypeId);
if (!rate) return modifiers;
const billedQty =
rate.rateUnit === 'PER_TON'
? Math.max(0, Number(input.bulkTons ?? 0))
: rate.rateUnit === 'PER_WAGON'
? Math.max(0, Number(input.bulkWagons ?? 0))
: 1;
push(rate, billedQty);
return modifiers;
}
/**
* Messages for container lines whose total weight exceeds the hard capacity
* ceiling (weight_limit_rules.max_capacity_tons). Non-empty ⇒ the booking