mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 14:48:18 +00:00
Enhance bulk booking handling and wagon capacity calculations across services
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
@@ -13,6 +15,7 @@ import { DataSource, In } from 'typeorm';
|
||||
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { BookingPricingService } from '../bookings/booking-pricing.service';
|
||||
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
||||
import { formatRouteLabel } from '../routes/entities/route.entity';
|
||||
import { RouteMilestone } from '../routes/entities/route-milestone.entity';
|
||||
@@ -42,6 +45,7 @@ import {
|
||||
bookingGrossWeightTons,
|
||||
bookingTrainLengthMeters,
|
||||
deriveTrainCapacityFromLocomotive,
|
||||
sizePartialOfferWagons,
|
||||
trainHardCaps,
|
||||
wagonTypeDimensionsFromEntity,
|
||||
} from './train-capacity.util';
|
||||
@@ -239,6 +243,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
private readonly trainSchedulingService: TrainSchedulingService,
|
||||
private readonly billing: BillingService,
|
||||
private readonly bookingWindowGateway: BookingWindowGateway,
|
||||
@Inject(forwardRef(() => BookingPricingService))
|
||||
private readonly pricingService: BookingPricingService,
|
||||
|
||||
@Optional() private readonly milestoneService?: ClearanceMilestoneService,
|
||||
@Optional() private readonly splitService?: BookingSplitService,
|
||||
@@ -1097,6 +1103,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
const pool = await this.bookingsRepository.findBatchPool(scheduleId);
|
||||
// Same bulk re-score as fillRouteDayInternal — the legacy per-schedule fill
|
||||
// must rank bulk bookings by their wagon-derived priority too.
|
||||
await this.recomputeBulkPriorities(pool, wagonDims);
|
||||
this.resortPoolByPriority(pool);
|
||||
const units = this.groupConsolidatedPool(pool);
|
||||
let armed = false;
|
||||
let reservedThisPass = 0;
|
||||
@@ -1314,6 +1324,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
corridorYards,
|
||||
day,
|
||||
);
|
||||
// BULK bookings only get their real (wagon-derived) priority score now, at
|
||||
// batch time — stamp it and re-rank before the fill consumes the pool.
|
||||
await this.recomputeBulkPriorities(pool, wagonDims);
|
||||
this.resortPoolByPriority(pool);
|
||||
// Consolidated partners collapse into one atomic unit (both-or-neither); a
|
||||
// consolidated booking whose partner isn't ready this cycle is skipped.
|
||||
const units = this.groupConsolidatedPool(pool);
|
||||
@@ -1505,11 +1519,24 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const bulkCapacityTons = await this.loadBulkWagonCapacityTons();
|
||||
|
||||
// The wagon-slot axis alone under-constrains the offer. On a weight- or
|
||||
// length-limited train (slots to spare, but e.g. only 798T of pull weight
|
||||
// left) sizing by slots either produced an offer the fits() check below
|
||||
// rejected, or — when the free slots exceeded the booking's own wagon
|
||||
// count — sizeOffer refused outright, so a bulk booking on a weight-bound
|
||||
// train was never offered a split at all. Size across all three axes.
|
||||
const perWagon =
|
||||
booking.freightType === "BULK" ? wagonDims.bulk : wagonDims.container;
|
||||
const partial = sizePartialOfferWagons(budget, need.wagons, perWagon);
|
||||
if (!partial) return null;
|
||||
|
||||
const sized = await this.splitService.sizeOffer(
|
||||
booking,
|
||||
budget.wagons,
|
||||
partial.wagons,
|
||||
need.wagons,
|
||||
bulkCapacityTons,
|
||||
partial.maxCargoTons,
|
||||
);
|
||||
if (!sized) return null;
|
||||
|
||||
@@ -2284,6 +2311,56 @@ export class BookingBatchService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp real priority scores on the pool's BULK bookings before the batch
|
||||
* ranks it. Submit-time scoring runs with totalWagons = 0 for bulk (a bulk
|
||||
* booking has no container lines to carry a wagon count), so every
|
||||
* wagon-range priority config missed and bulk import bookings entered the
|
||||
* batch at score 0 — they were never prioritized. Their wagon footprint is
|
||||
* derivable from tonnage vs. live wagon capacity (wagonsFor), so the score
|
||||
* is computed here — when doc review closes and the batch runs — and
|
||||
* persisted so the priority board shows the same ranking. The pool arrives
|
||||
* SQL-ordered by the old scores; the caller must re-sort after this.
|
||||
*/
|
||||
private async recomputeBulkPriorities(
|
||||
pool: Booking[],
|
||||
wagonDims: WagonDims,
|
||||
): Promise<void> {
|
||||
for (const booking of pool) {
|
||||
if (booking.freightType !== 'BULK') continue;
|
||||
try {
|
||||
const wagons = this.wagonsFor(booking, wagonDims);
|
||||
const score = await this.pricingService.computeSubmitPriorityScore(
|
||||
booking,
|
||||
wagons,
|
||||
);
|
||||
if (Number(booking.priorityScore ?? 0) === score) continue;
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(booking.id, { priorityScore: score });
|
||||
booking.priorityScore = score;
|
||||
} catch (err) {
|
||||
// A failed recompute keeps the stored score — never blocks the batch.
|
||||
this.logger.warn(
|
||||
`Bulk priority recompute failed for ${booking.reference ?? booking.id}: ` +
|
||||
`${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore the batch pool ordering (mirrors findBatchPool's ORDER BY) after scores changed. */
|
||||
private resortPoolByPriority(pool: Booking[]): void {
|
||||
pool.sort(
|
||||
(a, b) =>
|
||||
Number(b.isGovernment) - Number(a.isGovernment) ||
|
||||
Number(b.priorityScore ?? 0) - Number(a.priorityScore ?? 0) ||
|
||||
(a.fullyExecutedAt?.getTime() ?? Infinity) -
|
||||
(b.fullyExecutedAt?.getTime() ?? Infinity) ||
|
||||
a.createdAt.getTime() - b.createdAt.getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wagons a booking occupies. Two axes bind independently and the booking needs
|
||||
* enough wagons to satisfy BOTH, so the count is the larger of:
|
||||
@@ -2298,9 +2375,14 @@ export class BookingBatchService implements OnModuleInit {
|
||||
* 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);
|
||||
}
|
||||
// Stored wagonsRequired is a candidate, never an early return: rows written
|
||||
// while sumWagonsRequired hardcoded BULK to 1 wagon are still in the DB, and
|
||||
// trusting them charged one tare for a whole bulk consist (a 700T booking on
|
||||
// 70T wagons read 700 + 1 tare instead of 700 + 10 tares).
|
||||
const stored =
|
||||
booking.wagonsRequired && booking.wagonsRequired > 0
|
||||
? Math.ceil(booking.wagonsRequired)
|
||||
: 0;
|
||||
|
||||
// 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.
|
||||
@@ -2311,7 +2393,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const byWeight =
|
||||
cargoTons > 0 && capacityTons > 0 ? Math.ceil(cargoTons / capacityTons) : 0;
|
||||
|
||||
return Math.max(DEFAULT_WAGONS_PER_BOOKING, byLength, byWeight);
|
||||
return Math.max(DEFAULT_WAGONS_PER_BOOKING, stored, byLength, byWeight);
|
||||
}
|
||||
|
||||
private capacityFor(
|
||||
|
||||
Reference in New Issue
Block a user