From ef1d31041370655c0a7b04c8865295a2f40e3272 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sat, 29 Aug 2026 19:26:09 +0000 Subject: [PATCH] fix train build issue --- .../modules/trains/train-builder.service.ts | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/apps/edr-freight-api/src/modules/trains/train-builder.service.ts b/apps/edr-freight-api/src/modules/trains/train-builder.service.ts index 7b28a8582..26c29499d 100644 --- a/apps/edr-freight-api/src/modules/trains/train-builder.service.ts +++ b/apps/edr-freight-api/src/modules/trains/train-builder.service.ts @@ -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 { + 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 { + 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