implement intercity booking management and booking window websocket integration

This commit is contained in:
Marshal
2026-07-06 13:28:21 +00:00
parent 907f4edc0a
commit fed5f2f43f
46 changed files with 1772 additions and 101 deletions

View File

@@ -9,6 +9,7 @@ import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
import { NotificationsService } from '../notifications/notifications.service';
import { BookingBatchService } from './booking-batch.service';
import { BookingWindowGateway } from './booking-window.gateway';
import { TrainSchedulingService, effectiveWindowConfig } from './train-scheduling.service';
import { BATCH_TIMEZONE } from './booking-batch.constants';
import { eatDay, nextCycleOpensAt, type OfficeHours } from './batch-window.util';
@@ -40,6 +41,7 @@ export class BookingWindowService implements OnModuleInit {
private readonly bookingBatchService: BookingBatchService,
private readonly trainSchedulingService: TrainSchedulingService,
private readonly notifications: NotificationsService,
private readonly gateway: BookingWindowGateway,
) {}
async onModuleInit(): Promise<void> {
@@ -48,7 +50,10 @@ export class BookingWindowService implements OnModuleInit {
);
}
@Cron('* * * * *', { name: 'booking-window-tick', timeZone: BATCH_TIMEZONE })
// 10-second cadence: every transition is derived from persisted timestamps
// and applied idempotently, so a finer tick only shrinks the lag between a
// deadline passing and the phase actually moving (was a full minute).
@Cron('*/10 * * * * *', { name: 'booking-window-tick', timeZone: BATCH_TIMEZONE })
async tick(): Promise<void> {
if (this.ticking) return;
this.ticking = true;
@@ -89,9 +94,10 @@ export class BookingWindowService implements OnModuleInit {
await this.settleOverdueReservations();
// Legacy fill (DOMESTIC / pre-migration schedules) every 5th tick.
// Legacy fill (DOMESTIC / pre-migration schedules) every 5 minutes
// (30 ticks at the 10-second cadence).
this.tickCount += 1;
if (this.tickCount % 5 === 0) {
if (this.tickCount % 30 === 0) {
await this.bookingBatchService.runBatchFill();
}
} finally {
@@ -151,6 +157,9 @@ export class BookingWindowService implements OnModuleInit {
? await this.advanceExport(schedule, now)
: await this.advanceImport(schedule, cfg, now);
if (!advanced) return;
// Push the new window state to portal home / backoffice GL sections so
// they refresh instantly instead of waiting out their poll interval.
this.gateway.emitPhase(schedule);
}
}
@@ -169,7 +178,9 @@ export class BookingWindowService implements OnModuleInit {
await this.bookingBatchService.setWindow(schedule.id, 'OPEN');
schedule.bookingWindowStatus = 'OPEN';
}
await this.notifyWindowOpened(schedule);
// Fire-and-forget: a slow SMS/email gateway must not stall the tick loop
// (the `ticking` guard would otherwise delay every schedule's transition).
void this.notifyWindowOpened(schedule);
this.logger.log(`Export booking window opened for schedule ${schedule.id}`);
return true;
}
@@ -216,7 +227,8 @@ export class BookingWindowService implements OnModuleInit {
schedule.bookingWindowStatus = 'OPEN';
}
// Only announce the first opening of the day; reopen cycles don't re-notify.
if (schedule.bookingCycleNo === 1) await this.notifyWindowOpened(schedule);
// Fire-and-forget so a slow SMS/email gateway never stalls the tick loop.
if (schedule.bookingCycleNo === 1) void this.notifyWindowOpened(schedule);
this.logger.log(
`Import booking window opened for schedule ${schedule.id} (cycle ${schedule.bookingCycleNo})`,
);