Add window close hour to booking desk configuration

This commit is contained in:
Marshal
2026-07-05 08:53:27 +00:00
parent 9105e77439
commit 93b8b69464
11 changed files with 271 additions and 57 deletions

View File

@@ -11,7 +11,7 @@ import { NotificationsService } from '../notifications/notifications.service';
import { BookingBatchService } from './booking-batch.service';
import { TrainSchedulingService } from './train-scheduling.service';
import { BATCH_TIMEZONE } from './booking-batch.constants';
import { eatDay } from './batch-window.util';
import { eatDay, nextCycleOpensAt, type OfficeHours } from './batch-window.util';
import { type BookingWindowConfig } from './booking-window.config';
/**
@@ -64,7 +64,9 @@ export class BookingWindowService implements OnModuleInit {
],
})
).filter(
(s) => s.windowPhase != null && s.windowPhase !== 'DONE' && s.windowPhase !== 'CLOSED_FOR_DAY',
// CLOSED_FOR_DAY is legacy (the daily desk now reopens via PRE_WINDOW):
// still pick those rows up so advanceImport can revive them next morning.
(s) => s.windowPhase != null && s.windowPhase !== 'DONE',
);
for (const schedule of active) {
@@ -185,6 +187,14 @@ export class BookingWindowService implements OnModuleInit {
): Promise<boolean> {
const { windowPhase, windowOpensAt, windowClosesAt } = schedule;
// Legacy rows parked at CLOSED_FOR_DAY predate the daily-desk reopen: revive
// them through the same not-full conclude path so they resume next morning
// (or finalize as DONE if no cycle fits before departure).
if (windowPhase === 'CLOSED_FOR_DAY') {
await this.concludeCycle(schedule, cfg, now);
return true;
}
if (windowPhase === 'PRE_WINDOW' && windowOpensAt && now >= windowOpensAt) {
await this.setPhase(schedule, {
windowPhase: 'OPEN',
@@ -268,34 +278,46 @@ export class BookingWindowService implements OnModuleInit {
return;
}
const closesAt = schedule.windowClosesAt ?? now;
const reopenAt = new Date(closesAt.getTime() + cfg.reopenDelayMinutes * 60_000);
const nextOpensAt = reopenAt > now ? reopenAt : now;
let nextClosesAt = new Date(nextOpensAt.getTime() + cfg.windowDurationHours * 3_600_000);
// Doc review + payment have already run, so the desk is ready to reopen NOW —
// office hours decide whether that is this afternoon or tomorrow morning. Past
// the last cycle before departure, nextCycleOpensAt returns null and we finish.
const officeHours: OfficeHours = {
windowOpenHour: cfg.windowOpenHour,
windowCloseHour: cfg.windowCloseHour,
};
const nextOpensAt = nextCycleOpensAt(
now,
officeHours,
schedule.scheduledDepartureDate,
);
if (nextOpensAt == null) {
await this.setPhase(schedule, { windowPhase: 'DONE' });
this.logger.log(
`Schedule ${schedule.id} not full but no cycle fits before departure — window done`,
);
return;
}
let nextClosesAt = new Date(
nextOpensAt.getTime() + cfg.windowDurationHours * 3_600_000,
);
if (nextClosesAt > schedule.scheduledDepartureDate) {
nextClosesAt = schedule.scheduledDepartureDate;
}
const sameBookingDay = eatDay(nextOpensAt) === eatDay(closesAt);
const beforeDeparture = nextOpensAt < schedule.scheduledDepartureDate;
if (sameBookingDay && beforeDeparture) {
await this.setPhase(schedule, {
windowPhase: 'PRE_WINDOW',
windowOpensAt: nextOpensAt,
windowClosesAt: nextClosesAt,
docReviewCompletedAt: null,
docReviewEndsAt: null,
paymentPhaseEndsAt: null,
});
this.logger.log(
`Schedule ${schedule.id} not full — window reopens at ${nextOpensAt.toISOString()}`,
);
} else {
await this.setPhase(schedule, { windowPhase: 'CLOSED_FOR_DAY' });
this.logger.log(
`Booking day over for schedule ${schedule.id} — remaining capacity is staff-managed`,
);
}
// Stays PRE_WINDOW (not CLOSED_FOR_DAY): the tick reopens it at nextOpensAt,
// whether that is later today or next morning after the office-hours break.
await this.setPhase(schedule, {
windowPhase: 'PRE_WINDOW',
windowOpensAt: nextOpensAt,
windowClosesAt: nextClosesAt,
docReviewCompletedAt: null,
docReviewEndsAt: null,
paymentPhaseEndsAt: null,
});
const sameDay = eatDay(nextOpensAt) === eatDay(now);
this.logger.log(
`Schedule ${schedule.id} not full — window reopens ${sameDay ? 'today' : 'next booking day'} at ${nextOpensAt.toISOString()}`,
);
}
private async tryAutoFinalize(scheduleId: string): Promise<void> {