|
|
|
|
@@ -31,7 +31,6 @@ import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity
|
|
|
|
|
import { TrainScheduleBooking } from '../train-schedules/entities/train-schedule-booking.entity';
|
|
|
|
|
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
|
|
|
|
|
import { TrainScheduleBookingsRepository } from '../train-schedules/train-schedule-bookings.repository';
|
|
|
|
|
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
|
|
|
|
import { BookingNotifierService } from './booking-notifier.service';
|
|
|
|
|
import { TrainSchedulingService } from './train-scheduling.service';
|
|
|
|
|
import { eatDay, groupBookingsIntoBoardWindows } from './batch-window.util';
|
|
|
|
|
@@ -568,7 +567,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
const required = need ?? this.needFor(booking, wagonDims);
|
|
|
|
|
let corridorMatched = false;
|
|
|
|
|
@@ -578,7 +576,7 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
);
|
|
|
|
|
const locomotive = schedule?.trainSet?.locomotive;
|
|
|
|
|
if (!schedule || !locomotive) continue;
|
|
|
|
|
const limits = await this.capacityLimits(locomotive, rules);
|
|
|
|
|
const limits = await this.capacityLimits(locomotive);
|
|
|
|
|
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
|
|
|
|
const leg = budget.legOf(booking.originYardId, booking.destinationYardId);
|
|
|
|
|
if (!leg) continue; // this train's route doesn't carry the booking's leg
|
|
|
|
|
@@ -757,7 +755,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const linkRepo = this.dataSource.getRepository(TrainScheduleBooking);
|
|
|
|
|
|
|
|
|
|
const board: BatchBoardSchedule[] = [];
|
|
|
|
|
@@ -787,7 +784,7 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
board.push(this.buildScheduleSummary(s, items, rules));
|
|
|
|
|
board.push(this.buildScheduleSummary(s, items));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
@@ -817,7 +814,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const linkRepo = this.dataSource.getRepository(TrainScheduleBooking);
|
|
|
|
|
const links = await linkRepo.find({ where: { trainScheduleId: s.id } });
|
|
|
|
|
const linkedIds = new Set(links.map((l) => l.bookingId));
|
|
|
|
|
@@ -1011,7 +1007,7 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null, rules),
|
|
|
|
|
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null),
|
|
|
|
|
counts: {
|
|
|
|
|
allocated: items.filter((i) => i.state === "ALLOCATED").length,
|
|
|
|
|
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
|
|
|
|
|
@@ -1045,9 +1041,10 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
/**
|
|
|
|
|
* Board capacity figures. `usedWeightTons` is GROSS (each item's weight already
|
|
|
|
|
* includes the tare of the wagons it occupies), so the ceiling it is measured
|
|
|
|
|
* against must be the same one the fill loop spends from: the locomotive floored
|
|
|
|
|
* by the global rule caps and widened by its overage tolerance. Reading the raw
|
|
|
|
|
* `loco.maxPullWeightTons` here showed staff a ceiling the batch engine did not use.
|
|
|
|
|
* against must be the same one the fill loop spends from: the locomotive's own
|
|
|
|
|
* limits widened by its overage tolerance (global rule caps do not apply, same
|
|
|
|
|
* as {@link capacityLimits}). Reading the raw `loco.maxPullWeightTons` here
|
|
|
|
|
* showed staff a ceiling the batch engine did not use.
|
|
|
|
|
*/
|
|
|
|
|
private computeBoardCapacity(
|
|
|
|
|
items: Array<{
|
|
|
|
|
@@ -1058,29 +1055,18 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
}>,
|
|
|
|
|
loco: Locomotive | null,
|
|
|
|
|
maxWagons: number | null,
|
|
|
|
|
rules: TrainSchedulingGlobalRules | null,
|
|
|
|
|
): BatchBoardSchedule["capacity"] {
|
|
|
|
|
const allocated = items.filter((i) => i.state === "ALLOCATED");
|
|
|
|
|
const committed = items.filter(
|
|
|
|
|
(i) => i.state === "ALLOCATED" || i.state === "SELECTED_FOR_BATCH",
|
|
|
|
|
);
|
|
|
|
|
const caps = loco
|
|
|
|
|
? trainHardCaps(
|
|
|
|
|
{
|
|
|
|
|
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
|
|
|
|
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
|
|
|
|
overageToleranceTons: Number(loco.overageToleranceTons) || 0,
|
|
|
|
|
overageToleranceMeters: Number(loco.overageToleranceMeters) || 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
maxTrainWeightTons: rules?.maxTrainWeightTons
|
|
|
|
|
? Number(rules.maxTrainWeightTons)
|
|
|
|
|
: undefined,
|
|
|
|
|
maxTrainLengthMeters: rules?.maxTrainLengthMeters
|
|
|
|
|
? Number(rules.maxTrainLengthMeters)
|
|
|
|
|
: undefined,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
? trainHardCaps({
|
|
|
|
|
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
|
|
|
|
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
|
|
|
|
overageToleranceTons: Number(loco.overageToleranceTons) || 0,
|
|
|
|
|
overageToleranceMeters: Number(loco.overageToleranceMeters) || 0,
|
|
|
|
|
})
|
|
|
|
|
: null;
|
|
|
|
|
const round2 = (value: number) => Math.round(value * 100) / 100;
|
|
|
|
|
|
|
|
|
|
@@ -1099,7 +1085,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
private buildScheduleSummary(
|
|
|
|
|
s: TrainSchedule,
|
|
|
|
|
items: BatchBoardBooking[],
|
|
|
|
|
rules: TrainSchedulingGlobalRules | null,
|
|
|
|
|
): BatchBoardSchedule {
|
|
|
|
|
const loco = s.trainSet?.locomotive ?? null;
|
|
|
|
|
|
|
|
|
|
@@ -1134,7 +1119,7 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null, rules),
|
|
|
|
|
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null),
|
|
|
|
|
counts: {
|
|
|
|
|
allocated: items.filter((i) => i.state === "ALLOCATED").length,
|
|
|
|
|
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
|
|
|
|
|
@@ -1203,10 +1188,9 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
const limits = await this.capacityLimits(locomotive, rules);
|
|
|
|
|
await this.syncScheduleMaxWagons(schedule, locomotive, rules);
|
|
|
|
|
const limits = await this.capacityLimits(locomotive);
|
|
|
|
|
await this.syncScheduleMaxWagons(schedule, locomotive);
|
|
|
|
|
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
|
|
|
|
const minPerWagon = this.minPerWagonNeed(wagonDims);
|
|
|
|
|
if (budget.isExhausted(minPerWagon)) {
|
|
|
|
|
@@ -1405,7 +1389,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
return { scheduleIds: [], commercialReserved: 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
|
|
|
|
|
// Live per-schedule corridor budget + arm flag, in departure order.
|
|
|
|
|
@@ -1420,8 +1403,8 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const limits = await this.capacityLimits(locomotive, rules);
|
|
|
|
|
await this.syncScheduleMaxWagons(schedule, locomotive, rules);
|
|
|
|
|
const limits = await this.capacityLimits(locomotive);
|
|
|
|
|
await this.syncScheduleMaxWagons(schedule, locomotive);
|
|
|
|
|
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
|
|
|
|
trains.push({ id, budget, armed: false });
|
|
|
|
|
}
|
|
|
|
|
@@ -1973,9 +1956,8 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
|
|
|
|
const locomotive = schedule?.trainSet?.locomotive;
|
|
|
|
|
if (!schedule || !locomotive) return null;
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
const limits = await this.capacityLimits(locomotive, rules);
|
|
|
|
|
const limits = await this.capacityLimits(locomotive);
|
|
|
|
|
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
|
|
|
|
return { budget, needFor: (booking) => this.needFor(booking, wagonDims) };
|
|
|
|
|
}
|
|
|
|
|
@@ -2520,11 +2502,12 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
* `base` via {@link needFor}, whose weight axis is gross. The locomotive's
|
|
|
|
|
* overage tolerance is returned separately — the corridor budget spends it
|
|
|
|
|
* only to admit a booking whole, never to size a split.
|
|
|
|
|
*
|
|
|
|
|
* Limits come from the LOCOMOTIVE ALONE — the global-rules weight/length
|
|
|
|
|
* caps deliberately do not apply here (a mis-set global row once capped
|
|
|
|
|
* every train at 14m and no export booking could board).
|
|
|
|
|
*/
|
|
|
|
|
private async capacityLimits(
|
|
|
|
|
locomotive: Locomotive,
|
|
|
|
|
rules: TrainSchedulingGlobalRules | null,
|
|
|
|
|
): Promise<TrainLimits> {
|
|
|
|
|
private async capacityLimits(locomotive: Locomotive): Promise<TrainLimits> {
|
|
|
|
|
const wagonTypes = await this.loadWagonTypeDimensions();
|
|
|
|
|
const derived = deriveTrainCapacityFromLocomotive(
|
|
|
|
|
{
|
|
|
|
|
@@ -2534,14 +2517,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
overageToleranceMeters: Number(locomotive.overageToleranceMeters) || 0,
|
|
|
|
|
},
|
|
|
|
|
wagonTypes,
|
|
|
|
|
{
|
|
|
|
|
maxTrainWeightTons: rules?.maxTrainWeightTons
|
|
|
|
|
? Number(rules.maxTrainWeightTons)
|
|
|
|
|
: undefined,
|
|
|
|
|
maxTrainLengthMeters: rules?.maxTrainLengthMeters
|
|
|
|
|
? Number(rules.maxTrainLengthMeters)
|
|
|
|
|
: undefined,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
base: {
|
|
|
|
|
@@ -2556,18 +2531,27 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Keep schedule.max_wagons aligned with locomotive physical limits. */
|
|
|
|
|
/**
|
|
|
|
|
* Keep schedule.max_wagons aligned with the train's real boarding limit: the
|
|
|
|
|
* locomotive's length-derived slot count, floored by the physical wagons in
|
|
|
|
|
* the train set (slots that exist on paper but not in the yard must not be
|
|
|
|
|
* sold — see {@link remainingBudget}).
|
|
|
|
|
*/
|
|
|
|
|
private async syncScheduleMaxWagons(
|
|
|
|
|
schedule: TrainSchedule,
|
|
|
|
|
locomotive: Locomotive,
|
|
|
|
|
rules: TrainSchedulingGlobalRules | null,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const limits = await this.capacityLimits(locomotive, rules);
|
|
|
|
|
if ((schedule.maxWagons ?? 0) !== limits.base.wagons) {
|
|
|
|
|
const limits = await this.capacityLimits(locomotive);
|
|
|
|
|
const physicalWagons = schedule.trainSet?.wagons?.length ?? 0;
|
|
|
|
|
const maxWagons =
|
|
|
|
|
physicalWagons > 0
|
|
|
|
|
? Math.min(limits.base.wagons, physicalWagons)
|
|
|
|
|
: limits.base.wagons;
|
|
|
|
|
if ((schedule.maxWagons ?? 0) !== maxWagons) {
|
|
|
|
|
await this.dataSource
|
|
|
|
|
.getRepository(TrainSchedule)
|
|
|
|
|
.update(schedule.id, { maxWagons: limits.base.wagons });
|
|
|
|
|
schedule.maxWagons = limits.base.wagons;
|
|
|
|
|
.update(schedule.id, { maxWagons });
|
|
|
|
|
schedule.maxWagons = maxWagons;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2655,12 +2639,6 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadGlobalRules(): Promise<TrainSchedulingGlobalRules | null> {
|
|
|
|
|
return this.dataSource
|
|
|
|
|
.getRepository(TrainSchedulingGlobalRules)
|
|
|
|
|
.findOne({ where: {} });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ordered stop yards of the schedule's route (origin → milestones →
|
|
|
|
|
* destination); the legacy two-stop pseudo-route when milestones are absent.
|
|
|
|
|
@@ -2684,6 +2662,12 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
* Remaining capacity per corridor edge = hard caps minus what allocated +
|
|
|
|
|
* reserved bookings already use ON THEIR OWN LEGS. A booking riding only
|
|
|
|
|
* Dire→Djibouti leaves the Addis→Dire edges untouched.
|
|
|
|
|
*
|
|
|
|
|
* The wagon axis is additionally capped by the PHYSICAL wagons marshalled in
|
|
|
|
|
* the schedule's train set. The length-derived slot count says how many wagons
|
|
|
|
|
* the locomotive could pull, not how many exist: a 760m/54-slot train with a
|
|
|
|
|
* 50-wagon set once split-offered 4 wagons that were never buildable — the
|
|
|
|
|
* customer paid and the wagon planner had nothing to assign.
|
|
|
|
|
*/
|
|
|
|
|
private async remainingBudget(
|
|
|
|
|
schedule: TrainSchedule,
|
|
|
|
|
@@ -2691,7 +2675,12 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
wagonDims: WagonDims,
|
|
|
|
|
): Promise<CorridorBudget> {
|
|
|
|
|
const stops = await this.stopsForSchedule(schedule);
|
|
|
|
|
const budget = new CorridorBudget(stops, limits.base, limits.tolerance);
|
|
|
|
|
const physicalWagons = schedule.trainSet?.wagons?.length ?? 0;
|
|
|
|
|
const base =
|
|
|
|
|
physicalWagons > 0
|
|
|
|
|
? { ...limits.base, wagons: Math.min(limits.base.wagons, physicalWagons) }
|
|
|
|
|
: limits.base;
|
|
|
|
|
const budget = new CorridorBudget(stops, base, limits.tolerance);
|
|
|
|
|
const allocated = (schedule.scheduleBookings ?? [])
|
|
|
|
|
.map((sb) => sb.booking)
|
|
|
|
|
.filter((b): b is Booking => Boolean(b));
|
|
|
|
|
@@ -2795,9 +2784,8 @@ export class BookingBatchService implements OnModuleInit {
|
|
|
|
|
if ((await this.remainingWagons(schedule)) <= 0) return true;
|
|
|
|
|
const locomotive = schedule.trainSet?.locomotive;
|
|
|
|
|
if (!locomotive) return false; // no weight/length limits to bind against
|
|
|
|
|
const rules = await this.loadGlobalRules();
|
|
|
|
|
const wagonDims = await this.loadWagonDims();
|
|
|
|
|
const limits = await this.capacityLimits(locomotive, rules);
|
|
|
|
|
const limits = await this.capacityLimits(locomotive);
|
|
|
|
|
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
|
|
|
|
return budget.isExhausted(this.minPerWagonNeed(wagonDims));
|
|
|
|
|
}
|
|
|
|
|
|