Cut off time configuration

This commit is contained in:
Muluhabt
2026-07-24 09:01:27 +03:00
parent 64e1130f7d
commit 7320c359d1
10 changed files with 618 additions and 207 deletions

View File

@@ -0,0 +1,45 @@
/**
* 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.
*
* 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.
*/
export interface CheckinCutoff {
/** The stop's own estimated arrival time (or departure, for the first stop / missing data). */
segmentTime: Date;
/** Minutes before segmentTime that booking/holding closes. */
checkinMinutes: number;
/** The moment booking/holding closes for this stop. */
cutoffAt: Date;
}
export function resolveCheckinCutoff(
schedule: {
departureAt: Date;
route?: {
checkinMinutesBefore?: number | null;
stops?: Array<{ stationId: string; checkinMinutesBefore: number | null }>;
} | null;
},
stopTime: { plannedArrivalAt?: Date | null; plannedDepartureAt?: Date | null } | null | undefined,
stationId: string | null | undefined,
): CheckinCutoff {
const segmentTime = stopTime?.plannedArrivalAt ?? stopTime?.plannedDepartureAt ?? schedule.departureAt;
const routeStop = stationId ? schedule.route?.stops?.find((s) => s.stationId === stationId) : undefined;
const checkinMinutes = routeStop?.checkinMinutesBefore ?? schedule.route?.checkinMinutesBefore ?? 30;
return {
segmentTime,
checkinMinutes,
cutoffAt: new Date(segmentTime.getTime() - checkinMinutes * 60_000),
};
}

View File

@@ -0,0 +1,37 @@
/**
* Resolves a booking's actual boarding/alighting station AND time for one leg from
* originStationId/destinationStationId (set when the booking covers only part of a
* longer multi-stop schedule, e.g. train runs A→D but the passenger booked B→D), via
* the schedule's stopTimes — falling back to the schedule's own full-route
* station/time when there's no segment override (older records, or a booking that
* covers the whole run).
*
* Single source of truth for this resolution — station-only lookups used to be
* duplicated ad hoc across bookings/tickets/notifications while the departureAt/
* arrivalAt kept being read straight off the schedule (the train's full-route span),
* which showed the wrong boarding/alighting time for any stop-based booking.
*/
export interface ResolvedSegment {
origin: any;
destination: any;
departureAt: any;
arrivalAt: any;
}
export function resolveBookingSegment(
schedule: any,
originStationId: string | null | undefined,
destinationStationId: string | null | undefined,
): ResolvedSegment {
const stopTimes: any[] = schedule?.stopTimes ?? [];
const findStop = (stationId: string | null | undefined) =>
stationId && stopTimes.length > 0 ? stopTimes.find((st: any) => st.stationId === stationId) : undefined;
const originStop = findStop(originStationId);
const destStop = findStop(destinationStationId);
return {
origin: originStop?.station ?? schedule?.originStation ?? null,
destination: destStop?.station ?? schedule?.destinationStation ?? null,
departureAt: originStop?.plannedDepartureAt ?? schedule?.departureAt ?? null,
arrivalAt: destStop?.plannedArrivalAt ?? schedule?.arrivalAt ?? null,
};
}