remove reopen delay minutes from global rules and update related types

- Removed the  field from  and related components.
- Updated  to reflect the removal of the reopen delay input field.
- Modified  to include new train number fields:  and .
- Added  interface to manage active schedules with trade direction.
- Introduced  interface to track wagon shortages in bookings.
- Updated  logic to ensure consistent UI state representation.
- Created migrations to drop the  column and add  and  columns to the  table.
- Added tests for the new booking window display logic and wagon planning functionality.
This commit is contained in:
Marshal
2026-07-15 09:13:02 +00:00
parent 9be7f356f0
commit 11771e5f92
39 changed files with 1731 additions and 269 deletions

View File

@@ -17,7 +17,12 @@ 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';
import {
clampCloseToOfficeHours,
eatDay,
nextCycleOpensAt,
type OfficeHours,
} from './batch-window.util';
import { type BookingWindowConfig } from './booking-window.config';
/**
@@ -297,6 +302,17 @@ export class BookingWindowService implements OnModuleInit {
// (or allocating government) — skipped automatically for everyone who fits
// is handled inside the fill (all fit → all reserved → all notified).
await this.bookingBatchService.processRouteDay(routeDay);
// Batch reserved nobody (empty pool, or it allocated without pay windows):
// a PAYMENT phase with nobody to pay is a dead hour with the window shut.
// Conclude straight away — full → DONE, otherwise reopen per office hours.
if (!(await this.bookingBatchService.hasLiveReservations(schedule.id))) {
this.logger.log(
`[WINDOW] ${schedule.id} DOC_REVIEW→PAYMENT — batch reserved nothing; ` +
`skipping the empty payment phase and concluding the cycle`,
);
await this.concludeCycle(schedule, cfg, now);
return true;
}
this.logger.log(
`[WINDOW] ${schedule.id} DOC_REVIEW→PAYMENT — batch ran; payment phase ` +
`until ${paymentPhaseEndsAt.toISOString()}`,
@@ -353,7 +369,10 @@ export class BookingWindowService implements OnModuleInit {
return false;
}
/** After settle: full → finalize + DONE; space left → reopen same day or close for the day. */
/**
* After settle: full → finalize + DONE; waiting bookings still fit → fresh pay
* window, back to PAYMENT; otherwise reopen (office hours decide when) or DONE.
*/
private async concludeCycle(
schedule: TrainSchedule,
cfg: BookingWindowConfig,
@@ -386,6 +405,31 @@ export class BookingWindowService implements OnModuleInit {
if (fresh) schedule.bookingWindowStatus = fresh.bookingWindowStatus;
}
// The window reopens only once the waiting list is exhausted: a booking can
// still reach the pool mid-payment (late doc accept, consolidation partner),
// so retry the batch before reopening. Anything that fits gets a fresh pay
// window and the cycle stays in PAYMENT; check live reservations on THIS
// schedule because the day-level fill may have reserved onto a sibling.
// Waiting bookings that fit no train stay pooled and the window reopens.
const promoted = await this.bookingBatchService.fillFromWaitingList(schedule.id);
if (
promoted > 0 &&
(await this.bookingBatchService.hasLiveReservations(schedule.id))
) {
let paymentPhaseEndsAt = new Date(
now.getTime() + cfg.paymentWindowMinutes * 60_000,
);
if (paymentPhaseEndsAt > schedule.scheduledDepartureDate) {
paymentPhaseEndsAt = schedule.scheduledDepartureDate;
}
await this.setPhase(schedule, { windowPhase: 'PAYMENT', paymentPhaseEndsAt });
this.logger.log(
`[WINDOW] ${schedule.id} conclude → waiting list still had bookings that ` +
`fit — back in PAYMENT until ${paymentPhaseEndsAt.toISOString()}, no reopen yet`,
);
return;
}
// 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.
@@ -413,6 +457,9 @@ export class BookingWindowService implements OnModuleInit {
let nextClosesAt = new Date(
nextOpensAt.getTime() + cfg.windowDurationHours * 3_600_000,
);
// Office hours end a running window early: never let the duration outlive
// the desk close (open 16:00, 3h, desk 817 → closes 17:00).
nextClosesAt = clampCloseToOfficeHours(nextOpensAt, nextClosesAt, officeHours);
if (nextClosesAt > schedule.scheduledDepartureDate) {
nextClosesAt = schedule.scheduledDepartureDate;
}