mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 03:10:54 +00:00
fix(freight): guard booking-desk hours against overnight ranges
Review follow-ups on the window-close-hour feature: - Reject windowCloseHour < windowOpenHour when saving global rules. The reopen engine (nextCycleOpensAt) assumes the daily desk runs within one EAT day; an overnight range would misroute a ready-time inside the span to the next morning. openHour === closeHour stays valid (24-hour desk). - Derive the board projection's runaway cap from the real first-open → departure span over the minimum per-cycle advance, so a legitimate long-lead config is never silently truncated (was a flat 200). - Document the open <= close precondition on nextCycleOpensAt and clarify the ready-before-open comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,10 @@ export function isRoundTheClock(hours: OfficeHours): boolean {
|
||||
*
|
||||
* Returns `null` when the next open would fall on/after `departure` — the train
|
||||
* leaves before another cycle could run, so the window is done.
|
||||
*
|
||||
* Precondition: `windowCloseHour >= windowOpenHour` — the desk runs within a
|
||||
* single EAT day and never wraps past midnight (enforced when global rules are
|
||||
* saved). openHour === closeHour is the 24-hour desk, handled first.
|
||||
*/
|
||||
export function nextCycleOpensAt(
|
||||
earliestNextOpen: Date,
|
||||
@@ -179,7 +183,7 @@ export function nextCycleOpensAt(
|
||||
const openMinutes = hours.windowOpenHour * 60;
|
||||
const closeMinutes = hours.windowCloseHour * 60;
|
||||
if (readyMinutes < openMinutes) {
|
||||
// Desk not open yet today (ready before opening) → open this morning.
|
||||
// Ready before the desk opens on its own EAT calendar day → open this morning.
|
||||
opensAt = eatDayToUtc(eatDay(earliestNextOpen), hours.windowOpenHour);
|
||||
} else if (readyMinutes < closeMinutes) {
|
||||
// Inside office hours → open as soon as ready.
|
||||
@@ -397,9 +401,16 @@ export function listConfigBookingWindows(
|
||||
const windowDay = shiftEatDay(eatDay(departure), -cfg.importWindowLeadDays);
|
||||
|
||||
let opensAt: Date | null = anchorOpensAt ?? eatDayToUtc(windowDay, cfg.windowOpenHour);
|
||||
// Reopen daily until the train departs; cap the projection so a tiny duration
|
||||
// can't run away (spanning up to importWindowLeadDays of office days).
|
||||
for (let cycle = 0; cycle < 200; cycle += 1) {
|
||||
// The loop terminates naturally: every cycle advances opensAt by at least
|
||||
// (duration + reopen) > 0, and nextCycleOpensAt returns null once opensAt would
|
||||
// reach departure. maxCycles is a derived runaway backstop sized to the real
|
||||
// span (first open → departure) over the smallest possible advance, so a
|
||||
// legitimate config is never silently truncated — only a pathological
|
||||
// zero-length one would hit it.
|
||||
const spanMs = departure.getTime() - opensAt.getTime();
|
||||
const minAdvanceMs = Math.max(durationMs + reopenMs, 60_000);
|
||||
const maxCycles = Math.ceil(spanMs / minAdvanceMs) + 2;
|
||||
for (let cycle = 0; cycle < maxCycles; cycle += 1) {
|
||||
if (opensAt.getTime() >= departure.getTime()) break;
|
||||
let closesAt = new Date(opensAt.getTime() + durationMs);
|
||||
if (closesAt.getTime() > departure.getTime()) closesAt = departure;
|
||||
|
||||
Reference in New Issue
Block a user