solve priorty issue

This commit is contained in:
Marshal
2026-07-08 09:08:53 +00:00
parent 7bedfde6e7
commit 44023efb12
6 changed files with 770 additions and 28 deletions

View File

@@ -177,6 +177,8 @@ export interface BatchBoardSchedule {
/** Weight committed on the train (allocated + selected-for-batch). */
usedWeightTons: number;
maxWeightTons: number | null;
/** Wagon-slot cap for the train (locomotive/wagon-type derived). */
maxWagons: number | null;
};
counts: {
allocated: number;
@@ -290,6 +292,9 @@ export class BookingBatchService implements OnModuleInit {
* schedule-scoped — only the fill is day-level).
*/
async processRouteDay(group: RouteDayGroup): Promise<void> {
this.logger.log(
`[BATCH] processRouteDay START ${group.originYardId}->${group.destinationYardId} ${group.day}`,
);
const scheduleIds = await this.fillRouteDay(
group.originYardId,
group.destinationYardId,
@@ -863,7 +868,7 @@ export class BookingBatchService implements OnModuleInit {
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
}
: null,
capacity: this.computeBoardCapacity(items, loco),
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null),
counts: {
allocated: items.filter((i) => i.state === "ALLOCATED").length,
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
@@ -902,6 +907,7 @@ export class BookingBatchService implements OnModuleInit {
lengthMeters: number;
}>,
loco: Locomotive | null,
maxWagons: number | null,
): BatchBoardSchedule["capacity"] {
const allocated = items.filter((i) => i.state === "ALLOCATED");
const committed = items.filter(
@@ -918,6 +924,7 @@ export class BookingBatchService implements OnModuleInit {
Math.round(committed.reduce((sum, i) => sum + i.weightTons, 0) * 100) /
100,
maxWeightTons: loco ? Number(loco.maxPullWeightTons) : null,
maxWagons: maxWagons ?? null,
};
}
@@ -956,7 +963,7 @@ export class BookingBatchService implements OnModuleInit {
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
}
: null,
capacity: this.computeBoardCapacity(items, loco),
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null),
counts: {
allocated: items.filter((i) => i.state === "ALLOCATED").length,
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
@@ -1033,6 +1040,7 @@ export class BookingBatchService implements OnModuleInit {
const pool = await this.bookingsRepository.findBatchPool(scheduleId);
const units = this.groupConsolidatedPool(pool);
let armed = false;
let reservedThisPass = 0;
// Batch fill trace: caps + pool at entry. Kept on debug level — invaluable when
// reservations trickle instead of landing in one pass (a reserve() throwing
@@ -1087,18 +1095,33 @@ export class BookingBatchService implements OnModuleInit {
}
}
if (isGov) {
await this.allocate(scheduleId, booking, "gov");
if (partner) await this.allocate(scheduleId, partner, "gov");
} else {
await this.reserve(booking, scheduleId);
if (partner) await this.reserve(partner, scheduleId);
armed = true;
// Isolate each unit so a throw in reserve/allocate (e.g. billing hiccup)
// can't abort the whole top-up pass and leave the rest to trickle in one
// per tick. Log + skip the failing unit, keep going.
try {
if (isGov) {
await this.allocate(scheduleId, booking, "gov");
if (partner) await this.allocate(scheduleId, partner, "gov");
} else {
await this.reserve(booking, scheduleId);
if (partner) await this.reserve(partner, scheduleId);
armed = true;
}
budget.subtract(need, leg);
reservedThisPass += 1;
} catch (err) {
this.logger.error(
`[fillSchedule ${scheduleId}] reserve/allocate FAILED for ${booking.reference} ` +
`— skipping this unit, continuing: ${(err as Error).message}`,
);
continue;
}
budget.subtract(need, leg);
if (budget.maxRemaining().wagons <= 0) break; // every leg exhausted — nothing more can board
}
this.logger.log(
`[fillSchedule ${scheduleId}] reserved ${reservedThisPass}/${units.length} unit(s) this pass`,
);
if (budget.maxRemaining().wagons <= 0) await this.setWindow(scheduleId, "FULL");
if (armed) this.armSettle(scheduleId);
void this.triggerWagonAllocation(scheduleId);
@@ -1190,6 +1213,7 @@ export class BookingBatchService implements OnModuleInit {
`trains=${trains.map((t) => `${t.id}:${JSON.stringify(t.budget.maxRemaining())}`).join(",")} ` +
`poolSize=${pool.length} units=${units.length}`,
);
let reservedThisPass = 0;
for (const unit of units) {
const { primary: booking, partner } = unit;
@@ -1256,17 +1280,35 @@ export class BookingBatchService implements OnModuleInit {
continue;
}
if (isGov) {
await this.allocate(target.id, booking, "gov");
if (partner) await this.allocate(target.id, partner, "gov");
} else {
await this.reserve(booking, target.id);
if (partner) await this.reserve(partner, target.id);
target.armed = true;
// A throw here (e.g. a billing/invoice hiccup inside reserve) must NOT abort
// the whole pass — otherwise only the bookings before the failure get a pay
// window and the rest trickle in one-per-tick on later retries (the
// "selected one at a time / staggered" symptom). Isolate each unit: log +
// skip a failing one, keep reserving the others. The skipped unit stays in
// the pool and is retried next cycle.
try {
if (isGov) {
await this.allocate(target.id, booking, "gov");
if (partner) await this.allocate(target.id, partner, "gov");
} else {
await this.reserve(booking, target.id);
if (partner) await this.reserve(partner, target.id);
target.armed = true;
}
target.budget.subtract(need, legOn(target)!);
reservedThisPass += 1;
} catch (err) {
this.logger.error(
`[fillRouteDay] reserve/allocate FAILED for ${booking.reference} on ${target.id} ` +
`— skipping this unit, continuing the batch: ${(err as Error).message}`,
);
}
target.budget.subtract(need, legOn(target)!);
}
this.logger.log(
`[fillRouteDay ${originYardId}->${destinationYardId} ${day}] reserved ${reservedThisPass}/${units.length} unit(s) this pass`,
);
for (const t of trains) {
if (t.budget.maxRemaining().wagons <= 0) await this.setWindow(t.id, "FULL");
if (t.armed) this.armSettle(t.id);
@@ -1402,6 +1444,9 @@ export class BookingBatchService implements OnModuleInit {
const byId = new Map(reserved.map((b) => [b.id, b]));
const done = new Set<string>();
let anySettled = false;
this.logger.debug(
`[settleReserved ${scheduleId}] ${reserved.length} reserved booking(s) to settle`,
);
const isPaid = (b: Booking) =>
b.paymentStatus === "PAID" || b.status === "PAID";
@@ -1448,7 +1493,14 @@ export class BookingBatchService implements OnModuleInit {
/** Durable settle: allocate paid / expire overdue reservations, then top up. */
async settleDueReservations(scheduleId: string): Promise<void> {
const anySettled = await this.settleReserved(scheduleId, false);
if (anySettled) await this.fillSchedule(scheduleId);
// 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) {
this.logger.log(
`[BATCH] settle changed state on ${scheduleId} — running top-up fill for the waiting list`,
);
await this.fillSchedule(scheduleId);
}
}
// ---- settle (1h after a batch) -------------------------------------------
@@ -1637,6 +1689,11 @@ export class BookingBatchService implements OnModuleInit {
"PREPAID",
);
await this.notifier.payNow(booking, deadline);
this.logger.log(
`[BATCH] RESERVED ${booking.reference} (${this.wagonsFor(booking)}w, ` +
`priority ${booking.priorityScore ?? 0}) on schedule ${scheduleId}` +
`pay by ${deadline.toISOString()}`,
);
// Customer tracking: a wagon slot is reserved and the freight pay window is
// open. Doc-trigger path — silent no-op for bookings without milestone rows.
void this.completeTrackingMilestones(booking.id, [
@@ -1671,6 +1728,9 @@ export class BookingBatchService implements OnModuleInit {
selectedForBatchAt: null,
} as never);
});
this.logger.log(
`[BATCH] ALLOCATED ${booking.reference} (${reason}) to train on schedule ${scheduleId}`,
);
this.notifier.secured(booking, reason);
void this.triggerWagonAllocation(scheduleId);
void this.markWagonAllocatedMilestone(booking.id);
@@ -1738,6 +1798,10 @@ export class BookingBatchService implements OnModuleInit {
// source-agnostic.
await this.billing.expirePayable(Freight.InvoiceSource.Booking, booking.id, "PREPAID");
this.notifier.expired(booking);
this.logger.log(
`[BATCH] EXPIRED ${booking.reference} — payment window passed; freed its ` +
`wagons back to the pool for top-up`,
);
}
/**
@@ -1793,6 +1857,12 @@ export class BookingBatchService implements OnModuleInit {
corridorYards,
group.day,
);
if (unaccepted.length > 0) {
this.logger.log(
`[BATCH] doc-review end: expiring ${unaccepted.length} un-accepted booking(s) ` +
`on ${group.originYardId}->${group.destinationYardId} ${group.day}`,
);
}
for (const booking of unaccepted) {
await this.bookingsRepository.update(booking.id, {
status: "EXPIRED",
@@ -1807,8 +1877,7 @@ export class BookingBatchService implements OnModuleInit {
.catch(() => undefined);
this.notifier.expired(booking);
this.logger.log(
`Expired unaccepted booking ${booking.reference}:${booking.id} at doc-review end ` +
`(${group.originYardId}->${group.destinationYardId} ${group.day})`,
`[BATCH] EXPIRED (unaccepted) ${booking.reference}:${booking.id} at doc-review end`,
);
}
}