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. */
|
/** Reject submit when the booking's 20ft containers can't be balanced onto wagons. */
|
||||||
private async assert20ftPairable(booking: Booking): Promise<void> {
|
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 =
|
const violations =
|
||||||
await this.containerValidationService.validate20ftPairing(booking);
|
await this.containerValidationService.validate20ftPairing(booking);
|
||||||
if (violations.length) {
|
if (violations.length) {
|
||||||
|
|||||||
@@ -2458,6 +2458,22 @@ export class ContractBookingService {
|
|||||||
private async assert20ftPairableAtCreate(
|
private async assert20ftPairableAtCreate(
|
||||||
dto: CreateBookingUnderContractDto,
|
dto: CreateBookingUnderContractDto,
|
||||||
): Promise<void> {
|
): 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 ?? [])
|
const twentyFtUnits = (dto.containers ?? [])
|
||||||
.filter((line) => (line.containerSize ?? '').includes('20'))
|
.filter((line) => (line.containerSize ?? '').includes('20'))
|
||||||
.flatMap((line, lineIdx) =>
|
.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
|
// 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
|
// wagon: it is GL, not the customer, who links the two bookings. Anything else
|
||||||
// keeps the historical hard block on odd 20ft.
|
// 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,
|
// 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.
|
// once. GL can still switch it off — then odd is blocked exactly as before.
|
||||||
@@ -965,10 +970,11 @@ export default function GlCreateBookingForm() {
|
|||||||
!cargoDescriptionError
|
!cargoDescriptionError
|
||||||
: !bulkErrors.quantity && !bulkErrors.hazardous && !bulkErrors.reefer;
|
: !bulkErrors.quantity && !bulkErrors.hazardous && !bulkErrors.reefer;
|
||||||
|
|
||||||
// The unpaired 20ft container is resolved by the shared wagon, so with an
|
// Consolidation (sharing the wagon with another customer's odd booking) is
|
||||||
// active consolidation an odd total stops being a blocker; without one it
|
// built but switched off for now, so an odd 20ft total always blocks — the
|
||||||
// blocks exactly as before.
|
// shared wagon no longer resolves the unpaired container. Flip this back to
|
||||||
const oddBlocksSubmit = hasOdd20ft && !consolidationActive;
|
// `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
|
// 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.
|
// 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">
|
<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
|
<Alert
|
||||||
color="red"
|
color="red"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|||||||
@@ -172,8 +172,11 @@ export function Step8Review({
|
|||||||
values.cargoType === "container"
|
values.cargoType === "container"
|
||||||
? calcWagons(values.containers ?? [])
|
? calcWagons(values.containers ?? [])
|
||||||
: { hasOddUnit: false, ft20Wagons: 0 };
|
: { hasOddUnit: false, ft20Wagons: 0 };
|
||||||
const oddPairsViaCustoms = hasOdd20ft && Boolean(values.customsClearingEnabled);
|
// Consolidation (pairing an odd 20ft with another customer's odd booking) is
|
||||||
const oddBlocksSubmit = hasOdd20ft && !oddPairsViaCustoms;
|
// 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";
|
const isGeneralContract = values.bookingType === "general_contract";
|
||||||
// Both one-time and general contracts take the bulk amount from the cargo step
|
// 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];
|
contract.cargoScope?.[0];
|
||||||
const isPerItem = bulkScope?.cargoType?.unitOfMeasure === "PER_ITEM";
|
const isPerItem = bulkScope?.cargoType?.unitOfMeasure === "PER_ITEM";
|
||||||
|
|
||||||
// 20ft containers ride two per wagon. An odd total leaves one unpaired, which
|
// 20ft containers ride two per wagon, so an odd total leaves one unpaired and
|
||||||
// is allowed here: on a customs contract GL completes the booking and links it
|
// the request cannot be planned. Consolidation (pairing the odd container with
|
||||||
// to another customer's odd booking so the two share the wagon. The request is
|
// another customer's odd booking) is built but switched off for now, so an odd
|
||||||
// therefore informational only, not a block.
|
// request is blocked here rather than dead-ending downstream.
|
||||||
const ft20Requested = isContainer ? Number(qtyBySize["20ft"]) || 0 : 0;
|
const ft20Requested = isContainer ? Number(qtyBySize["20ft"]) || 0 : 0;
|
||||||
const hasOdd20ft = ft20Requested % 2 === 1;
|
const hasOdd20ft = ft20Requested % 2 === 1;
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
if (hasOdd20ft) return;
|
||||||
const dto: Freight.CreateBookingRequestDto = {
|
const dto: Freight.CreateBookingRequestDto = {
|
||||||
contractRouteId: route?.id,
|
contractRouteId: route?.id,
|
||||||
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
|
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
|
||||||
@@ -220,17 +221,17 @@ export default function NewShipmentRequestPage() {
|
|||||||
|
|
||||||
{hasOdd20ft ? (
|
{hasOdd20ft ? (
|
||||||
<Alert
|
<Alert
|
||||||
color="blue"
|
color="red"
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
icon={<AlertCircle size={16} />}
|
icon={<AlertCircle size={16} />}
|
||||||
title={`Odd number of 20ft containers (${ft20Requested})`}
|
title={`Odd number of 20ft containers (${ft20Requested})`}
|
||||||
>
|
>
|
||||||
<Text fz={13}>
|
<Text fz={13}>
|
||||||
20ft containers travel two per wagon, so one of yours will
|
20ft containers travel two per wagon, so they must be requested
|
||||||
share a wagon with another shipment. Global Logistics arranges
|
in even numbers. Please add one more 20ft container or remove
|
||||||
the pairing when completing your booking — you are billed only
|
one (e.g. request {ft20Requested + 1} or {ft20Requested - 1}{" "}
|
||||||
for your own containers.
|
instead of {ft20Requested}).
|
||||||
</Text>
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -281,6 +282,7 @@ export default function NewShipmentRequestPage() {
|
|||||||
leftSection={<Send size={16} />}
|
leftSection={<Send size={16} />}
|
||||||
loading={submit.isPending}
|
loading={submit.isPending}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
disabled={hasOdd20ft}
|
||||||
>
|
>
|
||||||
Submit shipment request
|
Submit shipment request
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user