mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
fix issue
This commit is contained in:
@@ -1328,10 +1328,9 @@ describe('BookingBatchService — built-train wagon capacity', () => {
|
||||
await expect(service.isScheduleFull(scheduleId)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('is FULL for the trade direction once the border edge is sold out, even with home legs free', async () => {
|
||||
// Export b→c holds every wagon of the border crossing: no further export
|
||||
// can board anywhere (they all must ride that edge), so the window closes —
|
||||
// while intercity keeps booking the free a→b leg through the per-leg budget.
|
||||
it('is NOT full when the border edge is sold out but a home leg still has room', async () => {
|
||||
// FULL is corridor-wide now: b→dj holds every wagon, but a→b is empty, so
|
||||
// sub-corridor bookings can still sell that leg — the window stays open.
|
||||
const { service } = buildService({
|
||||
physicalWagons: 2,
|
||||
routeStops: ['yard-a', 'yard-b', 'yard-dj'],
|
||||
@@ -1345,6 +1344,23 @@ describe('BookingBatchService — built-train wagon capacity', () => {
|
||||
reservedBooking('b2', { origin: 'yard-b', dest: 'yard-dj' }),
|
||||
],
|
||||
});
|
||||
await expect(service.isScheduleFull(scheduleId)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('is FULL once every leg of the corridor is sold out', async () => {
|
||||
const { service } = buildService({
|
||||
physicalWagons: 2,
|
||||
routeStops: ['yard-a', 'yard-b', 'yard-dj'],
|
||||
yardCountries: {
|
||||
'yard-a': 'ETHIOPIA',
|
||||
'yard-b': 'ETHIOPIA',
|
||||
'yard-dj': 'DJIBOUTI',
|
||||
},
|
||||
reserved: [
|
||||
reservedBooking('b1', { origin: 'yard-a', dest: 'yard-dj' }),
|
||||
reservedBooking('b2', { origin: 'yard-a', dest: 'yard-dj' }),
|
||||
],
|
||||
});
|
||||
await expect(service.isScheduleFull(scheduleId)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -4594,14 +4594,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* FULL is DIRECTIONAL: the schedule's trade direction is full when the
|
||||
* border-crossing edge (which every export/import must ride) can't take one
|
||||
* FULL is CORRIDOR-WIDE: the train is full only when NO leg can take one
|
||||
* more minimal wagon on any axis — slots for built trains (the consist is
|
||||
* the capacity, weight/length settled at build), all three axes otherwise
|
||||
* (PW2: weight binds at 37 wagons = 3522.4T of 3500+90T, slots bind at 44).
|
||||
* Home-side legs may still run empty; intercity ride-alongs keep filling
|
||||
* them via the per-leg budget and never consult this flag. Domestic routes
|
||||
* (no border) are full only when every edge is closed.
|
||||
* A full DCT→Dire leg alone does NOT close the window while Dire→GMP still
|
||||
* has room — sub-corridor bookings keep selling the open legs.
|
||||
*/
|
||||
async isScheduleFull(scheduleId: string): Promise<boolean> {
|
||||
const schedule =
|
||||
@@ -4652,13 +4650,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
/** See {@link isScheduleFull} — same check for callers that already hold the full graph. */
|
||||
private async isTrainFull(schedule: TrainSchedule): Promise<boolean> {
|
||||
// "Full" means full FOR THE TRAIN'S TRADE DIRECTION. Every export and
|
||||
// every import must cross the ET↔DJ border edge, so once that edge can't
|
||||
// take one more minimal wagon the booking window may close — even while
|
||||
// home-side legs still run empty. Intercity ride-alongs never consult this
|
||||
// flag; they keep booking the free legs through the per-leg budget.
|
||||
// A single-country (domestic) corridor has no mandatory edge, so it is
|
||||
// full only when EVERY edge is closed on some axis.
|
||||
// Full only when EVERY edge is closed on some axis: a full border edge
|
||||
// still leaves the home-side legs bookable by sub-corridor cargo, so the
|
||||
// window must stay open until not even the smallest wagon fits anywhere.
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const physicalWagons = await this.builtTrainWagonCount(schedule);
|
||||
let limits: TrainLimits;
|
||||
@@ -4681,42 +4675,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
||||
const minNeed = this.minPerWagonNeed(wagonDims);
|
||||
const border = await this.borderLeg(budget.stops);
|
||||
if (border) {
|
||||
return !budget.fits(
|
||||
{
|
||||
wagons: 1,
|
||||
weightTons: minNeed.grossWeightTons,
|
||||
lengthMeters: minNeed.lengthMeters,
|
||||
},
|
||||
border,
|
||||
);
|
||||
}
|
||||
return budget.isExhausted(minNeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* The corridor's single border-crossing edge (last home-country stop → first
|
||||
* far-country stop), or null when every stop is in one country. This is the
|
||||
* edge every EXPORT and IMPORT booking must ride, whichever sub-corridor it
|
||||
* books — which makes it the train's directional fullness gauge.
|
||||
*/
|
||||
private async borderLeg(stops: string[]): Promise<CorridorLeg | null> {
|
||||
if (stops.length < 2) return null;
|
||||
const yards = await this.dataSource
|
||||
.getRepository(Yard)
|
||||
.find({ where: { id: In(stops) } });
|
||||
const countryOf = new Map(yards.map((y) => [y.id, y.country]));
|
||||
const first = countryOf.get(stops[0]);
|
||||
if (!first) return null;
|
||||
const crossIdx = stops.findIndex((id) => {
|
||||
const country = countryOf.get(id);
|
||||
return country != null && country !== first;
|
||||
});
|
||||
if (crossIdx <= 0) return null;
|
||||
return { fromEdge: crossIdx - 1, toEdge: crossIdx };
|
||||
}
|
||||
|
||||
/**
|
||||
* Smallest gross weight / shortest length one more wagon could add: the
|
||||
* lightest wagon type at its rated payload. Feeds CorridorBudget.isExhausted,
|
||||
@@ -4762,6 +4723,31 @@ export class BookingBatchService implements OnModuleInit {
|
||||
// nothing can board.
|
||||
if (await this.isTrainFull(schedule)) return;
|
||||
|
||||
// FULL concluded the cycle (phase DONE) and DONE rows are skipped by the
|
||||
// window tick forever — so when wagons free up before departure, restart
|
||||
// the cycle or nobody (customer or batch) can ever book the freed space.
|
||||
// ponytail: reopens now and closes at departure; the office-hours clamp
|
||||
// reapplies on the next conclude cycle.
|
||||
const departure = schedule.scheduledDepartureDate;
|
||||
if (
|
||||
schedule.windowPhase === "DONE" &&
|
||||
["DRAFT", "SCHEDULED"].includes(schedule.status) &&
|
||||
departure &&
|
||||
departure.getTime() > Date.now()
|
||||
) {
|
||||
await this.dataSource.getRepository(TrainSchedule).update(scheduleId, {
|
||||
windowPhase: "PRE_WINDOW",
|
||||
windowOpensAt: new Date(),
|
||||
windowClosesAt: departure,
|
||||
});
|
||||
await this.setWindow(scheduleId, "OPEN");
|
||||
this.logger.log(
|
||||
`[BATCH] ${scheduleId} FULL cleared after wagons freed — window revived ` +
|
||||
`(PRE_WINDOW, reopens immediately, closes at departure)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const customerWindowOpen =
|
||||
schedule.windowPhase == null || schedule.windowPhase === "OPEN";
|
||||
await this.setWindow(scheduleId, customerWindowOpen ? "OPEN" : "CLOSED");
|
||||
|
||||
@@ -2031,6 +2031,11 @@ export class TrainSchedulingService {
|
||||
});
|
||||
});
|
||||
|
||||
// Freed wagons may un-full the train — re-derive the window status (this
|
||||
// also revives a DONE window pre-departure so the freed space is bookable
|
||||
// again for import/export).
|
||||
await this.bookingBatchService?.refreshWindowStatus(scheduleId);
|
||||
|
||||
await this.trainCompositionRemovalLogRepository.create({
|
||||
scheduleId,
|
||||
bookingId,
|
||||
|
||||
Reference in New Issue
Block a user