Merge branch 'dev' into tests

This commit is contained in:
mulish77
2026-07-27 14:59:00 +03:00
committed by GitHub
360 changed files with 39014 additions and 2448 deletions

View File

@@ -0,0 +1,76 @@
import { Logger } from '@nestjs/common';
const logger = new Logger('ScheduleTimesUtils');
export type StopForTiming = {
sequence: number;
distanceKm: number | null;
travelMinutesToStop: number | null;
checkinMinutesBefore: number | null;
};
export type PlannedStopTime = {
sequence: number;
plannedArrivalAt: string | undefined;
plannedDepartureAt: string | undefined;
};
/**
* Computes each stop's planned arrival/departure time by walking the route in sequence order.
*
* Model per intermediate stop:
* arrival = departureCursor + travelMinutesToStop (falls back to distance interpolation)
* departure = arrival + checkinMinutesBefore (dwell time; 0 if null)
* next-stop travel starts from this departure, not from arrival.
*
* This means booking for stop B closes at B.departureAt checkinMinutesBefore = B.arrivalAt,
* i.e. the train must not yet have arrived at the stop for a booking to succeed.
*
* The last stop is always locked to arr so schedule.arrivalAt stays authoritative.
*/
export function computePlannedStopTimes(
route: { id: string; stops: StopForTiming[] },
dep: Date,
arr: Date,
): PlannedStopTime[] {
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
// cursor tracks the DEPARTURE time from the most-recently processed stop.
let departureCursor = dep;
return route.stops.map((stop, index) => {
if (index === 0) {
// Origin: train starts here, no arrival.
departureCursor = dep;
return { sequence: stop.sequence, plannedArrivalAt: undefined, plannedDepartureAt: dep.toISOString() };
}
if (index === route.stops.length - 1) {
// Final destination: arrival is authoritative; no departure.
return { sequence: stop.sequence, plannedArrivalAt: arr.toISOString(), plannedDepartureAt: undefined };
}
// Intermediate stop: compute arrival from the previous stop's departure.
let arrivalAt: Date;
if (stop.travelMinutesToStop != null) {
arrivalAt = new Date(departureCursor.getTime() + stop.travelMinutesToStop * 60_000);
} else {
const stopDistance = stop.distanceKm || 0;
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
arrivalAt = new Date(dep.getTime() + totalDuration * progress);
logger.warn(`Route ${route.id} stop seq ${stop.sequence} missing travelMinutesToStop; falling back to distance interpolation`);
}
// Dwell at this stop = checkinMinutesBefore (the boarding window).
const dwell = stop.checkinMinutesBefore ?? 0;
const departureAt = new Date(arrivalAt.getTime() + dwell * 60_000);
departureCursor = departureAt;
return {
sequence: stop.sequence,
plannedArrivalAt: arrivalAt.toISOString(),
plannedDepartureAt: departureAt.toISOString(),
};
});
}