forked from Yara724/api
Fixed registrar and field expert
This commit is contained in:
320
scripts/seed-parsian-tehran.ts
Normal file
320
scripts/seed-parsian-tehran.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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<string, string> = {};
|
||||
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<string> {
|
||||
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<string, Types.ObjectId>();
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user