mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
244 lines
9.9 KiB
TypeScript
244 lines
9.9 KiB
TypeScript
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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 <n>", "Number of schedules to create (default: 3)")
|
|
.option("--direction <dirs>", "IMPORT,EXPORT (default: both)")
|
|
.option("--status <statuses>", "DRAFT,SCHEDULED,DISPATCHED (default: SCHEDULED)")
|
|
.option("--days-ahead <n>", "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`);
|
|
});
|
|
}
|