fix issues

This commit is contained in:
marshal
2026-09-06 13:50:07 +00:00
parent 75b75e3d4e
commit 19145306c9
26 changed files with 1716 additions and 14 deletions

View File

@@ -289,6 +289,99 @@ export function bookingCloseCutoff(
return new Date(departure.getTime() - offsetMinutes * 60_000);
}
/** The schedule fields the close-offset reopen guard reads. */
export interface CloseOffsetReopenSchedule {
status: string;
direction?: string | null;
windowPhase?: string | null;
bookingWindowStatus?: string | null;
scheduledDepartureDate?: Date | null;
}
export interface CloseOffsetReopenCheck {
/** True when shortening the close offset is the one thing that reopens booking. */
eligible: boolean;
/** Why the schedule is not eligible; null when it is. */
reason: string | null;
/** Minutes before departure this schedule currently stops taking bookings. */
offsetMinutes: number | null;
/** The cutoff that shut booking (departure offset); null without an offset. */
cutoffAt: Date | null;
}
/**
* Is this schedule's booking shut ONLY because of its close offset? That is the
* one case staff may fix from the board by shortening the offset (3 days → 1
* day, 2 hours, …) so the desk reopens before departure. Every other way a
* window ends stays closed: the train departed, it is full, it never had an
* offset (booking ran until departure), or the window is still mid-cycle.
*
* The last guard — "a cycle would fit before departure with no offset at all" —
* is what makes the offset the ONLY problem: when the desk's next opening lands
* after the train leaves, no offset change can help.
*/
export function closeOffsetReopenCheck(
schedule: CloseOffsetReopenSchedule,
cfg: {
importCloseOffsetMinutes?: number | null;
exportCloseOffsetMinutes?: number | null;
windowOpenHour: number;
windowCloseHour: number;
},
now: Date,
): CloseOffsetReopenCheck {
const departure = schedule.scheduledDepartureDate ?? null;
const offsetRaw =
schedule.direction === 'EXPORT'
? cfg.exportCloseOffsetMinutes
: cfg.importCloseOffsetMinutes;
const offsetMinutes = offsetRaw != null && offsetRaw > 0 ? offsetRaw : null;
const cutoffAt =
departure && offsetMinutes != null
? bookingCloseCutoff(departure, schedule.direction, cfg)
: null;
const no = (reason: string): CloseOffsetReopenCheck => ({
eligible: false,
reason,
offsetMinutes,
cutoffAt,
});
if (schedule.status !== 'DRAFT' && schedule.status !== 'SCHEDULED') {
return no(`A ${schedule.status.toLowerCase()} train cannot reopen booking.`);
}
if (!departure || departure.getTime() <= now.getTime()) {
return no('This train has already departed (or has no departure date).');
}
if (offsetMinutes == null) {
return no(
'This train has no close offset — booking ran until departure, so there is nothing to shorten.',
);
}
if (schedule.windowPhase !== 'DONE') {
return no(
schedule.windowPhase == null
? 'This train does not run a managed booking window.'
: `Booking is not closed yet — the window is in its ${schedule.windowPhase} phase.`,
);
}
if (schedule.bookingWindowStatus === 'FULL') {
return no(
'Booking closed because the train is full, not because of the close offset.',
);
}
const hours: OfficeHours = {
windowOpenHour: cfg.windowOpenHour,
windowCloseHour: cfg.windowCloseHour,
};
if (nextCycleOpensAt(now, hours, departure) == null) {
return no(
'The desk would not reopen before departure even with no close offset — the offset is not what is blocking booking.',
);
}
return { eligible: true, reason: null, offsetMinutes, cutoffAt };
}
export interface InitialWindowTimes {
windowOpensAt: Date;
windowClosesAt: Date;