mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Enhance wagon capacity handling and validation in booking service
- Added default capacities for container and bulk wagons. - Updated wagonsFor method to consider weight and length for wagon calculations. - Improved handling of overweight bookings with appropriate warnings. - Adjusted tests to reflect changes in wagon capacity and validation logic.
This commit is contained in:
@@ -29,8 +29,10 @@ import { BillingService } from "../billing/billing.service";
|
||||
|
||||
|
||||
import {
|
||||
DEFAULT_BULK_WAGON_CAPACITY_TONS,
|
||||
DEFAULT_BULK_WAGON_LENGTH_METERS,
|
||||
DEFAULT_BULK_WAGON_TARE_TONS,
|
||||
DEFAULT_CONTAINER_WAGON_CAPACITY_TONS,
|
||||
DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
||||
DEFAULT_CONTAINER_WAGON_TARE_TONS,
|
||||
DEFAULT_WAGONS_PER_BOOKING,
|
||||
@@ -73,8 +75,8 @@ interface RouteDayGroup {
|
||||
* its length on the train and the tare it adds to the locomotive's gross load.
|
||||
*/
|
||||
type WagonDims = {
|
||||
container: { lengthMeters: number; tareWeightTons: number };
|
||||
bulk: { lengthMeters: number; tareWeightTons: number };
|
||||
container: { lengthMeters: number; tareWeightTons: number; capacityTons: number };
|
||||
bulk: { lengthMeters: number; tareWeightTons: number; capacityTons: number };
|
||||
};
|
||||
|
||||
export type BatchBoardBookingState =
|
||||
@@ -1917,8 +1919,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
"PREPAID",
|
||||
);
|
||||
await this.notifier.payNow(booking, deadline);
|
||||
const reservedWagons = this.wagonsFor(booking, await this.loadWagonDims());
|
||||
this.logger.log(
|
||||
`[BATCH] RESERVED ${booking.reference} (${this.wagonsFor(booking)}w, ` +
|
||||
`[BATCH] RESERVED ${booking.reference} (${reservedWagons}w, ` +
|
||||
`priority ${booking.priorityScore ?? 0}) on schedule ${scheduleId} — ` +
|
||||
`pay by ${deadline.toISOString()}`,
|
||||
);
|
||||
@@ -2235,12 +2238,21 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const containers = (b: Booking): number =>
|
||||
(b.bookingContainers ?? []).reduce((sum, c) => sum + Number(c.quantity ?? 0), 0);
|
||||
const totalContainers = containers(primary) + containers(partner);
|
||||
const sharedWagons =
|
||||
totalContainers > 0
|
||||
? Math.ceil(totalContainers / MAX_TEU_SLOTS_PER_WAGON)
|
||||
: this.wagonsFor(primary) + this.wagonsFor(partner);
|
||||
const cargoTons =
|
||||
Number(primary.cargoTotalWeightVgm ?? 0) + Number(partner.cargoTotalWeightVgm ?? 0);
|
||||
|
||||
// Consolidation shares TEU slots, never rated payload: the pair still needs
|
||||
// enough wagons to carry its combined cargo, so the weight axis bounds the
|
||||
// shared count exactly as it bounds an individual booking's.
|
||||
const capacityTons = this.capacityFor(primary.freightType, wagonDims);
|
||||
const byWeight =
|
||||
cargoTons > 0 && capacityTons > 0 ? Math.ceil(cargoTons / capacityTons) : 0;
|
||||
const byLength =
|
||||
totalContainers > 0
|
||||
? Math.ceil(totalContainers / MAX_TEU_SLOTS_PER_WAGON)
|
||||
: this.wagonsFor(primary, wagonDims) + this.wagonsFor(partner, wagonDims);
|
||||
const sharedWagons = Math.max(byLength, byWeight);
|
||||
|
||||
return {
|
||||
wagons: sharedWagons,
|
||||
// Consolidation saves tare as well as slots: the pair rides `sharedWagons`
|
||||
@@ -2272,19 +2284,43 @@ export class BookingBatchService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
private wagonsFor(booking: Booking): number {
|
||||
/**
|
||||
* Wagons a booking occupies. Two axes bind independently and the booking needs
|
||||
* enough wagons to satisfy BOTH, so the count is the larger of:
|
||||
*
|
||||
* weight — ceil(cargoTons / wagonType.capacityTons), the rated payload
|
||||
* length — TEU geometry, two 20ft to a wagon (container bookings only)
|
||||
*
|
||||
* The weight axis was missing entirely. A BULK booking carries no container
|
||||
* lines, so `containerWagonsForLines` returned 0 and every bulk booking
|
||||
* collapsed to a single wagon no matter its tonnage — a 2590T fertilizer
|
||||
* booking counted as 1 wagon, and `needFor` then charged 1 tare instead of 37.
|
||||
* That under-reported the board and let the fill loop overbook the train.
|
||||
*/
|
||||
private wagonsFor(booking: Booking, wagonDims: WagonDims): number {
|
||||
if (booking.wagonsRequired && booking.wagonsRequired > 0) {
|
||||
return Math.ceil(booking.wagonsRequired);
|
||||
}
|
||||
// booking.wagonsRequired is NULL for most rows (only set on certain
|
||||
// scheduling paths). Derive from the container lines, TEU-aware: two 20ft
|
||||
// share one wagon (wagonsPerUnit = 0.5). The old fallback summed raw
|
||||
// container QUANTITY, so 20×20ft counted as 20 wagons instead of 10 and
|
||||
// wrongly filled the train.
|
||||
const fromContainers = containerWagonsForLines(
|
||||
booking.bookingContainers ?? [],
|
||||
);
|
||||
return Math.max(DEFAULT_WAGONS_PER_BOOKING, fromContainers);
|
||||
|
||||
// TEU-aware: two 20ft share one wagon (wagonsPerUnit = 0.5). The old fallback
|
||||
// summed raw container QUANTITY, so 20×20ft counted as 20 wagons, not 10.
|
||||
const byLength = containerWagonsForLines(booking.bookingContainers ?? []);
|
||||
|
||||
const capacityTons = this.capacityFor(booking.freightType, wagonDims);
|
||||
const cargoTons = Number(booking.cargoTotalWeightVgm ?? 0);
|
||||
const byWeight =
|
||||
cargoTons > 0 && capacityTons > 0 ? Math.ceil(cargoTons / capacityTons) : 0;
|
||||
|
||||
return Math.max(DEFAULT_WAGONS_PER_BOOKING, byLength, byWeight);
|
||||
}
|
||||
|
||||
private capacityFor(
|
||||
freightType: string | null | undefined,
|
||||
wagonDims: WagonDims,
|
||||
): number {
|
||||
return freightType === "BULK"
|
||||
? wagonDims.bulk.capacityTons
|
||||
: wagonDims.container.capacityTons;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2296,7 +2332,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
* 37-wagon box-wagon train read 2590T when it really weighed 3522T.
|
||||
*/
|
||||
private needFor(booking: Booking, wagonDims: WagonDims): Capacity {
|
||||
const wagons = this.wagonsFor(booking);
|
||||
const wagons = this.wagonsFor(booking, wagonDims);
|
||||
return {
|
||||
wagons,
|
||||
weightTons: bookingGrossWeightTons(
|
||||
@@ -2402,14 +2438,20 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
const nw5 = byCode.get("NW5");
|
||||
const cw3 = byCode.get("CW3");
|
||||
// capacityTons divides a bulk booking's cargo, so a 0 or missing rated payload
|
||||
// must fall back rather than yield an infinite wagon count.
|
||||
const payload = (value: number | undefined, fallback: number): number =>
|
||||
value && value > 0 ? value : fallback;
|
||||
return {
|
||||
container: {
|
||||
lengthMeters: nw5?.lengthMeters ?? DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
||||
tareWeightTons: nw5?.tareWeightTons ?? DEFAULT_CONTAINER_WAGON_TARE_TONS,
|
||||
capacityTons: payload(nw5?.capacityTons, DEFAULT_CONTAINER_WAGON_CAPACITY_TONS),
|
||||
},
|
||||
bulk: {
|
||||
lengthMeters: cw3?.lengthMeters ?? DEFAULT_BULK_WAGON_LENGTH_METERS,
|
||||
tareWeightTons: cw3?.tareWeightTons ?? DEFAULT_BULK_WAGON_TARE_TONS,
|
||||
capacityTons: payload(cw3?.capacityTons, DEFAULT_BULK_WAGON_CAPACITY_TONS),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user