fix schule issue and contianer type issue

This commit is contained in:
Marshal
2026-07-17 09:16:35 +00:00
parent 5a1e0dba4d
commit 7b7e7c3f62
39 changed files with 508 additions and 187 deletions

View File

@@ -68,6 +68,7 @@ import {
wagonTypeDimensionsFromEntity,
} from './train-capacity.util';
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
import { Wagon } from '../wagons/entities/wagon.entity';
import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service';
import { BookingSplitService } from './booking-split.service';
import { BookingWindowGateway } from './booking-window.gateway';
@@ -305,6 +306,9 @@ export class BookingBatchService implements OnModuleInit {
private readonly trainScheduleBookingsRepository: TrainScheduleBookingsRepository,
private readonly notifier: BookingNotifierService,
private readonly scheduler: SchedulerRegistry,
// forwardRef: TrainSchedulingService injects this service back (window
// refresh after adjust-consist), so the classes load in a cycle.
@Inject(forwardRef(() => TrainSchedulingService))
private readonly trainSchedulingService: TrainSchedulingService,
private readonly billing: BillingService,
private readonly bookingWindowGateway: BookingWindowGateway,
@@ -2915,7 +2919,7 @@ export class BookingBatchService implements OnModuleInit {
? Math.ceil(booking.wagonsRequired)
: 0;
// TEU-aware: two 20ft share one wagon (wagonsPerUnit = 0.5). The old fallback
// TEU-aware: two 20ft share one wagon (half a wagon each). The old fallback
// summed raw container QUANTITY, so 20×20ft counted as 20 wagons, not 10.
const byLength = containerWagonsForLines(booking.bookingContainers ?? []);
@@ -2993,17 +2997,20 @@ export class BookingBatchService implements OnModuleInit {
}
/**
* Keep schedule.max_wagons aligned with the train's boarding limit: the
* locomotive's length-derived slot count. The physical wagons currently in
* the train set do NOT cap this — bookings are admitted on length/weight
* alone and yard staff attach the wagons manually before departure.
* Keep schedule.max_wagons aligned with the train's boarding limit. A built
* train's limit is its physical consist — the wagon count staff marshalled
* (and may change via adjust-consist). Only schedules WITHOUT a built train
* fall back to the locomotive's length-derived slot count, where bookings
* are admitted on length/weight alone and yard staff attach the wagons
* manually before departure.
*/
private async syncScheduleMaxWagons(
schedule: TrainSchedule,
locomotive: Locomotive,
): Promise<void> {
const limits = await this.capacityLimits(locomotive);
const maxWagons = limits.base.wagons;
const physicalWagons = await this.builtTrainWagonCount(schedule);
const maxWagons =
physicalWagons ?? (await this.capacityLimits(locomotive)).base.wagons;
if ((schedule.maxWagons ?? 0) !== maxWagons) {
await this.dataSource
.getRepository(TrainSchedule)
@@ -3122,16 +3129,31 @@ export class BookingBatchService implements OnModuleInit {
* reserved bookings already use ON THEIR OWN LEGS. A booking riding only
* Dire→Djibouti leaves the Addis→Dire edges untouched.
*
* The wagon axis is the locomotive's length-derived slot count only — the
* physical wagons currently marshalled in the train set do NOT cap it.
* Bookings are admitted on length/weight capacity and yard staff attach
* the missing wagons manually before wagon assignment.
* Two capacity regimes, decided by the schedule's train:
* - Built train (Train Builder consist with physical wagons): the consist IS
* the capacity. Wagon slots = physical wagon count; weight and length are
* NOT re-checked here — the builder and adjust-consist already enforced the
* locomotive's pull/length limits when the consist was assembled.
* - No built train (legacy schedules): the locomotive's length-derived slot
* count plus its weight/length budgets, as before — yard staff attach the
* missing wagons manually before wagon assignment.
*/
private async remainingBudget(
schedule: TrainSchedule,
limits: TrainLimits,
wagonDims: WagonDims,
): Promise<CorridorBudget> {
const physicalWagons = await this.builtTrainWagonCount(schedule);
if (physicalWagons != null) {
limits = {
base: {
wagons: physicalWagons,
weightTons: Number.POSITIVE_INFINITY,
lengthMeters: Number.POSITIVE_INFINITY,
},
tolerance: { weightTons: 0, lengthMeters: 0 },
};
}
const stops = await this.stopsForSchedule(schedule);
const budget = new CorridorBudget(stops, limits.base, limits.tolerance);
const allocated = (schedule.scheduleBookings ?? [])
@@ -3149,6 +3171,23 @@ export class BookingBatchService implements OnModuleInit {
return budget;
}
/**
* Physical wagons marshalled in the schedule's built train, or null when the
* schedule has no built train (or the consist is still empty) and the legacy
* locomotive-derived capacity must apply. This count is what caps a built
* train's bookings: 50 wagons coupled → 50 wagon slots, no more.
*/
private async builtTrainWagonCount(
schedule: TrainSchedule,
): Promise<number | null> {
const trainId = schedule.trainSet?.train?.id;
if (!trainId) return null;
const count = await this.dataSource
.getRepository(Wagon)
.count({ where: { trainId } });
return count > 0 ? count : null;
}
/**
* Wagon slots still boardable somewhere on the corridor (most-open edge).
* ≤ 0 means no leg can take another booking. Slot axis ONLY — the train-wide
@@ -3219,11 +3258,14 @@ export class BookingBatchService implements OnModuleInit {
}
/**
* FULL on ANY capacity axis: out of wagon slots, or out of pull weight /
* train length for even one more loaded wagon. The old slot-only check let
* a weight-bound train (PW2: weight binds at 37 wagons = 3522.4T of
* 3500+90T, slots bind at 44) cycle its booking window forever instead of
* finalizing — 7 phantom slots kept it "not full" while nothing could board.
* Built train: FULL when every physical wagon slot is taken — the consist is
* the capacity, weight/length were settled at build time.
* No built train: FULL on ANY capacity axis — out of wagon slots, or out of
* pull weight / train length for even one more loaded wagon. The old
* slot-only check let a weight-bound train (PW2: weight binds at 37 wagons =
* 3522.4T of 3500+90T, slots bind at 44) cycle its booking window forever
* instead of finalizing — 7 phantom slots kept it "not full" while nothing
* could board.
*/
async isScheduleFull(scheduleId: string): Promise<boolean> {
const schedule =
@@ -3232,9 +3274,53 @@ export class BookingBatchService implements OnModuleInit {
return this.isTrainFull(schedule);
}
/**
* Wagon-slot usage snapshot for staff UIs (adjust-consist dialog): the
* schedule's slot capacity, how many slots allocated + reserved bookings
* already hold on the busiest edge, how many are still free on the most-open
* edge, and by how many slots the consist has been trimmed BELOW what is
* already committed (0 when nothing is over-allocated).
*/
async scheduleWagonUsage(scheduleId: string): Promise<{
maxWagons: number;
allocatedWagons: number;
remainingSlots: number;
overAllocatedBy: number;
} | null> {
const schedule =
await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
if (!schedule) return null;
const capacity =
(await this.builtTrainWagonCount(schedule)) ?? schedule.maxWagons ?? 0;
const wagonDims = await this.loadWagonDims();
const budget = await this.remainingBudget(
schedule,
{
base: {
wagons: capacity,
weightTons: Number.POSITIVE_INFINITY,
lengthMeters: Number.POSITIVE_INFINITY,
},
tolerance: { weightTons: 0, lengthMeters: 0 },
},
wagonDims,
);
const tightest = budget.remainingFor(budget.fullLeg()).wagons;
return {
maxWagons: capacity,
allocatedWagons: capacity - tightest,
remainingSlots: Math.max(0, budget.maxRemaining().wagons),
overAllocatedBy: Math.max(0, -tightest),
};
}
/** See {@link isScheduleFull} — same check for callers that already hold the full graph. */
private async isTrainFull(schedule: TrainSchedule): Promise<boolean> {
if ((await this.remainingWagons(schedule)) <= 0) return true;
// Built train: the physical consist is the only capacity axis. Weight and
// length were enforced when the consist was assembled (builder /
// adjust-consist), so a free wagon slot means the train genuinely has room.
if ((await this.builtTrainWagonCount(schedule)) != null) return false;
const locomotive = schedule.trainSet?.locomotive;
if (!locomotive) return false; // no weight/length limits to bind against
const wagonDims = await this.loadWagonDims();