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"; const COMPANY_IDS = [ "a0000001-0000-4000-8000-000000000001", "a0000001-0000-4000-8000-000000000002", "a0000001-0000-4000-8000-000000000003", "a0000001-0000-4000-8000-000000000004", ]; const PROFILE_IDS = [ "b0000001-0000-4000-8000-000000000001", "b0000001-0000-4000-8000-000000000002", "b0000001-0000-4000-8000-000000000003", "b0000001-0000-4000-8000-000000000004", "b0000001-0000-4000-8000-000000000005", "b0000001-0000-4000-8000-000000000006", "b0000001-0000-4000-8000-000000000007", "b0000001-0000-4000-8000-000000000008", ]; const EXTERNAL_PROFILE_IDS = [ "c0000001-0000-4000-8000-000000000001", "c0000001-0000-4000-8000-000000000002", "c0000001-0000-4000-8000-000000000003", "c0000001-0000-4000-8000-000000000004", ]; function generateRef(index: number): string { return `TST-CTR-${String(index).padStart(5, "0")}`; } export function registerSeedTestContracts( vorpal: Vorpal, ctx: CommandContext, ): void { vorpal .command("seed:test-contracts", "Generate test contracts with companies and all deps") .option("-n, --count ", "Number of contracts to create (default: 4)") .option("--status ", "Comma-separated contract statuses (default: DRAFT,SUBMITTED,APPROVED,CONTRACT_ACTIVE)") .option("--freight ", "Freight types: CONTAINER,BULK (default: both)") .option("--direction ", "Trade directions: IMPORT,EXPORT (default: both)") .option("--company ", "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); let contractCount = 0; 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 = generateRef(i + 1); const exists = await contractRepo.findOne({ where: { reference: ref } }); if (exists) { this.log(` Contract ${ref} already exists — skipping`); contractCount++; continue; } 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(); 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, }), ); } contractCount++; this.log(` Created ${status} ${freightType} ${direction} contract: ${ref} (${company.name})`); } this.log(`Done — ${contractCount} contracts ensured`); }); } interface CompanySeed { id: string; name: string; tin: string; profiles: Array<{ id: string; type: ProfileType; reference: string }>; externalProfile: { id: string; userId: string; firstName: string; lastName: string }; } const TEST_COMPANIES: CompanySeed[] = [ { id: COMPANY_IDS[0], name: "Test Importer Co.", tin: "TST000001", profiles: [ { id: PROFILE_IDS[0], type: ProfileType.importer, reference: "TST-IM-001" }, { id: PROFILE_IDS[1], type: ProfileType.exporter, reference: "TST-EX-001" }, ], externalProfile: { id: EXTERNAL_PROFILE_IDS[0], userId: "00000000-0000-0000-0000-000000000001", firstName: "Abebe", lastName: "Kebede" }, }, { id: COMPANY_IDS[1], name: "Test Exporter Ltd.", tin: "TST000002", profiles: [ { id: PROFILE_IDS[2], type: ProfileType.importer, reference: "TST-IM-002" }, { id: PROFILE_IDS[3], type: ProfileType.exporter, reference: "TST-EX-002" }, ], externalProfile: { id: EXTERNAL_PROFILE_IDS[1], userId: "00000000-0000-0000-0000-000000000002", firstName: "Bekele", lastName: "Alemu" }, }, { id: COMPANY_IDS[2], name: "Bulk Commodities PLC", tin: "TST000003", profiles: [ { id: PROFILE_IDS[4], type: ProfileType.importer, reference: "TST-IM-003" }, { id: PROFILE_IDS[5], type: ProfileType.exporter, reference: "TST-EX-003" }, ], externalProfile: { id: EXTERNAL_PROFILE_IDS[2], userId: "00000000-0000-0000-0000-000000000003", firstName: "Chala", lastName: "Tesfaye" }, }, { id: COMPANY_IDS[3], name: "Hazardous Logistics Inc.", tin: "TST000004", profiles: [ { id: PROFILE_IDS[6], type: ProfileType.importer, reference: "TST-IM-004" }, { id: PROFILE_IDS[7], type: ProfileType.exporter, reference: "TST-EX-004" }, ], externalProfile: { id: EXTERNAL_PROFILE_IDS[3], userId: "00000000-0000-0000-0000-000000000004", firstName: "Desta", lastName: "Hailu" }, }, ]; async function seedTestCompanies(ds: DataSource, log: (msg: string) => void): Promise { 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: { id: seed.id } }); if (!company) { company = await companyRepo.save( companyRepo.create({ id: seed.id, 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: { id: p.id } }); if (!existing) { await profileRepo.save( profileRepo.create({ id: p.id, companyId: seed.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: { id: ext.id } }); if (!existingExt) { await extProfileRepo.save( extProfileRepo.create({ id: ext.id, userId: ext.userId, companyId: seed.id, firstName: ext.firstName, lastName: ext.lastName, isPrimaryContact: true, onboardingCompleted: true, }), ); log(` Created external profile: ${ext.firstName} ${ext.lastName}`); } result.push(company); } return result; }