mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 13:28:11 +00:00
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:
@@ -497,6 +497,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const linked =
|
||||
await this.trainScheduleBookingsRepository.existsForBooking(bookingId);
|
||||
if (!linked) {
|
||||
if (await this.holdIfWagonShort(booking.trainScheduleId, booking)) return;
|
||||
await this.allocate(booking.trainScheduleId, booking, "paid");
|
||||
this.logger.log(
|
||||
`Linked PAID booking ${booking.reference ?? bookingId} to schedule ${booking.trainScheduleId}`,
|
||||
@@ -783,6 +784,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const unlinked =
|
||||
await this.bookingsRepository.findPaidUnlinkedForSchedule(scheduleId);
|
||||
for (const booking of unlinked) {
|
||||
// Held on purpose (paid, no wagon free) — the cron must not undo it.
|
||||
if (booking.schedulingStatus === "WAITING_FOR_WAGON") continue;
|
||||
if (await this.holdIfWagonShort(scheduleId, booking)) continue;
|
||||
await this.allocate(scheduleId, booking, "paid");
|
||||
this.logger.log(
|
||||
`Reconciled PAID booking ${booking.reference ?? booking.id} → schedule ${scheduleId}`,
|
||||
@@ -1050,7 +1054,11 @@ export class BookingBatchService implements OnModuleInit {
|
||||
s.ruleWindowDurationHours,
|
||||
liveCfg.windowDurationHours,
|
||||
),
|
||||
reopenDelayMinutes: num(s.ruleReopenDelayMinutes, liveCfg.reopenDelayMinutes),
|
||||
// Frozen doc-review + payment sum; legacy rows fall back to the live sum.
|
||||
reopenGapMinutes: num(
|
||||
s.ruleReopenDelayMinutes,
|
||||
liveCfg.docReviewMinutes + liveCfg.paymentWindowMinutes,
|
||||
),
|
||||
importWindowLeadDays: num(
|
||||
s.ruleImportWindowLeadDays,
|
||||
liveCfg.importWindowLeadDays,
|
||||
@@ -1894,7 +1902,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
done.add(booking.id);
|
||||
if (isPaid(booking)) {
|
||||
await this.allocate(scheduleId, booking, "paid");
|
||||
if (!(await this.holdIfWagonShort(scheduleId, booking))) {
|
||||
await this.allocate(scheduleId, booking, "paid");
|
||||
}
|
||||
anySettled = true;
|
||||
} else if (isExpired(booking)) {
|
||||
await this.expire(booking);
|
||||
@@ -1920,6 +1930,29 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conclude-time retry: promote whatever still fits from the route-day waiting
|
||||
* list, opening fresh pay windows. Returns how many commercial units got
|
||||
* reserved — corridor-wide, since the fill is day-level and may reserve onto a
|
||||
* sibling train; the caller must check `hasLiveReservations` for its OWN
|
||||
* schedule before deciding to stay in PAYMENT.
|
||||
*/
|
||||
async fillFromWaitingList(scheduleId: string): Promise<number> {
|
||||
return this.withScheduleLock(scheduleId, async () => {
|
||||
let promoted = 0;
|
||||
for (let round = 0; round < 10; round += 1) {
|
||||
const reservedThisRound = await this.topUpFill(scheduleId);
|
||||
if (reservedThisRound <= 0) break;
|
||||
promoted += reservedThisRound;
|
||||
await this.extendPaymentPhaseForTopUp(scheduleId);
|
||||
}
|
||||
if (promoted > 0) {
|
||||
this.notifyBoardChanged(scheduleId, "conclude_waiting_list_fill");
|
||||
}
|
||||
return promoted;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle, then keep promoting the waiting list until the train can take no more.
|
||||
* Returns whether anything settled.
|
||||
@@ -2050,7 +2083,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { paymentStatus: "PAID" });
|
||||
await this.allocate(booking.trainScheduleId, booking, "paid");
|
||||
if (!(await this.holdIfWagonShort(booking.trainScheduleId, booking))) {
|
||||
await this.allocate(booking.trainScheduleId, booking, "paid");
|
||||
}
|
||||
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(
|
||||
booking.trainScheduleId,
|
||||
@@ -2112,7 +2147,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
await manager.getRepository(Booking).update(bookingId, {
|
||||
trainScheduleId: newScheduleId,
|
||||
status: restoredStatus,
|
||||
schedulingStatus: "ELIGIBLE",
|
||||
// A paid booking still hunting for a wagon keeps its flag through the
|
||||
// move — it only clears when wagons are actually assigned.
|
||||
schedulingStatus:
|
||||
booking.schedulingStatus === "WAITING_FOR_WAGON"
|
||||
? "WAITING_FOR_WAGON"
|
||||
: "ELIGIBLE",
|
||||
paymentDeadline: null,
|
||||
selectedForBatchAt: null,
|
||||
} as never);
|
||||
@@ -2254,6 +2294,50 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
/** Allocate a booking to the schedule's train (creates the TrainScheduleBooking link). */
|
||||
/**
|
||||
* Fleet preflight shared by every single-booking paid-allocation path: when
|
||||
* no wagon of the booking's required type is free, hold it OUT of the train
|
||||
* instead of linking — it stays PAID + unlinked in the (route, day) pool,
|
||||
* flagged WAITING_FOR_WAGON, and staff place it on any same-day schedule from
|
||||
* the workspace "Paid · unassigned" panel once a wagon frees up. Returns true
|
||||
* when the booking was held. Consolidated pairs are exempt (the shared wagon
|
||||
* is both-or-neither and settles atomically in settleReserved).
|
||||
*/
|
||||
private async holdIfWagonShort(
|
||||
scheduleId: string,
|
||||
booking: Booking,
|
||||
): Promise<boolean> {
|
||||
if (booking.consolidationPartnerId) return false;
|
||||
const shortage =
|
||||
await this.trainSchedulingService.previewPaidBookingWagonShortage(
|
||||
scheduleId,
|
||||
booking.id,
|
||||
);
|
||||
if (!shortage) return false;
|
||||
|
||||
await this.dataSource.getRepository(Booking).update(booking.id, {
|
||||
status: "PAID",
|
||||
paymentStatus: "PAID",
|
||||
schedulingStatus: "WAITING_FOR_WAGON",
|
||||
paymentDeadline: null,
|
||||
selectedForBatchAt: null,
|
||||
} as never);
|
||||
// Payment landed — record it even though nothing boards yet. The wagon
|
||||
// milestone stays pending until staff assign one.
|
||||
void this.completeTrackingMilestones(booking.id, [
|
||||
"FREIGHT_PAYMENT_PENDING",
|
||||
"FREIGHT_PAYMENT_SETTLED",
|
||||
]);
|
||||
this.logger.warn(
|
||||
`PAID booking ${booking.reference ?? booking.id} is WAITING FOR WAGON: ` +
|
||||
`needs ${shortage.wagonsNeeded} × ${shortage.wagonTypeCodes}, ` +
|
||||
`${shortage.wagonsAvailable} available (short ${shortage.wagonsShort}). ` +
|
||||
`Held in the day pool for manual placement.`,
|
||||
);
|
||||
this.notifyBoardChanged(scheduleId, "booking_waiting_wagon");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async allocate(
|
||||
scheduleId: string,
|
||||
booking: Booking,
|
||||
@@ -2358,7 +2442,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
`[BATCH] expire skipped for ${booking.reference} — payment already ` +
|
||||
`landed; allocating on schedule ${paidScheduleId} instead`,
|
||||
);
|
||||
await this.allocate(paidScheduleId, fresh, "paid");
|
||||
if (!(await this.holdIfWagonShort(paidScheduleId, fresh))) {
|
||||
await this.allocate(paidScheduleId, fresh, "paid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user