implement Global Logistics booking process for customs contracts and enhance contract document handling

This commit is contained in:
Marshal
2026-06-29 08:08:10 +00:00
parent 4be4cc9054
commit aeb5e0046e
13 changed files with 436 additions and 104 deletions

View File

@@ -0,0 +1,132 @@
import type { Freight } from "@edr/types";
export interface GlShipmentTotalLine {
label: string;
unitPrice: number;
unit: Freight.ContractRateUnit | string;
quantity: number;
amount: number;
}
export interface GlShipmentTotal {
currency: string;
lines: GlShipmentTotalLine[];
total: number;
}
/** A normalized view of the form quantities, freight-shape agnostic. */
export interface GlShipmentQuantities {
isContainer: boolean;
/** Container lines: size + total qty + hazardous/reefer qty. */
containers: Array<{
containerSize: string;
quantity: number;
hazardousQuantity: number;
reeferQuantity: number;
}>;
/** Bulk: tons (or item count) + hazardous qty. */
bulkQuantity: number;
bulkHazardousQuantity: number;
}
/**
* Compute the booking total client-side from the contract's frozen unit rates ×
* the quantities GL enters. Mirrors the portal customer estimate
* (new-shipment-form/total.ts) — the server recomputes the authoritative total
* on submit. Shown in the price-confirm modal before GL books on behalf of the
* customer.
*/
export function computeGlShipmentTotal(
contract: Freight.IContract,
q: GlShipmentQuantities,
): GlShipmentTotal {
const breakdown = contract.pricingBreakdown;
const currency = breakdown?.currency ?? contract.paymentCurrency ?? "ETB";
const items = breakdown?.lineItems ?? [];
const lines: GlShipmentTotalLine[] = [];
const rateFor = (
predicate: (i: Freight.ContractUnitRateLineItem) => boolean,
) => items.find(predicate);
if (q.isContainer) {
let hazardTotalQty = 0;
let reeferTotalQty = 0;
for (const line of q.containers) {
const qty = line.quantity;
if (qty <= 0) continue;
const rate =
rateFor(
(i) =>
i.containerSize === line.containerSize &&
i.unit === "per_container" &&
!i.conditionalOn,
) ?? rateFor((i) => i.containerSize === line.containerSize);
if (rate) {
lines.push({
label: rate.label,
unitPrice: rate.unitPrice,
unit: rate.unit,
quantity: qty,
amount: rate.unitPrice * qty,
});
}
hazardTotalQty += line.hazardousQuantity;
reeferTotalQty += line.reeferQuantity;
}
if (contract.isHazardous && hazardTotalQty > 0) {
const hz = rateFor((i) => i.conditionalOn === "is_hazardous");
if (hz) {
lines.push({
label: hz.label,
unitPrice: hz.unitPrice,
unit: hz.unit,
quantity: hazardTotalQty,
amount: hz.unitPrice * hazardTotalQty,
});
}
}
if (contract.isReefer && reeferTotalQty > 0) {
const rf = rateFor((i) => i.conditionalOn === "is_reefer");
if (rf) {
lines.push({
label: rf.label,
unitPrice: rf.unitPrice,
unit: rf.unit,
quantity: reeferTotalQty,
amount: rf.unitPrice * reeferTotalQty,
});
}
}
} else {
const qty = q.bulkQuantity;
const rate =
rateFor((i) => i.unit === "per_ton" || i.unit === "per_item") ?? items[0];
if (rate && qty > 0) {
lines.push({
label: rate.label,
unitPrice: rate.unitPrice,
unit: rate.unit,
quantity: qty,
amount: rate.unitPrice * qty,
});
}
}
const total = lines.reduce((s, l) => s + l.amount, 0);
return { currency, lines, total };
}
/** Human-readable label for a contract unit-rate's charge unit. */
export function formatRateUnit(unit: Freight.ContractRateUnit | string): string {
const map: Record<string, string> = {
per_container: "container",
per_ton: "ton",
per_item: "item",
per_km: "km",
flat: "flat",
};
return map[unit] ?? unit.replace(/_/g, " ").replace(/^per /, "");
}