mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -81,6 +81,28 @@ export class BookingTransitionService {
|
||||
|
||||
/** Reject submit when the booking's 20ft containers can't be balanced onto wagons. */
|
||||
private async assert20ftPairable(booking: Booking): Promise<void> {
|
||||
// Parity gate. 20ft ride two per wagon, so an odd total leaves one container
|
||||
// that cannot be placed. Consolidation (pairing it with another customer's
|
||||
// odd booking) is built end to end but switched off for now, so an odd total
|
||||
// is rejected here rather than parked for a partner.
|
||||
// containerSize is not always populated (some rows carry only the container
|
||||
// type), so fall back to the type's sizeFt rather than silently skipping
|
||||
// those lines and letting an odd booking through.
|
||||
const ft20Quantity = (booking.bookingContainers ?? [])
|
||||
.filter((bc) =>
|
||||
bc.containerSize
|
||||
? bc.containerSize.includes("20")
|
||||
: Number(bc.containerType?.sizeFt) === 20,
|
||||
)
|
||||
.reduce((sum, bc) => sum + Number(bc.quantity || 0), 0);
|
||||
if (ft20Quantity % 2 === 1) {
|
||||
throw new BadRequestException(
|
||||
`20ft containers travel two per wagon, so they must be booked in even ` +
|
||||
`numbers. This booking has ${ft20Quantity} — add one more or remove ` +
|
||||
`one (book ${ft20Quantity + 1} or ${ft20Quantity - 1}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const violations =
|
||||
await this.containerValidationService.validate20ftPairing(booking);
|
||||
if (violations.length) {
|
||||
|
||||
@@ -2458,6 +2458,22 @@ export class ContractBookingService {
|
||||
private async assert20ftPairableAtCreate(
|
||||
dto: CreateBookingUnderContractDto,
|
||||
): Promise<void> {
|
||||
// Parity gate. 20ft containers ride two per wagon, so an odd total leaves
|
||||
// one container that cannot be placed. Consolidation (pairing it with
|
||||
// another customer's odd booking) is built end to end but switched off for
|
||||
// now, so an odd total is rejected outright — server-side, because the
|
||||
// frontend block alone is not a guarantee.
|
||||
const ft20Quantity = (dto.containers ?? [])
|
||||
.filter((line) => (line.containerSize ?? '').includes('20'))
|
||||
.reduce((sum, line) => sum + Number(line.quantity || 0), 0);
|
||||
if (ft20Quantity % 2 === 1) {
|
||||
throw new BadRequestException(
|
||||
`20ft containers travel two per wagon, so they must be booked in even ` +
|
||||
`numbers. This booking has ${ft20Quantity} — add one more or remove ` +
|
||||
`one (book ${ft20Quantity + 1} or ${ft20Quantity - 1}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const twentyFtUnits = (dto.containers ?? [])
|
||||
.filter((line) => (line.containerSize ?? '').includes('20'))
|
||||
.flatMap((line, lineIdx) =>
|
||||
|
||||
@@ -874,9 +874,14 @@ export default function GlCreateBookingForm() {
|
||||
// Only a customs (Path B) instance being COMPLETED by GL can use the shared
|
||||
// wagon: it is GL, not the customer, who links the two bookings. Anything else
|
||||
// keeps the historical hard block on odd 20ft.
|
||||
const oddConsolidationAvailable = Boolean(
|
||||
completeBookingId && isContainer && contract?.customsClearingEnabled,
|
||||
);
|
||||
//
|
||||
// Switched OFF for now: consolidation is built end to end (toggle, parent
|
||||
// picker, split entry, paired pricing, approval gate) but not in use, so an
|
||||
// odd 20ft total is rejected outright instead of offering the shared wagon.
|
||||
// Drop the `false &&` to bring the whole flow back.
|
||||
const oddConsolidationAvailable =
|
||||
false &&
|
||||
Boolean(completeBookingId && isContainer && contract?.customsClearingEnabled);
|
||||
|
||||
// Auto-on: entering an odd 20ft total opens the consolidation panel by itself,
|
||||
// once. GL can still switch it off — then odd is blocked exactly as before.
|
||||
@@ -965,10 +970,11 @@ export default function GlCreateBookingForm() {
|
||||
!cargoDescriptionError
|
||||
: !bulkErrors.quantity && !bulkErrors.hazardous && !bulkErrors.reefer;
|
||||
|
||||
// The unpaired 20ft container is resolved by the shared wagon, so with an
|
||||
// active consolidation an odd total stops being a blocker; without one it
|
||||
// blocks exactly as before.
|
||||
const oddBlocksSubmit = hasOdd20ft && !consolidationActive;
|
||||
// Consolidation (sharing the wagon with another customer's odd booking) is
|
||||
// built but switched off for now, so an odd 20ft total always blocks — the
|
||||
// shared wagon no longer resolves the unpaired container. Flip this back to
|
||||
// `hasOdd20ft && !consolidationActive` to re-enable the shared-wagon path.
|
||||
const oddBlocksSubmit = hasOdd20ft;
|
||||
|
||||
// Partner side: a linked partner must be picked, carry an odd 20ft count of
|
||||
// its own (odd + odd = even fills the wagon) and have complete unit details.
|
||||
@@ -2168,7 +2174,23 @@ export default function GlCreateBookingForm() {
|
||||
}}
|
||||
>
|
||||
<Box maw={896} mx="auto">
|
||||
{showErrors && !formValid ? (
|
||||
{/* The review button is disabled on an odd 20ft total, so the click
|
||||
that would surface the errors never lands — state the reason here
|
||||
rather than leaving it in a tooltip nobody hovers. */}
|
||||
{oddBlocksSubmit ? (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertCircle size={16} />}
|
||||
mb="sm"
|
||||
title={`Odd number of 20ft containers (${ft20Total})`}
|
||||
>
|
||||
20ft containers travel two per wagon, so they must be booked in
|
||||
even numbers. Add one more 20ft container or remove one — book{" "}
|
||||
{ft20Total + 1} or {ft20Total - 1} instead of {ft20Total}.
|
||||
</Alert>
|
||||
) : showErrors && !formValid ? (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
|
||||
@@ -172,8 +172,11 @@ export function Step8Review({
|
||||
values.cargoType === "container"
|
||||
? calcWagons(values.containers ?? [])
|
||||
: { hasOddUnit: false, ft20Wagons: 0 };
|
||||
const oddPairsViaCustoms = hasOdd20ft && Boolean(values.customsClearingEnabled);
|
||||
const oddBlocksSubmit = hasOdd20ft && !oddPairsViaCustoms;
|
||||
// Consolidation (pairing an odd 20ft with another customer's odd booking) is
|
||||
// built but switched off for now, so an odd total blocks submit on every path
|
||||
// — customs included.
|
||||
const oddPairsViaCustoms = false;
|
||||
const oddBlocksSubmit = hasOdd20ft;
|
||||
|
||||
const isGeneralContract = values.bookingType === "general_contract";
|
||||
// Both one-time and general contracts take the bulk amount from the cargo step
|
||||
|
||||
@@ -106,14 +106,15 @@ export default function NewShipmentRequestPage() {
|
||||
contract.cargoScope?.[0];
|
||||
const isPerItem = bulkScope?.cargoType?.unitOfMeasure === "PER_ITEM";
|
||||
|
||||
// 20ft containers ride two per wagon. An odd total leaves one unpaired, which
|
||||
// is allowed here: on a customs contract GL completes the booking and links it
|
||||
// to another customer's odd booking so the two share the wagon. The request is
|
||||
// therefore informational only, not a block.
|
||||
// 20ft containers ride two per wagon, so an odd total leaves one unpaired and
|
||||
// the request cannot be planned. Consolidation (pairing the odd container with
|
||||
// another customer's odd booking) is built but switched off for now, so an odd
|
||||
// request is blocked here rather than dead-ending downstream.
|
||||
const ft20Requested = isContainer ? Number(qtyBySize["20ft"]) || 0 : 0;
|
||||
const hasOdd20ft = ft20Requested % 2 === 1;
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (hasOdd20ft) return;
|
||||
const dto: Freight.CreateBookingRequestDto = {
|
||||
contractRouteId: route?.id,
|
||||
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
|
||||
@@ -220,17 +221,17 @@ export default function NewShipmentRequestPage() {
|
||||
|
||||
{hasOdd20ft ? (
|
||||
<Alert
|
||||
color="blue"
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertCircle size={16} />}
|
||||
title={`Odd number of 20ft containers (${ft20Requested})`}
|
||||
>
|
||||
<Text fz={13}>
|
||||
20ft containers travel two per wagon, so one of yours will
|
||||
share a wagon with another shipment. Global Logistics arranges
|
||||
the pairing when completing your booking — you are billed only
|
||||
for your own containers.
|
||||
20ft containers travel two per wagon, so they must be requested
|
||||
in even numbers. Please add one more 20ft container or remove
|
||||
one (e.g. request {ft20Requested + 1} or {ft20Requested - 1}{" "}
|
||||
instead of {ft20Requested}).
|
||||
</Text>
|
||||
</Alert>
|
||||
) : null}
|
||||
@@ -281,6 +282,7 @@ export default function NewShipmentRequestPage() {
|
||||
leftSection={<Send size={16} />}
|
||||
loading={submit.isPending}
|
||||
onClick={handleSubmit}
|
||||
disabled={hasOdd20ft}
|
||||
>
|
||||
Submit shipment request
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user