fix: improve handling of container sizes in wagon cancellation logic

This commit is contained in:
Marshal
2026-08-15 11:54:35 +00:00
parent b22f8c838e
commit 156fa9d2e4

View File

@@ -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,