import type Vorpal from "vorpal"; import { DataSource } from "typeorm"; import { WagonStatus } from "@edr/types"; import type { CommandContext } from "./types"; import { Yard } from "../../modules/rule-engine/entities/yard.entity"; import { Route } from "../../modules/routes/entities/route.entity"; import { RouteMilestone } from "../../modules/routes/entities/route-milestone.entity"; import { Locomotive } from "../../modules/locomotives/entities/locomotive.entity"; import { Wagon } from "../../modules/wagons/entities/wagon.entity"; import { WagonType } from "../../modules/wagon-types/entities/wagon-type.entity"; import { TrainSet } from "../../modules/train-sets/entities/train-set.entity"; import { TrainSetLocomotive } from "../../modules/train-sets/entities/train-set-locomotive.entity"; import { TrainSetWagon } from "../../modules/train-sets/entities/train-set-wagon.entity"; import { TrainSchedule } from "../../modules/train-schedules/entities/train-schedule.entity"; async function nextSequence(ds: DataSource, pattern: string): Promise { const like = pattern.replace(/\*/g, "%"); const raw = await ds.query( `SELECT "train_number" FROM "freight"."train_schedules" WHERE "train_number" LIKE $1 AND "deleted_at" IS NULL ORDER BY "train_number" DESC LIMIT 1`, [like.replace(/%/g, "") + "%"], ); if (raw.length === 0) return 1; const ref: string = raw[0].train_number; const num = parseInt(ref.replace(pattern.split("*")[0], ""), 10); return isNaN(num) ? 1 : num + 1; } async function nextRouteSeq(ds: DataSource, prefix: string): Promise { const raw = await ds.query( `SELECT "name" FROM "freight"."routes" WHERE "name" LIKE $1 AND "deleted_at" IS NULL ORDER BY "name" DESC LIMIT 1`, [prefix + "%"], ); if (raw.length === 0) return 1; const num = parseInt(raw[0].name.replace(prefix, ""), 10); return isNaN(num) ? 1 : num + 1; } async function nextWagonSeq(ds: DataSource, prefix: string): Promise { const raw = await ds.query( `SELECT "wagon_number" FROM "freight"."wagons" WHERE "wagon_number" LIKE $1 AND "deleted_at" IS NULL ORDER BY "wagon_number" DESC LIMIT 1`, [prefix + "%"], ); if (raw.length === 0) return 1; const num = parseInt(raw[0].wagon_number.replace(prefix, ""), 10); return isNaN(num) ? 1 : num + 1; } export function registerSeedTestSchedules( vorpal: Vorpal, ctx: CommandContext, ): void { vorpal .command("seed:test-schedules", "Seed train schedules with routes, wagons, and all deps for booking") .option("-n, --count ", "Number of schedules to create (default: 3)") .option("--direction ", "IMPORT,EXPORT (default: both)") .option("--status ", "DRAFT,SCHEDULED,DISPATCHED (default: SCHEDULED)") .option("--days-ahead ", "Days from now for departure (default: 3)") .action(async function (this: any, args: any) { const { app } = ctx; const ds = app.get(DataSource); const count = Math.max(1, Math.min(10, parseInt(args.options?.count ?? "3", 10))); const directionList = (args.options?.direction ?? "IMPORT,EXPORT") .split(",").map((s: string) => s.toUpperCase().trim()) .filter((s: string) => s === "IMPORT" || s === "EXPORT"); const statusList = (args.options?.status ?? "SCHEDULED") .split(",").map((s: string) => s.toUpperCase().trim()) .filter((s: string) => s === "DRAFT" || s === "SCHEDULED" || s === "DISPATCHED"); const daysAhead = Math.max(0, parseInt(args.options?.daysAhead ?? "3", 10)); if (directionList.length === 0 || statusList.length === 0) { this.log("error: at least one direction and status required"); return; } const yards = await ds.getRepository(Yard).find({ where: { isActive: true } }); const yardByCode = new Map(yards.map((y) => [y.code.toUpperCase(), y])); const djibouti = yardByCode.get("DJIBOUTI") ?? yards.find((y) => y.country === "Djibouti"); const addis = yardByCode.get("ADDIS_ABABA") ?? yards.find((y) => y.country === "Ethiopia"); if (!djibouti || !addis) { this.log("error: need at least one Djibouti and one Ethiopia yard"); return; } const wagonTypes = await ds.getRepository(WagonType).find({ where: { isActive: true } }); if (wagonTypes.length === 0) { this.log("error: no wagon types found — seed reference data first"); return; } const wagonType = wagonTypes[0]; const wagonCapacity = Number(wagonType.capacityTons) || 70; const wagonLength = Number(wagonType.lengthMeters) || 14; const tareWeight = Number(wagonType.tareWeightTons) || 14; const locomotiveRepo = ds.getRepository(Locomotive); const scheduleRepo = ds.getRepository(TrainSchedule); const trainSetRepo = ds.getRepository(TrainSet); const wagonRepo = ds.getRepository(Wagon); const routeRepo = ds.getRepository(Route); const milestoneRepo = ds.getRepository(RouteMilestone); let nextTrainNum = await nextSequence(ds, "TST-SCH-*"); const routePrefix = "TST-RTE-"; let nextRouteNum = await nextRouteSeq(ds, routePrefix); const now = new Date(); const travelHours = 11; const intermediateYards = yards.filter( (y) => y.id !== djibouti.id && y.id !== addis.id, ); let loco = await locomotiveRepo.findOne({ where: { code: "TST-LOCO-01" } }); if (!loco) { loco = await locomotiveRepo.save( locomotiveRepo.create({ code: "TST-LOCO-01", name: "Test Locomotive", locomotiveType: "DIESEL", maxPullWeightTons: 4200, maxTrainLengthMeters: 760, status: "AVAILABLE", currentYardId: djibouti.id, }), ); } for (let i = 0; i < count; i++) { const seq = nextTrainNum + i; const trainNumber = `TST-SCH-${String(seq).padStart(5, "0")}`; const dir = directionList[i % directionList.length]; const status = statusList[i % statusList.length]; const isDispatched = status === "DISPATCHED"; const originYard = dir === "IMPORT" ? djibouti : addis; const destYard = dir === "IMPORT" ? addis : djibouti; const routeName = `${routePrefix}${String(nextRouteNum + i).padStart(3, "0")}`; const departure = new Date(now); departure.setDate(departure.getDate() + daysAhead + i); departure.setHours(7, 0, 0, 0); const arrival = new Date(departure.getTime() + travelHours * 60 * 60 * 1000); const route = await routeRepo.save( routeRepo.create({ name: routeName, originYardId: originYard.id, destinationYardId: destYard.id, isActive: true, }), ); await milestoneRepo.save( milestoneRepo.create({ routeId: route.id, yardId: originYard.id, sequenceNo: 1 }), ); for (const [mi, y] of intermediateYards.entries()) { await milestoneRepo.save( milestoneRepo.create({ routeId: route.id, yardId: y.id, sequenceNo: (mi + 1) * 2 }), ); } await milestoneRepo.save( milestoneRepo.create({ routeId: route.id, yardId: destYard.id, sequenceNo: (intermediateYards.length + 1) * 2, }), ); const totalWagonWeight = 4 * (tareWeight + 20); const trainSet = await trainSetRepo.save( trainSetRepo.create({ locomotiveId: loco.id, totalWeightTons: totalWagonWeight, totalLengthMeters: wagonLength * 4, wagonCount: 4, status: isDispatched ? "DISPATCHED" : status === "DRAFT" ? "DRAFT" : "ASSIGNED", }), ); await ds.getRepository(TrainSetLocomotive).save( ds.getRepository(TrainSetLocomotive).create({ trainSetId: trainSet.id, locomotiveId: loco.id, sequenceNo: 0, }), ); const schedule = await scheduleRepo.save( scheduleRepo.create({ trainSetId: trainSet.id, routeId: route.id, originStationId: originYard.id, destinationStationId: destYard.id, scheduledDepartureDate: departure, scheduledArrivalDate: arrival, actualDepartureAt: isDispatched ? departure : null, status, trainNumber, direction: dir, maxWagons: 53, bookingWindowStatus: isDispatched ? "CLOSED" : "OPEN", }), ); const wagonPrefix = `${trainNumber}-W`; let nextWagon = await nextWagonSeq(ds, wagonPrefix); for (let w = 0; w < 4; w++) { const ws = nextWagon + w; const wagonNumber = `${wagonPrefix}${String(ws).padStart(2, "0")}`; const wagon = wagonRepo.create({ wagonNumber, wagonTypeId: wagonType.id, currentYardId: originYard.id, currentTrainScheduleId: schedule.id, tareWeight, maxPayloadWeight: wagonCapacity, status: isDispatched ? WagonStatus.Assigned : WagonStatus.Available, notes: "Test seed wagon", }); const saved = await wagonRepo.save(wagon as any); const physicalWagon = Array.isArray(saved) ? saved[0] : saved; await ds.getRepository(TrainSetWagon).save( ds.getRepository(TrainSetWagon).create({ trainSetId: trainSet.id, wagonTypeId: wagonType.id, physicalWagonId: physicalWagon.id, sequenceNo: w + 1, capacityTons: wagonCapacity, lengthMeters: wagonLength, assignedWeightTons: 20, status: isDispatched ? "DEPARTED" : "PLANNED", }), ); } this.log(` Created ${status} ${dir} schedule: ${trainNumber} (${originYard.label} → ${destYard.label})`); } this.log(`Done — ${count} new train schedules created`); }); }