This commit is contained in:
Marshal
2026-07-23 14:18:24 +00:00
parent 15a6bab5d0
commit 127676055b
15 changed files with 362 additions and 160 deletions

View File

@@ -189,6 +189,36 @@ export class ContractPricingService {
});
}
}
// Lashing / cargo securing — BULK only, shown when the contract's commodity
// needs lashing (cargoType.hasLashing). The commodity-scoped rate for the
// contract's direction wins over the commodity-wide catch-all; billed at
// booking on the live rate (per ton / per wagon), this line is display.
if (contract.freightType === 'BULK') {
const scope = (contract.cargoScope ?? []).find((c) => c.cargoTypeId);
if (scope?.cargoType?.hasLashing) {
const onDirection = liveRates.filter(
(r) =>
r.trigger === 'LASHING' &&
r.currency === 'USD' &&
!r.containerTypeId &&
r.tradeDirection === contract.tradeDirection,
);
const lashing =
onDirection.find((r) => r.cargoTypeId === scope.cargoTypeId) ??
onDirection.find((r) => !r.cargoTypeId);
if (lashing && Number(lashing.rateValue) > 0) {
lineItems.push({
code: 'LASHING',
label: `Lashing / cargo securing (${scope.cargoType.cargoTypeName})`,
unit: toContractUnit(lashing.rateUnit),
unitPrice: convert(Number(lashing.rateValue)),
cargoTypeCode: scope.cargoType.code ?? null,
conditionalOn: 'has_lashing',
});
}
}
}
// Empty-container return service — container contracts only, toggled on the
// contract like hazard/reefer. Billed at booking per WITH_RETURN container.
if (
@@ -295,18 +325,26 @@ export class ContractPricingService {
});
}
} else {
// Bulk fee = the route's type-less customs rate (per ton / per wagon).
const rate = onLeg.find((r) => !r.containerTypeId);
// Bulk fee the rate scoped to the contract's commodity wins; a
// commodity-less rate (legacy) is the catch-all fallback.
const scope = (contract.cargoScope ?? []).find((c) => c.cargoTypeId);
const rate =
(scope?.cargoTypeId
? onLeg.find(
(r) => !r.containerTypeId && r.cargoTypeId === scope.cargoTypeId,
)
: undefined) ?? onLeg.find((r) => !r.containerTypeId && !r.cargoTypeId);
if (!rate || Number(rate.rateValue) <= 0) {
throw new UnprocessableEntityException(
'No bulk customs clearance service fee is configured for this direction and route. Ask the rates team to set a live bulk CUSTOMS_CLEARANCE rate for this origin → destination.',
'No bulk customs clearance service fee is configured for this cargo type on this direction and route. Ask the rates team to set a live bulk CUSTOMS_CLEARANCE rate for this commodity and origin → destination.',
);
}
lineItems.push({
code: 'CUSTOMS_CLEARANCE',
label: 'Customs clearance service (bulk)',
label: `Customs clearance service (${scope?.cargoType?.cargoTypeName ?? 'bulk'})`,
unit: toContractUnit(rate.rateUnit),
unitPrice: convert(Number(rate.rateValue)),
cargoTypeCode: scope?.cargoType?.code ?? null,
isClearance: true,
});
}