mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
Fix checkin time cutoff
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
/**
|
||||
* Resolves the booking/check-in cutoff for one boarding stop: stop-level
|
||||
* `RouteStop.checkinMinutesBefore` override wins, else the route-level default
|
||||
* (`Route.checkinMinutesBefore`), else a bare 30-minute fallback for routes/stops with
|
||||
* neither configured. The basis is the stop's own estimated ARRIVAL time (the train reaching
|
||||
* that stop), not its departure or the schedule's overall origin departure — a downstream
|
||||
* stop's cutoff must be independent of how long ago the train left its origin. The first stop
|
||||
* of a route has no arrival (nothing to arrive at), so it falls back to its own departure.
|
||||
* Resolves the booking/check-in cutoff for one boarding stop.
|
||||
*
|
||||
* Single source of truth for this computation — SeatsService.holdSeats and
|
||||
* SearchService.buildScheduleResult already applied it (search results only ever showed a
|
||||
* segment as bookable if this same cutoff hadn't passed); GuestBookingService.createGuestBooking
|
||||
* used to independently hardcode a flat, non-configurable 30 minutes off the schedule's origin
|
||||
* departure, which could reject a booking the search/hold steps had just accepted under the
|
||||
* route's actual configured cutoff.
|
||||
* Priority for checkinMinutes: RouteStop.checkinMinutesBefore → Route.checkinMinutesBefore → 30.
|
||||
*
|
||||
* Anchor (segmentTime): plannedDepartureAt ?? plannedArrivalAt ?? schedule.departureAt.
|
||||
* - For the origin stop: plannedDepartureAt = schedule.departureAt (no arrival).
|
||||
* - For intermediate stops: plannedDepartureAt = plannedArrivalAt + dwell (checkinMinutesBefore).
|
||||
* cutoffAt = departureAt − checkinMinutesBefore = arrivalAt, so booking closes the
|
||||
* moment the train reaches the stop — independent of how long ago it left the origin.
|
||||
*
|
||||
* Single source of truth — SeatsService.holdSeats and SearchService.buildScheduleResult both
|
||||
* apply it; GuestBookingService.createGuestBooking also applies it per boarding stop.
|
||||
*/
|
||||
export interface CheckinCutoff {
|
||||
/** The stop's own estimated arrival time (or departure, for the first stop / missing data). */
|
||||
/** The stop's planned departure time (or arrival / schedule departure as fallback). */
|
||||
segmentTime: Date;
|
||||
/** Minutes before segmentTime that booking/holding closes. */
|
||||
checkinMinutes: number;
|
||||
@@ -34,7 +32,7 @@ export function resolveCheckinCutoff(
|
||||
stopTime: { plannedArrivalAt?: Date | null; plannedDepartureAt?: Date | null } | null | undefined,
|
||||
stationId: string | null | undefined,
|
||||
): CheckinCutoff {
|
||||
const segmentTime = stopTime?.plannedArrivalAt ?? stopTime?.plannedDepartureAt ?? schedule.departureAt;
|
||||
const segmentTime = stopTime?.plannedDepartureAt ?? stopTime?.plannedArrivalAt ?? schedule.departureAt;
|
||||
const routeStop = stationId ? schedule.route?.stops?.find((s) => s.stationId === stationId) : undefined;
|
||||
const checkinMinutes = routeStop?.checkinMinutesBefore ?? schedule.route?.checkinMinutesBefore ?? 30;
|
||||
return {
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user