Files
edr-platform/apps/edr-freight-api/src/modules/bookings/container-pairing.util.ts
Marshal 35cb20da0b feat: implement 20ft container weight-pairing validation
- Added ContainerValidationService to handle 20ft weight-pairing logic.
- Introduced validate20ftWeightPairing utility function to check weight differences.
- Updated BookingPricingService to include overweight line details and pairing errors in price response.
- Enhanced BookingTransitionService to reject submissions with unpairable 20ft containers.
- Created ShipmentValidation interface for pre-submit validation of container contracts.
- Integrated shipment validation into the contract booking process, providing warnings for overweight containers and hard blocks for pairing errors.
- Updated front-end components to display validation results and prevent submission when errors are present.
2026-07-03 09:26:27 +00:00

65 lines
2.5 KiB
TypeScript

/**
* Booking-time 20ft weight-pairing rule.
*
* A container wagon holds two 20ft containers (2 TEU). When two 20ft ride the
* same wagon their gross-weight difference must not exceed `maxPairDiffTons`
* (global rule `max20ftPairWeightDiffTons`, default 10t) so the wagon load stays
* balanced. 40ft containers occupy a whole wagon alone and never pair.
*
* At booking time the customer enters every 20ft container's weight but not its
* wagon slot, so we auto-pair: sort the 20ft weights ascending and pair adjacent
* (0-1, 2-3, …). Adjacent pairing minimises the diff of every pair, so if ANY
* valid pairing exists this one finds it — a violation here means no balanced
* pairing is possible and the booking must be blocked. An odd leftover 20ft is
* fine: it has no partner in this booking and flows to consolidation.
*/
export interface Container20ftUnit {
/** Human label for messages, e.g. the container number. */
label: string;
grossWeightTons: number;
}
export interface PairingViolation {
message: string;
/** The two container labels whose pairing exceeds the diff cap. */
labels: [string, string];
diffTons: number;
}
const round2 = (n: number): number => Math.round(n * 100) / 100;
/**
* Validate that the given 20ft units can all be paired onto wagons within the
* weight-difference cap. Returns one violation per over-cap adjacent pair (empty
* when every wagon pair is balanced or there is nothing to pair). A single
* leftover unit (odd count) is not a violation.
*/
export function validate20ftWeightPairing(
units: Container20ftUnit[],
maxPairDiffTons: number,
): PairingViolation[] {
if (units.length < 2 || maxPairDiffTons == null) return [];
// Ascending by weight: adjacent pairs have the smallest possible diffs.
const sorted = [...units].sort((a, b) => a.grossWeightTons - b.grossWeightTons);
const violations: PairingViolation[] = [];
for (let i = 0; i + 1 < sorted.length; i += 2) {
const a = sorted[i];
const b = sorted[i + 1];
const diff = Math.abs(a.grossWeightTons - b.grossWeightTons);
if (diff > maxPairDiffTons) {
violations.push({
message:
`20ft containers ${a.label} (${round2(a.grossWeightTons)}T) and ` +
`${b.label} (${round2(b.grossWeightTons)}T) cannot share a wagon: ` +
`weight difference ${round2(diff)}T exceeds the ${maxPairDiffTons}T limit.`,
labels: [a.label, b.label],
diffTons: round2(diff),
});
}
}
return violations;
}