From 156fa9d2e408f3cd09f198273e92586f6be1dd3d Mon Sep 17 00:00:00 2001 From: Marshal Date: Sat, 15 Aug 2026 11:54:35 +0000 Subject: [PATCH] fix: improve handling of container sizes in wagon cancellation logic --- .../booking-wagon-cancellation.service.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/bookings/booking-wagon-cancellation.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-wagon-cancellation.service.ts index 418fbd5c6..0b4315d62 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-wagon-cancellation.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-wagon-cancellation.service.ts @@ -54,6 +54,10 @@ import { * fee scales with the cancelled wagon count and differs by what was booked. */ export const WAGON_CANCELLATION_FEE_RATE_TYPE = 'CANCELLATION_FEE'; + +/** `booking_container.container_size` is stored as "20ft"/"40ft" — `Number()` on it is NaN. */ +const sizeFtOf = (size: string | number | null | undefined): number => + parseInt(String(size ?? ''), 10); /** invoices.type of the fee invoice — the settlement branch key in BookingInvoiceService. */ export const WAGON_CANCEL_FEE_INVOICE_TYPE = 'WAGON_CANCEL_FEE'; @@ -603,11 +607,11 @@ export class BookingWagonCancellationService { const live = liveBySize.get(cut.containerSize) ?? 0; if (cut.quantity > live) { throw new BadRequestException( - `Cannot cancel ${cut.quantity} × ${cut.containerSize}ft — the booking only has ${live}.`, + `Cannot cancel ${cut.quantity} × ${sizeFtOf(cut.containerSize)}ft — the booking only has ${live}.`, ); } bySize[cut.containerSize] = cut.quantity; - wagons += cut.quantity * wagonsPerUnitForSize(Number(cut.containerSize)); + wagons += cut.quantity * wagonsPerUnitForSize(sizeFtOf(cut.containerSize)); } wagons = round2(wagons); if (wagons > totalWagons) { @@ -849,7 +853,7 @@ export class BookingWagonCancellationService { // wagon-space each size's units occupy, so the total always equals // cut.wagons (whole wagons on an allocation cut, fractional on a quantity cut). const bySize = Object.entries(cut.quantities.bySize ?? {}).filter(([, qty]) => qty > 0); - const spaceOf = ([size, qty]: [string, number]) => qty * wagonsPerUnitForSize(Number(size)); + const spaceOf = ([size, qty]: [string, number]) => qty * wagonsPerUnitForSize(sizeFtOf(size)); const totalSpace = bySize.reduce((s, e) => s + spaceOf(e), 0); if (!bySize.length || totalSpace <= 0) throw missing('containers'); const containerTypes = await this.dataSource.getRepository(ContainerType).find(); @@ -858,7 +862,7 @@ export class BookingWagonCancellationService { let currency = ''; for (const entry of bySize) { const [size] = entry; - const sizeFt = Number(size); + const sizeFt = sizeFtOf(size); const typeIds = new Set( containerTypes.filter((ct) => Number(ct.sizeFt) === sizeFt).map((ct) => ct.id), ); @@ -892,7 +896,7 @@ export class BookingWagonCancellationService { const live = lines.reduce((s, l) => s + Number(l.quantity ?? 0), 0); if (live < toDrop) { throw new BadRequestException( - `Booking changed since the request: only ${live} × ${size}ft left, cannot cancel ${toDrop}.`, + `Booking changed since the request: only ${live} × ${sizeFtOf(size)}ft left, cannot cancel ${toDrop}.`, ); } for (const line of lines) { @@ -936,7 +940,7 @@ export class BookingWagonCancellationService { }); await manager.getRepository(BookingContainer).update(line.id, { quantity: qty - drop, - wagonsRequired: round2((qty - drop) * wagonsPerUnitForSize(Number(size))), + wagonsRequired: round2((qty - drop) * wagonsPerUnitForSize(sizeFtOf(size))), totalVgmTons: round3(Number(line.totalVgmTons) - droppedVgm), hazardousQuantity: keptUnits.filter((u) => u.isHazardous).length, reeferQuantity: keptUnits.filter((u) => u.isReefer).length, @@ -1024,7 +1028,7 @@ export class BookingWagonCancellationService { const doomedVgm = round3(doomed.reduce((s, u) => s + Number(u.vgmTons || 0), 0)); await manager.getRepository(BookingContainer).update(line.id, { quantity: kept.length, - wagonsRequired: round2(kept.length * wagonsPerUnitForSize(Number(size))), + wagonsRequired: round2(kept.length * wagonsPerUnitForSize(sizeFtOf(size))), totalVgmTons: round3(Number(line.totalVgmTons) - doomedVgm), hazardousQuantity: kept.filter((u) => u.isHazardous).length, reeferQuantity: kept.filter((u) => u.isReefer).length,