From 9cd29a21ed1385c6d94182279e84eec2bc62bb9e Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Fri, 28 Aug 2026 11:15:18 +0000 Subject: [PATCH] feat(train-scheduling): log leg-slot board/alight events at checkpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leg slots (stampSlotLegs — bookings boarding/alighting mid-corridor) reaching their board/alight yard produced no ScheduleWagonAdjustmentLog row, unlike planned couples/cuts. Without it the marshalling document has no way to show a wagon coupling in already loaded, or uncoupling with cargo, at a mid-corridor stop. recordCheckpoint now logs ADD at the board yard and REMOVE at the alight yard for these slots, deduped against existing rows since passedYardIds re-includes earlier stops on every checkpoint call. Purely observational — the physical wagon was already pinned to the slot at schedule-build time, so no wagon/consist state changes. Stage 3 of the multi-stop marshalling plan (per-event numbered docs); stages 4-6 (generalized doc builder, endpoints, UI) still open. --- .../services/train-scheduling.service.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts index 7312df2da..1fc0da319 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/services/train-scheduling.service.ts @@ -5007,6 +5007,76 @@ export class TrainSchedulingService { await manager.getRepository(ScheduleWagonAdjustmentLog).save(coupleLogRows); } } + + // Leg slots (booking legs boarding/alighting mid-corridor — see + // stampSlotLegs) reaching their board/alight yard here: logged same as + // planned couples/cuts above, so the marshalling document can show + // what coupled ALREADY LOADED / uncoupled WITH cargo at this stop. + // Purely observational — their physical wagon was already pinned to + // the slot at schedule-build time (assignPhysicalWagonsToSlots), so + // nothing here changes wagon state, only the log. Dedupe against + // existing rows (not a wagon-state flag, unlike the couple/cut blocks + // above) since passedYardIds re-includes earlier stops on every call. + const legSlotsHere = (schedule.trainSet?.wagons ?? []).filter( + (slot) => + slot.physicalWagonId && + ((slot.boardYardId && passedYardIds.includes(slot.boardYardId)) || + (slot.alightYardId && passedYardIds.includes(slot.alightYardId))), + ); + if (legSlotsHere.length && builtTrainId) { + const legWagonIds = [ + ...new Set(legSlotsHere.map((slot) => slot.physicalWagonId!)), + ]; + const legWagonById = new Map( + ( + await manager.getRepository(Wagon).find({ where: { id: In(legWagonIds) } }) + ).map((w) => [w.id, w]), + ); + const alreadyLogged = new Set( + ( + await manager.getRepository(ScheduleWagonAdjustmentLog).find({ + where: { + trainScheduleId: scheduleId, + wagonId: In(legWagonIds), + action: In(['ADD', 'REMOVE']), + }, + }) + ).map((row) => `${row.wagonId}:${row.action}:${row.yardId}`), + ); + const legLogRows: ScheduleWagonAdjustmentLog[] = []; + for (const slot of legSlotsHere) { + const wagon = legWagonById.get(slot.physicalWagonId!); + if (!wagon) continue; + const events: Array<{ action: 'ADD' | 'REMOVE'; yardId: string }> = []; + if (slot.boardYardId && passedYardIds.includes(slot.boardYardId)) { + events.push({ action: 'ADD', yardId: slot.boardYardId }); + } + if (slot.alightYardId && passedYardIds.includes(slot.alightYardId)) { + events.push({ action: 'REMOVE', yardId: slot.alightYardId }); + } + for (const { action, yardId } of events) { + const key = `${wagon.id}:${action}:${yardId}`; + if (alreadyLogged.has(key)) continue; + alreadyLogged.add(key); + legLogRows.push( + manager.getRepository(ScheduleWagonAdjustmentLog).create({ + trainScheduleId: scheduleId, + trainId: builtTrainId, + action, + wagonId: wagon.id, + wagonNumber: wagon.wagonNumber, + adjustedByUserId: null, + yardId, + occurredAt, + }), + ); + } + } + if (legLogRows.length) { + await manager.getRepository(ScheduleWagonAdjustmentLog).save(legLogRows); + } + } + await manager .getRepository(Wagon) .createQueryBuilder()