/** * One-time seed for Parsian (clientCode=8) Tehran branches + field experts. * * Usage (before starting the app): * npm run seed:parsian-tehran * * Optional env: * SEED_FIELD_EXPERT_DEFAULT_PASSWORD=Parsian@724 */ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import * as crypto from "node:crypto"; import mongoose, { Schema, Types } from "mongoose"; type BranchSeed = { code: string; name: string; fullName?: string; city: string; state: string; address: string; phoneNumber?: string; isActive?: boolean; }; type FieldExpertSeed = { nationalCode: string; mobile?: string; firstName: string; lastName: string; branchCode: string; branchName?: string; city?: string; state?: string; title?: string; }; function stripQuotes(value: string): string { const trimmed = value.trim(); if ( (trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"')) ) { return trimmed.slice(1, -1); } return trimmed; } function stripInlineComment(value: string): string { const hashIdx = value.indexOf(" #"); return hashIdx === -1 ? value : value.slice(0, hashIdx).trim(); } function expandEnvValue(value: string, env: NodeJS.ProcessEnv): string { return value.replace(/\$\{([^}]+)\}/g, (_, key: string) => env[key] ?? ""); } function loadEnvFile() { const envPath = join(process.cwd(), ".env"); if (!existsSync(envPath)) return; const raw: Record = {}; for (const line of readFileSync(envPath, "utf8").split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const idx = trimmed.indexOf("="); if (idx === -1) continue; const key = trimmed.slice(0, idx).trim(); const value = stripInlineComment(trimmed.slice(idx + 1).trim()); raw[key] = value; } for (const [key, value] of Object.entries(raw)) { if (process.env[key]) continue; process.env[key] = stripQuotes(value); } // Expand ${VAR} placeholders (same as Nest ConfigModule expandVariables). for (let pass = 0; pass < 5; pass++) { let changed = false; for (const key of Object.keys(process.env)) { const current = process.env[key]; if (!current || !current.includes("${")) continue; const expanded = expandEnvValue(stripQuotes(current), process.env); if (expanded !== current) { process.env[key] = expanded; changed = true; } } if (!changed) break; } for (const key of Object.keys(process.env)) { const value = process.env[key]; if (value) process.env[key] = stripQuotes(value); } } function resolveMongoUri(): string { const uri = process.env.MONGO_URI?.trim(); if (!uri) { throw new Error("MONGO_URI is not set in .env"); } if (!uri.startsWith("mongodb://") && !uri.startsWith("mongodb+srv://")) { throw new Error( `Invalid MONGO_URI after env expansion: "${uri.slice(0, 40)}..."`, ); } return uri; } async function ensureFieldExpertIndexes(collection: mongoose.Collection) { const indexes = await collection.indexes(); const emailIndex = indexes.find((idx) => idx.key?.email === 1); if (emailIndex && !emailIndex.sparse) { await collection.dropIndex(emailIndex.name); console.log(`Dropped legacy non-sparse index: ${emailIndex.name}`); } await collection.createIndex({ email: 1 }, { unique: true, sparse: true }); await collection.createIndex( { clientKey: 1, nationalCode: 1 }, { unique: true, sparse: true }, ); } function hashPassword(password: string): Promise { return new Promise((resolve, reject) => { const salt = crypto.randomBytes(16).toString("hex"); crypto.scrypt(password, salt, 64, (err, derivedKey) => { if (err) reject(err); resolve(`${salt}:${derivedKey.toString("hex")}`); }); }); } const ClientSchema = new Schema( { clientName: { type: Object, required: true }, clientCode: { type: Number, required: true }, useExpertMode: { type: String, required: true }, }, { collection: "clients", versionKey: false }, ); const BranchSchema = new Schema( { clientKey: { type: Schema.Types.ObjectId, required: true, index: true }, name: { type: String, required: true }, code: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true }, address: { type: String, required: true }, phoneNumber: { type: String }, isActive: { type: Boolean, default: true }, }, { collection: "branches", versionKey: false, timestamps: true }, ); BranchSchema.index({ clientKey: 1, code: 1 }, { unique: true }); const FieldExpertSchema = new Schema( { firstName: { type: String, required: true }, lastName: { type: String, required: true }, email: { type: String, unique: true, sparse: true }, username: { type: String }, nationalCode: { type: String, index: true, sparse: true }, clientKey: { type: Schema.Types.ObjectId, index: true }, branchId: { type: Schema.Types.ObjectId, index: true }, password: { type: String, required: true }, mobile: { type: String }, phone: { type: String }, role: { type: String, default: "field_expert" }, otp: { type: String, default: "" }, }, { collection: "field-expert", versionKey: false, timestamps: true }, ); FieldExpertSchema.index( { clientKey: 1, nationalCode: 1 }, { unique: true, sparse: true }, ); async function main() { loadEnvFile(); const mongoUri = resolveMongoUri(); const dataDir = join(process.cwd(), "scripts/data/parsian-tehran"); const branchesFile = JSON.parse( readFileSync(join(dataDir, "branches.json"), "utf8"), ) as { clientCode: number; branches: BranchSeed[] }; const expertsFile = JSON.parse( readFileSync(join(dataDir, "field-experts.json"), "utf8"), ) as { clientCode: number; fieldExperts: FieldExpertSeed[] }; const defaultPassword = process.env.SEED_FIELD_EXPERT_DEFAULT_PASSWORD ?? "123321"; const hashedPassword = await hashPassword(defaultPassword); await mongoose.connect(mongoUri, { tls: process.env.MONGO_TLS === "true", tlsAllowInvalidCertificates: process.env.MONGO_TLS_ALLOW_INVALID_CERTS === "true", }); const Client = mongoose.model("ClientSeedClient", ClientSchema); const Branch = mongoose.model("ClientSeedBranch", BranchSchema); const FieldExpert = mongoose.model("ClientSeedFieldExpert", FieldExpertSchema); await ensureFieldExpertIndexes(FieldExpert.collection); const client = await Client.findOne({ clientCode: branchesFile.clientCode, }).lean(); if (!client?._id) { throw new Error( `Client with clientCode=${branchesFile.clientCode} not found in database`, ); } const clientKey = new Types.ObjectId(String(client._id)); const branchIdByCode = new Map(); let branchesCreated = 0; let branchesUpdated = 0; for (const branch of branchesFile.branches) { const existing = await Branch.findOne({ clientKey, code: branch.code, }); const payload = { clientKey, name: branch.name, code: branch.code, city: branch.city, state: branch.state, address: branch.address, phoneNumber: branch.phoneNumber, isActive: branch.isActive ?? true, }; if (existing) { await Branch.updateOne({ _id: existing._id }, { $set: payload }); branchIdByCode.set(branch.code, existing._id as Types.ObjectId); branchesUpdated++; } else { const created = await Branch.create(payload); branchIdByCode.set(branch.code, created._id as Types.ObjectId); branchesCreated++; } } let expertsCreated = 0; let expertsUpdated = 0; let expertsSkipped = 0; for (const expert of expertsFile.fieldExperts) { const branchId = branchIdByCode.get(expert.branchCode); if (!branchId) { console.warn( `Skipping ${expert.nationalCode}: unknown branch ${expert.branchCode}`, ); expertsSkipped++; continue; } const payload = { firstName: expert.firstName, lastName: expert.lastName, username: expert.nationalCode, nationalCode: expert.nationalCode, clientKey, branchId, password: hashedPassword, mobile: expert.mobile, role: "field_expert", otp: "", }; const existing = await FieldExpert.findOne({ clientKey, nationalCode: expert.nationalCode, }); if (existing) { await FieldExpert.updateOne( { _id: existing._id }, { $set: { ...payload, // Do not rotate password on re-seed unless explicitly desired. password: existing.password, }, }, ); expertsUpdated++; } else { await FieldExpert.create(payload); expertsCreated++; } } console.log("Parsian Tehran seed completed."); console.log({ clientCode: branchesFile.clientCode, clientKey: String(clientKey), branchesCreated, branchesUpdated, expertsCreated, expertsUpdated, expertsSkipped, defaultPassword, loginHint: "Use nationalCode + password on POST /actor/login with role field_expert", }); await mongoose.disconnect(); } main().catch((err) => { console.error(err); process.exit(1); });