mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
export flow and fix intercity issue
This commit is contained in:
@@ -1088,6 +1088,15 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
* the whole (route, day) pool rather than bookings pre-targeted to one train.
|
||||
*/
|
||||
day?: string;
|
||||
/**
|
||||
* The schedule's ordered route stops. When given, the corridor filter
|
||||
* replaces the exact origin/destination match: any booking whose BOTH yards
|
||||
* lie on the route qualifies (sub-corridor bookings like Dire→DCT on a
|
||||
* GMT→Dire→DCT train — the caller still checks stop ORDER). Dateless
|
||||
* DOMESTIC (intercity) bookings also join the pool: they ride any train on
|
||||
* their corridor.
|
||||
*/
|
||||
corridorYardIds?: string[];
|
||||
}): Promise<Booking[]> {
|
||||
const qb = this.repository
|
||||
.createQueryBuilder('booking')
|
||||
@@ -1111,8 +1120,11 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
// single-schedule filter only when no day is supplied (e.g. a staff-pinned
|
||||
// booking that still carries train_schedule_id).
|
||||
if (options.day) {
|
||||
// Dateless DOMESTIC (intercity) bookings ride any train on their corridor
|
||||
// — no scheduled_date to match, so the day filter must not hide them.
|
||||
qb.andWhere(
|
||||
`DATE(booking.scheduled_date AT TIME ZONE 'Africa/Addis_Ababa') = :day`,
|
||||
`(DATE(booking.scheduled_date AT TIME ZONE 'Africa/Addis_Ababa') = :day
|
||||
OR (booking.trade_direction = 'DOMESTIC' AND booking.scheduled_date IS NULL))`,
|
||||
{ day: options.day },
|
||||
);
|
||||
} else if (options.trainScheduleId) {
|
||||
@@ -1125,15 +1137,23 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
qb.andWhere('booking.freightType = :freightType', { freightType: options.freightType });
|
||||
}
|
||||
|
||||
if (options.originStationId) {
|
||||
qb.andWhere('booking.originYardId = :originStationId', {
|
||||
originStationId: options.originStationId,
|
||||
});
|
||||
}
|
||||
if (options.destinationStationId) {
|
||||
qb.andWhere('booking.destinationYardId = :destinationStationId', {
|
||||
destinationStationId: options.destinationStationId,
|
||||
if (options.corridorYardIds?.length) {
|
||||
qb.andWhere('booking.originYardId IN (:...corridorYardIds)', {
|
||||
corridorYardIds: options.corridorYardIds,
|
||||
}).andWhere('booking.destinationYardId IN (:...corridorYardIds)', {
|
||||
corridorYardIds: options.corridorYardIds,
|
||||
});
|
||||
} else {
|
||||
if (options.originStationId) {
|
||||
qb.andWhere('booking.originYardId = :originStationId', {
|
||||
originStationId: options.originStationId,
|
||||
});
|
||||
}
|
||||
if (options.destinationStationId) {
|
||||
qb.andWhere('booking.destinationYardId = :destinationStationId', {
|
||||
destinationStationId: options.destinationStationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (options.schedulingStatus) {
|
||||
qb.andWhere('booking.scheduling_status = :schedulingStatus', {
|
||||
|
||||
@@ -638,23 +638,41 @@ export class TrainSchedulingService {
|
||||
let day: string | undefined;
|
||||
let originStationId = query.originStationId;
|
||||
let destinationStationId = query.destinationStationId;
|
||||
// Corridor mode: a schedule with intermediate stops pools every booking
|
||||
// whose leg lies ON its route (Dire→DCT on a GMT→Dire→DCT train), not just
|
||||
// exact endpoint matches — otherwise a mid-corridor booking unassigned from
|
||||
// a wagon vanishes from the "Paid · unassigned" pool forever.
|
||||
let corridorStops: string[] | undefined;
|
||||
if (query.trainScheduleId) {
|
||||
const schedule = await this.trainSchedulesRepository.findById(query.trainScheduleId);
|
||||
if (schedule?.scheduledDepartureDate) {
|
||||
day = eatDay(schedule.scheduledDepartureDate);
|
||||
originStationId = originStationId ?? schedule.originStationId;
|
||||
destinationStationId = destinationStationId ?? schedule.destinationStationId;
|
||||
const stops = await this.stopYardsForSchedule(schedule);
|
||||
if (stops.length > 2) corridorStops = stops;
|
||||
}
|
||||
}
|
||||
|
||||
const bookings = await this.bookingsRepository.findEligibleForScheduling({
|
||||
let bookings = await this.bookingsRepository.findEligibleForScheduling({
|
||||
freightType: query.freightType,
|
||||
originStationId,
|
||||
destinationStationId,
|
||||
schedulingStatus: query.schedulingStatus,
|
||||
trainScheduleId: query.trainScheduleId,
|
||||
day,
|
||||
corridorYardIds: corridorStops,
|
||||
});
|
||||
if (corridorStops) {
|
||||
// The IN-filter admits both yards anywhere on the route; only origin
|
||||
// strictly before destination is actually rideable on this train.
|
||||
const stopIdx = new Map(corridorStops.map((yardId, i) => [yardId, i]));
|
||||
bookings = bookings.filter((b) => {
|
||||
const from = stopIdx.get(b.originYardId);
|
||||
const to = stopIdx.get(b.destinationYardId);
|
||||
return from != null && to != null && from < to;
|
||||
});
|
||||
}
|
||||
const tareDims = await this.loadWagonTareDims();
|
||||
return {
|
||||
count: bookings.length,
|
||||
@@ -4604,8 +4622,12 @@ export class TrainSchedulingService {
|
||||
wagonTypeCode: typeCodeById.get(slot.wagonTypeId) ?? slot.wagonTypeId,
|
||||
trainSetWagonId: slot.id,
|
||||
boardYardId: slot.boardYardId ?? null,
|
||||
alightYardId: slot.alightYardId ?? null,
|
||||
}));
|
||||
|
||||
const pinSchedule = await this.trainSchedulesRepository.findById(scheduleId);
|
||||
const stops = pinSchedule ? await this.stopYardsForSchedule(pinSchedule) : [];
|
||||
|
||||
const unpinnable = this.findUnpinnableWagonSlots(
|
||||
planSlots,
|
||||
wagons,
|
||||
@@ -4613,6 +4635,7 @@ export class TrainSchedulingService {
|
||||
originYardId,
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
stops,
|
||||
);
|
||||
if (unpinnable.length) {
|
||||
throw new BadRequestException({
|
||||
@@ -4621,14 +4644,16 @@ export class TrainSchedulingService {
|
||||
});
|
||||
}
|
||||
|
||||
const assignedPhysicalIds = new Set<string>();
|
||||
const occupiedSpans = new Map<string, Array<[number, number]>>();
|
||||
for (const slot of planSlots) {
|
||||
const span = this.slotSpanOf(slot, stops);
|
||||
const physical = this.pickPhysicalWagonForSlot(
|
||||
slot,
|
||||
wagons,
|
||||
scheduleId,
|
||||
originYardId,
|
||||
assignedPhysicalIds,
|
||||
occupiedSpans,
|
||||
span,
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
reverseWagonOrder,
|
||||
@@ -4642,7 +4667,9 @@ export class TrainSchedulingService {
|
||||
physicalWagonId: physical.id,
|
||||
status: 'RESERVED',
|
||||
});
|
||||
assignedPhysicalIds.add(physical.id);
|
||||
const pinnedSpans = occupiedSpans.get(physical.id) ?? [];
|
||||
pinnedSpans.push(span);
|
||||
occupiedSpans.set(physical.id, pinnedSpans);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4659,44 +4686,74 @@ export class TrainSchedulingService {
|
||||
this.builtTrainIdOfSchedule(targetScheduleId),
|
||||
this.pinnedPhysicalWagonIdsForSchedule(targetScheduleId),
|
||||
]);
|
||||
const targetSchedule = targetScheduleId
|
||||
? await this.trainSchedulesRepository.findById(targetScheduleId)
|
||||
: null;
|
||||
const stops = targetSchedule
|
||||
? await this.stopYardsForSchedule(targetSchedule)
|
||||
: [];
|
||||
return this.findUnpinnableWagonSlots(
|
||||
wagonPlan.map((slot) => ({
|
||||
sequenceNo: slot.sequenceNo,
|
||||
wagonTypeId: slot.wagonTypeId,
|
||||
wagonTypeCode: slot.wagonTypeCode,
|
||||
boardYardId: slot.boardYardId ?? null,
|
||||
alightYardId: slot.alightYardId ?? null,
|
||||
})),
|
||||
wagons,
|
||||
targetScheduleId,
|
||||
originYardId,
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
stops,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop-index span [board, alight) a slot occupies along the route. Slots with
|
||||
* unknown/missing yards conservatively span the whole route (never share).
|
||||
*/
|
||||
private slotSpanOf(
|
||||
slot: { boardYardId?: string | null; alightYardId?: string | null },
|
||||
stops: string[],
|
||||
): [number, number] {
|
||||
const last = Math.max(1, stops.length - 1);
|
||||
const from = slot.boardYardId ? stops.indexOf(slot.boardYardId) : 0;
|
||||
const to = slot.alightYardId ? stops.indexOf(slot.alightYardId) : last;
|
||||
if (from < 0 || to < 0 || from >= to) return [0, last];
|
||||
return [from, to];
|
||||
}
|
||||
|
||||
private findUnpinnableWagonSlots(
|
||||
slots: Array<{
|
||||
sequenceNo: number;
|
||||
wagonTypeId: string;
|
||||
wagonTypeCode: string;
|
||||
boardYardId?: string | null;
|
||||
alightYardId?: string | null;
|
||||
}>,
|
||||
wagons: Wagon[],
|
||||
scheduleId: string | undefined,
|
||||
originYardId: string,
|
||||
builtTrainId: string | null = null,
|
||||
pinnedToScheduleIds: Set<string> = new Set(),
|
||||
stops: string[] = [],
|
||||
): string[] {
|
||||
const violations: string[] = [];
|
||||
const assignedPhysicalIds = new Set<string>();
|
||||
// One physical wagon may serve several slots whose leg spans don't overlap
|
||||
// (freed at its alight yard, reloaded downstream) — track occupied spans
|
||||
// per wagon, not a flat taken-set.
|
||||
const occupiedSpans = new Map<string, Array<[number, number]>>();
|
||||
|
||||
for (const slot of [...slots].sort((a, b) => a.sequenceNo - b.sequenceNo)) {
|
||||
const span = this.slotSpanOf(slot, stops);
|
||||
const physical = this.pickPhysicalWagonForSlot(
|
||||
slot,
|
||||
wagons,
|
||||
scheduleId,
|
||||
originYardId,
|
||||
assignedPhysicalIds,
|
||||
occupiedSpans,
|
||||
span,
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
);
|
||||
@@ -4706,7 +4763,9 @@ export class TrainSchedulingService {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
assignedPhysicalIds.add(physical.id);
|
||||
const spans = occupiedSpans.get(physical.id) ?? [];
|
||||
spans.push(span);
|
||||
occupiedSpans.set(physical.id, spans);
|
||||
}
|
||||
|
||||
return violations;
|
||||
@@ -4722,14 +4781,21 @@ export class TrainSchedulingService {
|
||||
wagons: Wagon[],
|
||||
scheduleId: string | undefined,
|
||||
originYardId: string,
|
||||
assignedPhysicalIds: Set<string>,
|
||||
occupiedSpans: Map<string, Array<[number, number]>>,
|
||||
span: [number, number],
|
||||
builtTrainId: string | null = null,
|
||||
pinnedToScheduleIds: Set<string> = new Set(),
|
||||
reverseWagonOrder = false,
|
||||
): Wagon | undefined {
|
||||
// Free for this slot = no already-assigned span on this wagon overlaps the
|
||||
// slot's own leg. Disjoint legs (alight before board) share the wagon.
|
||||
const spanFree = (wagonId: string): boolean =>
|
||||
(occupiedSpans.get(wagonId) ?? []).every(
|
||||
([from, to]) => to <= span[0] || span[1] <= from,
|
||||
);
|
||||
const usable = (wagon: Wagon): boolean => {
|
||||
if (wagon.wagonTypeId !== slot.wagonTypeId) return false;
|
||||
if (assignedPhysicalIds.has(wagon.id)) return false;
|
||||
if (!spanFree(wagon.id)) return false;
|
||||
// Loose pool never lends a wagon coupled to a built train's consist.
|
||||
if (wagon.trainId) return false;
|
||||
// Out on a dispatched train right now — physically gone.
|
||||
@@ -4755,7 +4821,7 @@ export class TrainSchedulingService {
|
||||
(w) =>
|
||||
w.trainId === builtTrainId &&
|
||||
w.wagonTypeId === slot.wagonTypeId &&
|
||||
!assignedPhysicalIds.has(w.id),
|
||||
spanFree(w.id),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.sequenceNumber == null || b.sequenceNumber == null) {
|
||||
|
||||
Reference in New Issue
Block a user