fix train build issue

This commit is contained in:
Marshal
2026-08-29 19:26:09 +00:00
parent 6ac41cf078
commit ef1d310413

View File

@@ -19,6 +19,7 @@ import { combinedLocomotiveLimits } from '../train-scheduling/train-capacity.uti
import { ScheduleWagonAdjustmentLog } from '../train-schedules/entities/schedule-wagon-adjustment-log.entity';
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
import { TrainSet } from '../train-sets/entities/train-set.entity';
import { TrainSetLocomotive } from '../train-sets/entities/train-set-locomotive.entity';
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
@@ -504,7 +505,7 @@ export class TrainBuilderService {
if (locomotiveIds.length < 1) {
throw new BadRequestException('A train must be pulled by at least one locomotive');
}
await this.dataSource.transaction(async (manager) => {
const pending = await this.dataSource.transaction(async (manager) => {
const train = await this.getEditableTrain(manager, id);
const yard = await manager
.getRepository(Yard)
@@ -523,10 +524,76 @@ export class TrainBuilderService {
await manager
.getRepository(Train)
.update(train.id, { capacityTons: round(limits?.maxPullWeightTons ?? 0) });
// Capacity math reads the SET's locomotives, not the train's — push the
// new pull weight onto the live runs too, or they keep the old ceiling.
return this.syncLiveSchedulesAfterLocomotiveChange(manager, train.id, locomotiveIds);
});
// Re-derive FULL/reopen once committed — a bigger pull weight can free room
// on a schedule that had closed as FULL.
await this.reconcileWindowsAfterConsistChange(pending);
return this.getComposition(id);
}
/**
* Mirror a built train's locomotive change onto every LIVE (DRAFT/SCHEDULED)
* schedule formed from it. The three capacity axes are derived from
* `train_set_locomotives` (see trainSetLocomotiveLimits), which is snapshotted
* when the set is built and never re-synced — so adding a second locomotive
* raised `trains.capacity_tons` but left every existing schedule pulling on
* the old single-loco ceiling, still refusing bookings for want of weight.
*
* Only DRAFT/SCHEDULED runs follow the live train; DISPATCHED/ARRIVED render
* from their frozen snapshot and must not be disturbed (same rule as
* syncLiveScheduleAfterConsistChange).
*/
private async syncLiveSchedulesAfterLocomotiveChange(
manager: EntityManager,
trainId: string,
locomotiveIds: string[],
): Promise<PendingWindowCheck[]> {
const trainSets = await manager.getRepository(TrainSet).find({ where: { trainId } });
if (!trainSets.length) return [];
const schedules = await manager.getRepository(TrainSchedule).find({
where: {
trainSetId: In(trainSets.map((s) => s.id)),
status: In(['DRAFT', 'SCHEDULED']),
},
});
if (!schedules.length) return [];
// Only the sets still backing a live run — a set behind an ARRIVED schedule
// keeps the locomotives it actually ran with.
const liveSetIds = [...new Set(schedules.map((s) => s.trainSetId))];
const [primaryId] = locomotiveIds;
for (const trainSetId of liveSetIds) {
await manager.getRepository(TrainSetLocomotive).delete({ trainSetId });
await manager.getRepository(TrainSetLocomotive).save(
locomotiveIds.map((locomotiveId, index) =>
manager
.getRepository(TrainSetLocomotive)
.create({ trainSetId, locomotiveId, sequenceNo: index }),
),
);
// `locomotiveId` is the primary-locomotive fallback for single-loco reads.
await manager.getRepository(TrainSet).update(trainSetId, { locomotiveId: primaryId });
}
return schedules.map((s) => ({
scheduleId: s.id,
wasFull: s.bookingWindowStatus === 'FULL',
}));
}
/** {@link reconcileWindowAfterConsistChange} over several schedules. */
private async reconcileWindowsAfterConsistChange(
pending: PendingWindowCheck[],
): Promise<void> {
for (const check of pending) {
await this.reconcileWindowAfterConsistChange(check);
}
}
/**
* Edit a built train's display identity: name and fixed import/export run
* numbers. Mirrors the build-time number rules — the pair may not collide