Merge pull request #826 from Tria-plc/freight_feature/usermanagement

changes
This commit is contained in:
marshal
2026-07-20 06:06:06 +03:00
committed by GitHub
10 changed files with 456 additions and 271 deletions

View File

@@ -104,7 +104,8 @@ export class IntercityService {
async listCandidates(scheduleId: string) {
const schedule = await this.getSchedule(scheduleId);
const milestoneSeq = await this.routeMilestoneSequence(schedule);
const milestones = await this.routeMilestones(schedule);
const milestoneSeq = this.milestoneSequenceOf(schedule, milestones);
const capacity = await this.bookingBatchService.intercityCapacity(scheduleId);
const waiting = milestoneSeq
@@ -112,16 +113,32 @@ export class IntercityService {
: [];
const accepted = await this.findAcceptedIntercityBookings(scheduleId);
// Mid-corridor intercity matching needs a real stop list (>= 2 route
// milestones). Without one the fallback is a 2-stop origin->destination
// pseudo-route that only matches bookings on the train's exact corridor —
// surface that so an empty candidate list isn't misread as "nobody waiting".
const warning =
milestoneSeq == null
? 'This schedule has no route or origin/destination set, so no intercity corridors can be served.'
: schedule.routeId && milestones.length < 2
? "This schedule's route has no stop list (needs at least 2 route milestones), so mid-corridor intercity bookings cannot be matched — only bookings on the train's exact origin→destination will appear."
: null;
return {
scheduleId,
routeId: schedule.routeId ?? null,
warning,
// Segment-based: "remaining" is the most-open edge; each candidate's
// `fits` is judged against ITS OWN leg, so a booking on a free leg fits
// even when the train is full elsewhere.
remaining: capacity?.budget.maxRemaining() ?? null,
candidates: waiting.map((booking) => {
const need = capacity?.needFor(booking) ?? null;
const leg = capacity?.budget.legOf(
// legForYards, not legOf: on a built train the budget is a single
// whole-route edge (see intercityCapacity), so a mid-corridor booking
// must draw from that one pool via the whole-route fallback. On a
// locomotive-derived schedule it still resolves to the booking's own leg.
const leg = capacity?.budget.legForYards(
booking.originYardId,
booking.destinationYardId,
);
@@ -186,14 +203,17 @@ export class IntercityService {
continue;
}
const need = capacity.needFor(booking);
const leg = budget.legOf(booking.originYardId, booking.destinationYardId);
// Segment-based: only the booking's own leg must have room, so an
// intercity booking still boards a train that is full on other legs.
if (!leg || !budget.fits(need, leg)) {
// legForYards, not legOf: a built train's budget is a single whole-route
// pool (mid-corridor wagons are committed for the whole trip and never
// reloaded), so the booking draws from that pool via the whole-route
// fallback; a locomotive-derived schedule still gets the booking's own
// leg, so it can still board a train that is full only on other legs.
const leg = budget.legForYards(booking.originYardId, booking.destinationYardId);
if (!budget.fits(need, leg)) {
rejected.push({
bookingId,
reason:
'Does not fit the remaining wagon/weight/length capacity on its leg',
'Does not fit the remaining wagon/weight/length capacity for this train',
});
continue;
}
@@ -244,16 +264,21 @@ export class IntercityService {
* so an intercity booking exactly matching the train's own corridor still
* qualifies.
*/
private async routeMilestoneSequence(
private async routeMilestones(
schedule: TrainSchedule,
): Promise<Map<string, number> | null> {
if (schedule.routeId) {
const milestones = await this.dataSource
.getRepository(RouteMilestone)
.find({ where: { routeId: schedule.routeId }, order: { sequenceNo: 'ASC' } });
if (milestones.length >= 2) {
return new Map(milestones.map((m) => [m.yardId, m.sequenceNo]));
}
): Promise<RouteMilestone[]> {
if (!schedule.routeId) return [];
return this.dataSource
.getRepository(RouteMilestone)
.find({ where: { routeId: schedule.routeId }, order: { sequenceNo: 'ASC' } });
}
private milestoneSequenceOf(
schedule: TrainSchedule,
milestones: RouteMilestone[],
): Map<string, number> | null {
if (milestones.length >= 2) {
return new Map(milestones.map((m) => [m.yardId, m.sequenceNo]));
}
if (schedule.originStationId && schedule.destinationStationId) {
return new Map([
@@ -264,6 +289,15 @@ export class IntercityService {
return null;
}
private async routeMilestoneSequence(
schedule: TrainSchedule,
): Promise<Map<string, number> | null> {
return this.milestoneSequenceOf(
schedule,
await this.routeMilestones(schedule),
);
}
/** Waiting = ready intercity bookings not yet on any train, corridor on this route. */
private async findWaitingIntercityBookings(
milestoneSeq: Map<string, number>,