mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 11:21:18 +00:00
enhance gate pass and freight payment handling in train scheduling
- Updated the logic in to ensure that a booking only earns its gate pass once the freight charges are settled. - Added logging for bookings that have not settled freight payment when securing gate passes. - Modified seeders to ensure that bookings have associated company profiles to prevent data inconsistencies. - Updated freight permissions to include new clearance actions for bookings. - Enhanced the UI to reflect changes in the clearance process, including new shipment request pages and improved status handling in the clearance action panel. - Adjusted the contract clearance list to accommodate both customs contracts and shipment bookings. - Improved the handling of GENERAL contracts in various components to ensure proper booking flow and visibility.
This commit is contained in:
@@ -219,6 +219,14 @@ export interface BatchBoardSchedule {
|
||||
export class BookingBatchService implements OnModuleInit {
|
||||
private readonly logger = new Logger(BookingBatchService.name);
|
||||
|
||||
/**
|
||||
* Serialises settle/top-up per schedule. The PAYMENT phase transition and the
|
||||
* tick's overdue backstop both call settleDueReservations for the same schedule
|
||||
* in the same second; without this they interleave and the top-up runs against a
|
||||
* schedule whose phase has already been concluded.
|
||||
*/
|
||||
private readonly scheduleLocks = new Map<string, Promise<void>>();
|
||||
|
||||
constructor(
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
private readonly bookingsRepository: BookingsRepository,
|
||||
@@ -1602,21 +1610,92 @@ export class BookingBatchService implements OnModuleInit {
|
||||
return anySettled;
|
||||
}
|
||||
|
||||
/** Durable settle: allocate paid / expire overdue reservations, then top up. */
|
||||
/**
|
||||
* Durable settle: allocate paid / expire overdue reservations, then top up the
|
||||
* freed capacity from the waiting list.
|
||||
*
|
||||
* Serialised per schedule. Two callers race here every time a payment phase
|
||||
* ends: `advanceImport`'s PAYMENT branch and the tick's `settleOverdueReservations`
|
||||
* backstop. Both read the same reserved rows in the same second, so without the
|
||||
* lock the second caller re-settles rows the first is mid-way through expiring,
|
||||
* and `concludeCycle` observes capacity that is neither pre- nor post-expiry.
|
||||
*/
|
||||
async settleDueReservations(scheduleId: string): Promise<void> {
|
||||
const anySettled = await this.settleReserved(scheduleId, false);
|
||||
// A settle that allocated/expired anything frees or fills capacity → re-run the
|
||||
// fill so the next waiting-list bookings get a fresh pay window (top-up).
|
||||
if (anySettled) {
|
||||
await this.withScheduleLock(scheduleId, () =>
|
||||
this.settleAndTopUp(scheduleId, false),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle, then keep promoting the waiting list until the train can take no more.
|
||||
* Returns whether anything settled.
|
||||
*
|
||||
* One top-up pass is not enough: expiring an N-wagon booking can free room for
|
||||
* several smaller ones, and reserving those can in turn leave room for the next
|
||||
* size down. Loop until a pass reserves nothing, so the batch ends with the train
|
||||
* as full as the pool allows — rather than leaving a booking stranded until the
|
||||
* next window cycle.
|
||||
*
|
||||
* Each round that opens a fresh pay window pushes `paymentPhaseEndsAt` out, so
|
||||
* `concludeCycle` cannot fire before the promoted customers' deadlines.
|
||||
*/
|
||||
private async settleAndTopUp(
|
||||
scheduleId: string,
|
||||
expireUnpaidUnknownDeadline: boolean,
|
||||
): Promise<boolean> {
|
||||
const anySettled = await this.settleReserved(
|
||||
scheduleId,
|
||||
expireUnpaidUnknownDeadline,
|
||||
);
|
||||
if (!anySettled) return false;
|
||||
|
||||
this.logger.log(
|
||||
`[BATCH] settle changed state on ${scheduleId} — running top-up fill for the waiting list`,
|
||||
);
|
||||
|
||||
// Bounded: every round either reserves at least one unit (shrinking the pool)
|
||||
// or breaks. The cap is a backstop against a pathological reserve/expire cycle.
|
||||
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.logger.log(
|
||||
`[BATCH] settle changed state on ${scheduleId} — running top-up fill for the waiting list`,
|
||||
`[BATCH] top-up promoted ${promoted} waiting booking(s) onto ${scheduleId} ` +
|
||||
`— payment phase extended for them`,
|
||||
);
|
||||
const topUpReserved = await this.topUpFill(scheduleId);
|
||||
// A top-up opened a fresh pay window for waiting bookings — push the
|
||||
// schedule's PAYMENT phase out so the window tick's concludeCycle doesn't
|
||||
// fire before those customers' new deadlines and expire them prematurely.
|
||||
if (topUpReserved > 0) {
|
||||
await this.extendPaymentPhaseForTopUp(scheduleId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `fn` with exclusive access to `scheduleId`. Concurrent callers await the
|
||||
* in-flight run rather than interleaving with it. Single-process only — a second
|
||||
* API replica would need a row lock on the schedule instead.
|
||||
*/
|
||||
private async withScheduleLock<T>(
|
||||
scheduleId: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const inFlight = this.scheduleLocks.get(scheduleId) ?? Promise.resolve();
|
||||
// Chain onto the previous holder; swallow its rejection so one failure does
|
||||
// not poison every later caller's lock.
|
||||
const run = inFlight.catch(() => undefined).then(fn);
|
||||
const gate = run.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
this.scheduleLocks.set(scheduleId, gate);
|
||||
try {
|
||||
return await run;
|
||||
} finally {
|
||||
// Last one out clears the slot so the map does not grow without bound.
|
||||
if (this.scheduleLocks.get(scheduleId) === gate) {
|
||||
this.scheduleLocks.delete(scheduleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1626,11 +1705,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
/** Allocate paid reservations, expire the rest, then top up. */
|
||||
async settleBatch(scheduleId: string): Promise<void> {
|
||||
this.removeTimeout(scheduleId);
|
||||
await this.settleReserved(scheduleId, true);
|
||||
const topUpReserved = await this.topUpFill(scheduleId);
|
||||
if (topUpReserved > 0) {
|
||||
await this.extendPaymentPhaseForTopUp(scheduleId);
|
||||
}
|
||||
await this.withScheduleLock(scheduleId, () =>
|
||||
this.settleAndTopUp(scheduleId, true),
|
||||
);
|
||||
void this.triggerWagonAllocation(scheduleId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user