mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
fix: script fial
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
import type Vorpal from "vorpal";
|
|
||||||
import type { CommandContext } from "./types";
|
|
||||||
|
|
||||||
import { registerSeedTestContracts } from "./seed-test-contracts.cmd";
|
|
||||||
import { registerSeedTestSchedules } from "./seed-test-schedules.cmd";
|
|
||||||
import { registerSeedTestCompany } from "./seed-test-company.cmd";
|
|
||||||
|
|
||||||
export function registerCommands(vorpal: Vorpal, ctx: CommandContext): void {
|
|
||||||
registerSeedTestContracts(vorpal, ctx);
|
|
||||||
registerSeedTestSchedules(vorpal, ctx);
|
|
||||||
registerSeedTestCompany(vorpal, ctx);
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import type Vorpal from "vorpal";
|
|
||||||
import { DataSource } from "typeorm";
|
|
||||||
import type { CommandContext } from "./types";
|
|
||||||
import { Company, CompanyType, CompanyKind, CompanyStatus, CompanyNationality } from "../../modules/companies/entities/company.entity";
|
|
||||||
import { CompanyProfile, ProfileStatus, ProfileType } from "../../modules/companies/entities/company-profile.entity";
|
|
||||||
import { ExternalProfile } from "../../modules/companies/entities/external-profile.entity";
|
|
||||||
|
|
||||||
export function registerSeedTestCompany(
|
|
||||||
vorpal: Vorpal,
|
|
||||||
ctx: CommandContext,
|
|
||||||
): void {
|
|
||||||
vorpal
|
|
||||||
.command("seed:test-company", "Generate a test company with approved importer/exporter profiles and an external user")
|
|
||||||
.option("--name <name>", "Company name (default: Test Company)")
|
|
||||||
.option("--email <email>", "Company email (default: company@test.com)")
|
|
||||||
.option("--tin <tin>", "Tax ID (default: auto-generated TSTxxxxx)")
|
|
||||||
.action(async function (this: any, args: any) {
|
|
||||||
const { app } = ctx;
|
|
||||||
const ds = app.get(DataSource);
|
|
||||||
|
|
||||||
const raw = await ds.query(
|
|
||||||
`SELECT "tin" FROM "freight"."companies" WHERE "tin" LIKE 'TST%' AND "deleted_at" IS NULL ORDER BY "tin" DESC LIMIT 1`,
|
|
||||||
);
|
|
||||||
let nextTinNum = 1;
|
|
||||||
if (raw.length > 0) {
|
|
||||||
const num = parseInt((raw[0] as any).tin.replace("TST", ""), 10);
|
|
||||||
if (!isNaN(num)) nextTinNum = num + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = args.options?.name ?? "Test Company";
|
|
||||||
const email = args.options?.email ?? "company@test.com";
|
|
||||||
const tin = args.options?.tin ?? `TST${String(nextTinNum).padStart(6, "0")}`;
|
|
||||||
const userId = `ffffffff-0000-4000-8000-${String(nextTinNum).padStart(12, "0")}`;
|
|
||||||
|
|
||||||
const existing = await ds.getRepository(Company).findOne({ where: { tin } });
|
|
||||||
if (existing) {
|
|
||||||
this.log(`Company with TIN ${tin} already exists (${existing.name})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const company = await ds.getRepository(Company).save(
|
|
||||||
ds.getRepository(Company).create({
|
|
||||||
name,
|
|
||||||
type: CompanyType.Customer,
|
|
||||||
kind: CompanyKind.Commercial,
|
|
||||||
status: CompanyStatus.Active,
|
|
||||||
tin,
|
|
||||||
country: "Ethiopia",
|
|
||||||
nationality: CompanyNationality.Ethiopian,
|
|
||||||
email,
|
|
||||||
phone: "+251911000000",
|
|
||||||
address: "Test Address",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.log(` Created company: ${company.name} (TIN: ${tin})`);
|
|
||||||
|
|
||||||
for (const type of [ProfileType.importer, ProfileType.exporter]) {
|
|
||||||
await ds.getRepository(CompanyProfile).save(
|
|
||||||
ds.getRepository(CompanyProfile).create({
|
|
||||||
companyId: company.id,
|
|
||||||
type,
|
|
||||||
reference: `TST-${type.toUpperCase()}-${String(nextTinNum).padStart(3, "0")}`,
|
|
||||||
status: ProfileStatus.Active,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.log(` Created ${type} profile (approved)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ds.getRepository(ExternalProfile).save(
|
|
||||||
ds.getRepository(ExternalProfile).create({
|
|
||||||
userId,
|
|
||||||
companyId: company.id,
|
|
||||||
firstName: "Test",
|
|
||||||
lastName: "User",
|
|
||||||
isPrimaryContact: true,
|
|
||||||
activeProfileType: ProfileType.importer,
|
|
||||||
onboardingCompleted: true,
|
|
||||||
onboardingStep: "done",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.log(` Created external profile: Test User (userId: ${userId})`);
|
|
||||||
|
|
||||||
this.log(`\nDone — login with email "${email}" and password "password"`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
import type Vorpal from "vorpal";
|
|
||||||
import { DataSource } from "typeorm";
|
|
||||||
import type { CommandContext } from "./types";
|
|
||||||
import { Company, CompanyType, CompanyKind, CompanyStatus, CompanyNationality } from "../../modules/companies/entities/company.entity";
|
|
||||||
import { CompanyProfile, ProfileStatus, ProfileType } from "../../modules/companies/entities/company-profile.entity";
|
|
||||||
import { ExternalProfile } from "../../modules/companies/entities/external-profile.entity";
|
|
||||||
import { Yard } from "../../modules/rule-engine/entities/yard.entity";
|
|
||||||
import { ServiceType } from "../../modules/rule-engine/entities/service-type.entity";
|
|
||||||
import { CargoType } from "../../modules/rule-engine/entities/cargo-type.entity";
|
|
||||||
import { Rate } from "../../modules/rule-engine/entities/rate.entity";
|
|
||||||
import { Contract } from "../../modules/contracts/entities/contract.entity";
|
|
||||||
import { ContractRoute } from "../../modules/contracts/entities/contract-route.entity";
|
|
||||||
import { ContractCargoScope } from "../../modules/contracts/entities/contract-cargo-scope.entity";
|
|
||||||
import { ContractRateSnapshot } from "../../modules/contracts/entities/contract-rate-snapshot.entity";
|
|
||||||
|
|
||||||
export function registerSeedTestContracts(
|
|
||||||
vorpal: Vorpal,
|
|
||||||
ctx: CommandContext,
|
|
||||||
): void {
|
|
||||||
vorpal
|
|
||||||
.command("seed:test-contracts", "Generate test contracts with companies and all deps")
|
|
||||||
.option("-n, --count <n>", "Number of contracts to create (default: 4)")
|
|
||||||
.option("--status <statuses>", "Comma-separated contract statuses (default: DRAFT,SUBMITTED,APPROVED,CONTRACT_ACTIVE)")
|
|
||||||
.option("--freight <types>", "Freight types: CONTAINER,BULK (default: both)")
|
|
||||||
.option("--direction <dirs>", "Trade directions: IMPORT,EXPORT (default: both)")
|
|
||||||
.option("--company <name>", "Only create contracts for company matching name/TIN")
|
|
||||||
.action(async function (this: any, args: any) {
|
|
||||||
const { app } = ctx;
|
|
||||||
const ds = app.get(DataSource);
|
|
||||||
|
|
||||||
const count = Math.max(1, Math.min(20, parseInt(args.options?.count ?? "4", 10)));
|
|
||||||
const statusList = (args.options?.status ?? "DRAFT,SUBMITTED,APPROVED,CONTRACT_ACTIVE")
|
|
||||||
.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
||||||
const freightList = (args.options?.freight ?? "CONTAINER,BULK")
|
|
||||||
.split(",").map((s: string) => s.toUpperCase().trim())
|
|
||||||
.filter((s: string) => s === "CONTAINER" || s === "BULK");
|
|
||||||
const directionList = (args.options?.direction ?? "IMPORT,EXPORT")
|
|
||||||
.split(",").map((s: string) => s.toUpperCase().trim())
|
|
||||||
.filter((s: string) => s === "IMPORT" || s === "EXPORT");
|
|
||||||
const companyFilter = args.options?.company as string | undefined;
|
|
||||||
|
|
||||||
if (freightList.length === 0 || directionList.length === 0) {
|
|
||||||
this.log("error: at least one freight type and trade direction required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(`Seeding ${count} contracts (statuses=${statusList.join(",")}, freight=${freightList.join(",")}, dir=${directionList.join(",")})...`);
|
|
||||||
|
|
||||||
const yards = await ds.getRepository(Yard).find({ where: { isActive: true } });
|
|
||||||
const yardByCode = new Map(yards.map((y) => [y.code, y]));
|
|
||||||
const djibouti = yardByCode.get("DJIBOUTI");
|
|
||||||
const addis = yardByCode.get("ADDIS_ABABA");
|
|
||||||
if (!djibouti || !addis) {
|
|
||||||
this.log("error: need at least DJIBOUTI and ADDIS_ABABA yards seeded");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceTypes = await ds
|
|
||||||
.getRepository(ServiceType)
|
|
||||||
.find({ where: { isActive: true } });
|
|
||||||
const stByCode = new Map(serviceTypes.map((st) => [st.code, st]));
|
|
||||||
const railContainer = stByCode.get("RAIL_CONTAINER");
|
|
||||||
const railBulk = stByCode.get("RAIL_BULK");
|
|
||||||
if (!railContainer && !railBulk) {
|
|
||||||
this.log("error: need at least RAIL_CONTAINER or RAIL_BULK service type seeded");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cargoTypes = await ds
|
|
||||||
.getRepository(CargoType)
|
|
||||||
.find({ where: { isActive: true } });
|
|
||||||
const cargoByCode = new Map(cargoTypes.map((c) => [c.code, c]));
|
|
||||||
const grain = cargoByCode.get("GRAIN");
|
|
||||||
const sugar = cargoByCode.get("SUGAR");
|
|
||||||
const fertilizer = cargoByCode.get("FERTILIZER");
|
|
||||||
|
|
||||||
const rates = await ds.getRepository(Rate).find({ where: { status: "LIVE" } });
|
|
||||||
|
|
||||||
const companyRepo = ds.getRepository(Company);
|
|
||||||
let companies = await companyRepo.find({});
|
|
||||||
|
|
||||||
if (companyFilter) {
|
|
||||||
companies = companies.filter(
|
|
||||||
(c) =>
|
|
||||||
c.name.toLowerCase().includes(companyFilter.toLowerCase()) ||
|
|
||||||
c.tin.includes(companyFilter),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (companies.length === 0) {
|
|
||||||
this.log("No existing companies found — seeding test companies...");
|
|
||||||
companies = await seedTestCompanies(ds, (msg) => this.log(msg));
|
|
||||||
} else {
|
|
||||||
this.log(`Using ${companies.length} existing companies from DB`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractRepo = ds.getRepository(Contract);
|
|
||||||
|
|
||||||
const maxRaw = await ds.query(
|
|
||||||
`SELECT "reference" FROM "freight"."contracts" WHERE "reference" LIKE 'TST-CTR-%' AND "deleted_at" IS NULL ORDER BY "reference" DESC LIMIT 1`,
|
|
||||||
);
|
|
||||||
let nextRef = 1;
|
|
||||||
if (maxRaw.length > 0) {
|
|
||||||
const num = parseInt(maxRaw[0].reference.replace("TST-CTR-", ""), 10);
|
|
||||||
if (!isNaN(num)) nextRef = num + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const statusIdx = i % statusList.length;
|
|
||||||
const ftIdx = i % freightList.length;
|
|
||||||
const dirIdx = i % directionList.length;
|
|
||||||
const companyIdx = i % companies.length;
|
|
||||||
|
|
||||||
const status = statusList[statusIdx];
|
|
||||||
const freightType = freightList[ftIdx];
|
|
||||||
const direction = directionList[dirIdx];
|
|
||||||
const company = companies[companyIdx];
|
|
||||||
|
|
||||||
const profile = await ds.getRepository(CompanyProfile).findOne({
|
|
||||||
where: {
|
|
||||||
companyId: company.id,
|
|
||||||
type: direction === "IMPORT" ? ProfileType.importer : ProfileType.exporter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!profile) continue;
|
|
||||||
|
|
||||||
const ref = `TST-CTR-${String(nextRef + i).padStart(5, "0")}`;
|
|
||||||
|
|
||||||
const serviceTypeId =
|
|
||||||
freightType === "BULK" && railBulk
|
|
||||||
? railBulk.id
|
|
||||||
: railContainer
|
|
||||||
? railContainer.id
|
|
||||||
: serviceTypes[0].id;
|
|
||||||
|
|
||||||
const originId = direction === "IMPORT" ? djibouti.id : addis.id;
|
|
||||||
const destId = direction === "IMPORT" ? addis.id : djibouti.id;
|
|
||||||
|
|
||||||
const contract = contractRepo.create({
|
|
||||||
reference: ref,
|
|
||||||
companyId: company.id,
|
|
||||||
companyProfileId: profile.id,
|
|
||||||
contractKind: "ONE_TIME" as const,
|
|
||||||
tradeDirection: direction,
|
|
||||||
freightType,
|
|
||||||
serviceTypeId,
|
|
||||||
paymentCurrency: "USD",
|
|
||||||
customsClearingEnabled: false,
|
|
||||||
equipmentReturn: "without_return",
|
|
||||||
status,
|
|
||||||
versionNumber: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const saved = await contractRepo.save(contract);
|
|
||||||
|
|
||||||
await ds.getRepository(ContractRoute).save(
|
|
||||||
ds.getRepository(ContractRoute).create({
|
|
||||||
contractId: saved.id,
|
|
||||||
originYardId: originId,
|
|
||||||
destinationYardId: destId,
|
|
||||||
sortOrder: 1,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (freightType === "CONTAINER") {
|
|
||||||
for (const size of ["20FT", "40FT"] as const) {
|
|
||||||
await ds.getRepository(ContractCargoScope).save(
|
|
||||||
ds.getRepository(ContractCargoScope).create({
|
|
||||||
contractId: saved.id,
|
|
||||||
containerSize: size,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const bulkCargo = grain || sugar || fertilizer;
|
|
||||||
if (bulkCargo) {
|
|
||||||
await ds.getRepository(ContractCargoScope).save(
|
|
||||||
ds.getRepository(ContractCargoScope).create({
|
|
||||||
contractId: saved.id,
|
|
||||||
cargoTypeId: bulkCargo.id,
|
|
||||||
quantityCap: 10000,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingRates = rates.filter((r) => {
|
|
||||||
if (r.appliesTo === "CONTAINER" && freightType !== "CONTAINER") return false;
|
|
||||||
if (r.appliesTo === "BULK" && freightType !== "BULK") return false;
|
|
||||||
if (r.tradeDirection && r.tradeDirection !== direction) return false;
|
|
||||||
return r.status === "LIVE" && r.trigger === "ALWAYS";
|
|
||||||
});
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const rate of matchingRates.slice(0, 3)) {
|
|
||||||
const sig = `${rate.rateType}|${rate.currency}|${rate.rateValue}`;
|
|
||||||
if (seen.has(sig)) continue;
|
|
||||||
seen.add(sig);
|
|
||||||
|
|
||||||
await ds.getRepository(ContractRateSnapshot).save(
|
|
||||||
ds.getRepository(ContractRateSnapshot).create({
|
|
||||||
contractId: saved.id,
|
|
||||||
rateId: rate.id,
|
|
||||||
rateCode: rate.rateType,
|
|
||||||
unitPrice: Number(rate.rateValue),
|
|
||||||
unitOfMeasure: rate.rateUnit,
|
|
||||||
currency: rate.currency ?? "USD",
|
|
||||||
containerSize: freightType === "CONTAINER" ? "20FT" : null,
|
|
||||||
isSurcharge: rate.trigger !== "ALWAYS",
|
|
||||||
conditionalOn: rate.trigger !== "ALWAYS" ? rate.trigger : null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(` Created ${status} ${freightType} ${direction} contract: ${ref} (${company.name})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log(`Done — ${count} new contracts created`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CompanySeed {
|
|
||||||
name: string;
|
|
||||||
tin: string;
|
|
||||||
profiles: Array<{ type: ProfileType; reference: string }>;
|
|
||||||
externalProfile: { userId: string; firstName: string; lastName: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_COMPANIES: CompanySeed[] = [
|
|
||||||
{
|
|
||||||
name: "Test Importer Co.", tin: "TST000001",
|
|
||||||
profiles: [
|
|
||||||
{ type: ProfileType.importer, reference: "TST-IM-001" },
|
|
||||||
{ type: ProfileType.exporter, reference: "TST-EX-001" },
|
|
||||||
],
|
|
||||||
externalProfile: { userId: "00000000-0000-0000-0000-000000000001", firstName: "Abebe", lastName: "Kebede" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test Exporter Ltd.", tin: "TST000002",
|
|
||||||
profiles: [
|
|
||||||
{ type: ProfileType.importer, reference: "TST-IM-002" },
|
|
||||||
{ type: ProfileType.exporter, reference: "TST-EX-002" },
|
|
||||||
],
|
|
||||||
externalProfile: { userId: "00000000-0000-0000-0000-000000000002", firstName: "Bekele", lastName: "Alemu" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bulk Commodities PLC", tin: "TST000003",
|
|
||||||
profiles: [
|
|
||||||
{ type: ProfileType.importer, reference: "TST-IM-003" },
|
|
||||||
{ type: ProfileType.exporter, reference: "TST-EX-003" },
|
|
||||||
],
|
|
||||||
externalProfile: { userId: "00000000-0000-0000-0000-000000000003", firstName: "Chala", lastName: "Tesfaye" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Hazardous Logistics Inc.", tin: "TST000004",
|
|
||||||
profiles: [
|
|
||||||
{ type: ProfileType.importer, reference: "TST-IM-004" },
|
|
||||||
{ type: ProfileType.exporter, reference: "TST-EX-004" },
|
|
||||||
],
|
|
||||||
externalProfile: { userId: "00000000-0000-0000-0000-000000000004", firstName: "Desta", lastName: "Hailu" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function seedTestCompanies(ds: DataSource, log: (msg: string) => void): Promise<Company[]> {
|
|
||||||
const companyRepo = ds.getRepository(Company);
|
|
||||||
const profileRepo = ds.getRepository(CompanyProfile);
|
|
||||||
const extProfileRepo = ds.getRepository(ExternalProfile);
|
|
||||||
const result: Company[] = [];
|
|
||||||
|
|
||||||
for (const seed of TEST_COMPANIES) {
|
|
||||||
let company = await companyRepo.findOne({ where: { tin: seed.tin } });
|
|
||||||
if (!company) {
|
|
||||||
company = await companyRepo.save(
|
|
||||||
companyRepo.create({
|
|
||||||
name: seed.name,
|
|
||||||
type: CompanyType.Customer,
|
|
||||||
kind: CompanyKind.Commercial,
|
|
||||||
status: CompanyStatus.Active,
|
|
||||||
tin: seed.tin,
|
|
||||||
country: "Ethiopia",
|
|
||||||
nationality: CompanyNationality.Ethiopian,
|
|
||||||
email: `info@${seed.name.toLowerCase().replace(/\s+/g, "")}.com`,
|
|
||||||
phone: "+251911000001",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
log(` Created company: ${seed.name}`);
|
|
||||||
} else {
|
|
||||||
log(` Company already exists: ${seed.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const p of seed.profiles) {
|
|
||||||
const existing = await profileRepo.findOne({
|
|
||||||
where: { companyId: company.id, type: p.type },
|
|
||||||
});
|
|
||||||
if (!existing) {
|
|
||||||
await profileRepo.save(
|
|
||||||
profileRepo.create({
|
|
||||||
companyId: company.id,
|
|
||||||
type: p.type,
|
|
||||||
reference: p.reference,
|
|
||||||
status: ProfileStatus.Active,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
log(` Created ${p.type} profile: ${p.reference}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ext = seed.externalProfile;
|
|
||||||
const existingExt = await extProfileRepo.findOne({
|
|
||||||
where: { companyId: company.id, userId: ext.userId },
|
|
||||||
});
|
|
||||||
if (!existingExt) {
|
|
||||||
await extProfileRepo.save(
|
|
||||||
extProfileRepo.create({
|
|
||||||
userId: ext.userId,
|
|
||||||
companyId: company.id,
|
|
||||||
firstName: ext.firstName,
|
|
||||||
lastName: ext.lastName,
|
|
||||||
isPrimaryContact: true,
|
|
||||||
onboardingCompleted: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
log(` Created external profile: ${ext.firstName} ${ext.lastName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(company);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import type { INestApplicationContext } from "@nestjs/common";
|
|
||||||
|
|
||||||
export type CommandContext = {
|
|
||||||
app: INestApplicationContext;
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ import { config } from "dotenv";
|
|||||||
config();
|
config();
|
||||||
|
|
||||||
import Vorpal from "vorpal";
|
import Vorpal from "vorpal";
|
||||||
import { registerCommands } from "./cmds/index";
|
|
||||||
|
|
||||||
import { NestFactory } from "@nestjs/core";
|
import { NestFactory } from "@nestjs/core";
|
||||||
import { AppModule } from "../app.module";
|
import { AppModule } from "../app.module";
|
||||||
@@ -17,7 +16,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
registerCommands(vorpal, { app });
|
// registerCommands(vorpal, { app });
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
if (args.length > 0) {
|
if (args.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user