forked from Yara724/api
513 lines
14 KiB
TypeScript
513 lines
14 KiB
TypeScript
/**
|
|
* One-time seed for Parsian (clientCode=8) Tehran branches + file reviewers + file makers.
|
|
*
|
|
* Usage (before starting the app):
|
|
* npm run seed:parsian-tehran
|
|
*
|
|
* Optional env:
|
|
* SEED_PARSIAN_TEHRAN_DEFAULT_PASSWORD=Parsian@724
|
|
*
|
|
* Backward-compatible env alias:
|
|
* SEED_FIELD_EXPERT_DEFAULT_PASSWORD=Parsian@724
|
|
*/
|
|
import { existsSync, readFileSync } 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 ExpertLocationSeed = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
type FileReviewerSeed = {
|
|
nationalCode: string;
|
|
mobile?: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
locations: ExpertLocationSeed[];
|
|
ThirdPartyExpertiseClaim?: string;
|
|
CarBodyExpertiseClaim?: string;
|
|
};
|
|
|
|
type FileMakerSeed = {
|
|
nationalCode: string;
|
|
mobile?: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
locations: ExpertLocationSeed[];
|
|
ThirdPartyClaimExpertId?: string;
|
|
CarBodyClaimExpertId?: 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);
|
|
}
|
|
|
|
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 ensureUserIndexes(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")}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
function normalizeLocations(locations: ExpertLocationSeed[]): ExpertLocationSeed[] {
|
|
const deduped = new Map<string, ExpertLocationSeed>();
|
|
for (const location of locations ?? []) {
|
|
const id = String(location?.id ?? "").trim();
|
|
const name = String(location?.name ?? "").trim();
|
|
if (!id || !name || deduped.has(id)) continue;
|
|
deduped.set(id, { id, name });
|
|
}
|
|
return [...deduped.values()];
|
|
}
|
|
|
|
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 ExpertLocationSchema = new Schema(
|
|
{
|
|
id: { type: String, required: true },
|
|
name: { type: String, required: true },
|
|
},
|
|
{ _id: false, id: false, versionKey: false },
|
|
);
|
|
|
|
const FileReviewerSchema = 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 },
|
|
locations: { type: [ExpertLocationSchema], default: [] },
|
|
password: { type: String, required: true },
|
|
mobile: { type: String },
|
|
phone: { type: String },
|
|
role: { type: String, default: "file_reviewer" },
|
|
otp: { type: String, default: "" },
|
|
expertCode: { type: String, required: false },
|
|
ThirdPartyExpertiseClaim: { type: String, required: false },
|
|
CarBodyExpertiseClaim: { type: String, required: false },
|
|
},
|
|
{ collection: "file-reviewer", versionKey: false, timestamps: true },
|
|
);
|
|
|
|
FileReviewerSchema.index(
|
|
{ clientKey: 1, nationalCode: 1 },
|
|
{ unique: true, sparse: true },
|
|
);
|
|
|
|
const FileMakerSchema = 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 },
|
|
locations: { type: [ExpertLocationSchema], default: [] },
|
|
password: { type: String, required: true },
|
|
mobile: { type: String },
|
|
phone: { type: String },
|
|
role: { type: String, default: "file_maker" },
|
|
otp: { type: String, default: "" },
|
|
expertCode: { type: String, required: false },
|
|
ThirdPartyClaimExpertId: { type: String, required: false },
|
|
CarBodyClaimExpertId: { type: String, required: false },
|
|
},
|
|
{ collection: "file-maker", versionKey: false, timestamps: true },
|
|
);
|
|
|
|
FileMakerSchema.index(
|
|
{ clientKey: 1, nationalCode: 1 },
|
|
{ unique: true, sparse: true },
|
|
);
|
|
|
|
function resolveSeedLocations(
|
|
seedLocations: ExpertLocationSeed[],
|
|
branchMetaByCode: Map<string, { _id: Types.ObjectId; name: string }>,
|
|
) {
|
|
const resolved: { id: string; name: string; branchId: Types.ObjectId }[] = [];
|
|
const skippedCodes: string[] = [];
|
|
|
|
for (const location of normalizeLocations(seedLocations)) {
|
|
const branch = branchMetaByCode.get(location.id);
|
|
if (!branch) {
|
|
skippedCodes.push(location.id);
|
|
continue;
|
|
}
|
|
resolved.push({
|
|
id: location.id,
|
|
name: branch.name || location.name,
|
|
branchId: branch._id,
|
|
});
|
|
}
|
|
|
|
return {
|
|
resolved,
|
|
skippedCodes,
|
|
primaryBranchId: resolved[0]?.branchId,
|
|
};
|
|
}
|
|
|
|
async function upsertRoleUsers({
|
|
label,
|
|
seeds,
|
|
model,
|
|
clientKey,
|
|
hashedPassword,
|
|
branchMetaByCode,
|
|
role,
|
|
codeFields,
|
|
}: {
|
|
label: string;
|
|
seeds: Array<Record<string, any>>;
|
|
model: mongoose.Model<any>;
|
|
clientKey: Types.ObjectId;
|
|
hashedPassword: string;
|
|
branchMetaByCode: Map<string, { _id: Types.ObjectId; name: string }>;
|
|
role: string;
|
|
codeFields: string[];
|
|
}) {
|
|
let created = 0;
|
|
let updated = 0;
|
|
let skipped = 0;
|
|
|
|
for (const seed of seeds) {
|
|
const { resolved, skippedCodes, primaryBranchId } = resolveSeedLocations(
|
|
seed.locations,
|
|
branchMetaByCode,
|
|
);
|
|
|
|
if (skippedCodes.length > 0) {
|
|
console.warn(
|
|
`Skipping unknown ${label} locations for ${seed.nationalCode}: ${skippedCodes.join(
|
|
", ",
|
|
)}`,
|
|
);
|
|
}
|
|
|
|
if (!primaryBranchId || resolved.length === 0) {
|
|
console.warn(
|
|
`Skipping ${label} ${seed.nationalCode}: no valid branch locations remained`,
|
|
);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const setPayload: Record<string, unknown> = {
|
|
firstName: seed.firstName,
|
|
lastName: seed.lastName,
|
|
username: seed.nationalCode,
|
|
nationalCode: seed.nationalCode,
|
|
clientKey,
|
|
branchId: primaryBranchId,
|
|
locations: resolved.map(({ id, name }) => ({ id, name })),
|
|
role,
|
|
otp: "",
|
|
};
|
|
|
|
if (seed.mobile) {
|
|
setPayload.mobile = seed.mobile;
|
|
}
|
|
|
|
const unsetPayload: Record<string, ""> = {
|
|
expertCode: "",
|
|
};
|
|
|
|
for (const field of codeFields) {
|
|
if (seed[field]) {
|
|
setPayload[field] = seed[field];
|
|
} else {
|
|
unsetPayload[field] = "";
|
|
}
|
|
}
|
|
|
|
const existing = await model.findOne({
|
|
clientKey,
|
|
nationalCode: seed.nationalCode,
|
|
});
|
|
|
|
if (existing) {
|
|
await model.updateOne(
|
|
{ _id: existing._id },
|
|
{
|
|
$set: {
|
|
...setPayload,
|
|
password: existing.password,
|
|
},
|
|
$unset: unsetPayload,
|
|
},
|
|
);
|
|
updated++;
|
|
} else {
|
|
await model.create({
|
|
...setPayload,
|
|
password: hashedPassword,
|
|
});
|
|
created++;
|
|
}
|
|
}
|
|
|
|
return { created, updated, skipped };
|
|
}
|
|
|
|
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 fileReviewersFile = JSON.parse(
|
|
readFileSync(join(dataDir, "file-reviewers.json"), "utf8"),
|
|
) as { clientCode: number; fileReviewers: FileReviewerSeed[] };
|
|
const fileMakersFile = JSON.parse(
|
|
readFileSync(join(dataDir, "file-makers.json"), "utf8"),
|
|
) as { clientCode: number; fileMakers: FileMakerSeed[] };
|
|
|
|
if (
|
|
branchesFile.clientCode !== fileReviewersFile.clientCode ||
|
|
branchesFile.clientCode !== fileMakersFile.clientCode
|
|
) {
|
|
throw new Error("Seed data clientCode mismatch between branch/reviewer/maker files");
|
|
}
|
|
|
|
const defaultPassword =
|
|
process.env.SEED_PARSIAN_TEHRAN_DEFAULT_PASSWORD ??
|
|
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 FileReviewer = mongoose.model("ClientSeedFileReviewer", FileReviewerSchema);
|
|
const FileMaker = mongoose.model("ClientSeedFileMaker", FileMakerSchema);
|
|
|
|
await ensureUserIndexes(FileReviewer.collection);
|
|
await ensureUserIndexes(FileMaker.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 branchMetaByCode = new Map<string, { _id: Types.ObjectId; name: string }>();
|
|
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 });
|
|
branchMetaByCode.set(branch.code, {
|
|
_id: existing._id as Types.ObjectId,
|
|
name: branch.name,
|
|
});
|
|
branchesUpdated++;
|
|
} else {
|
|
const created = await Branch.create(payload);
|
|
branchMetaByCode.set(branch.code, {
|
|
_id: created._id as Types.ObjectId,
|
|
name: branch.name,
|
|
});
|
|
branchesCreated++;
|
|
}
|
|
}
|
|
|
|
const fileReviewersResult = await upsertRoleUsers({
|
|
label: "file-reviewer",
|
|
seeds: fileReviewersFile.fileReviewers,
|
|
model: FileReviewer,
|
|
clientKey,
|
|
hashedPassword,
|
|
branchMetaByCode,
|
|
role: "file_reviewer",
|
|
codeFields: ["ThirdPartyExpertiseClaim", "CarBodyExpertiseClaim"],
|
|
});
|
|
|
|
const fileMakersResult = await upsertRoleUsers({
|
|
label: "file-maker",
|
|
seeds: fileMakersFile.fileMakers,
|
|
model: FileMaker,
|
|
clientKey,
|
|
hashedPassword,
|
|
branchMetaByCode,
|
|
role: "file_maker",
|
|
codeFields: ["ThirdPartyClaimExpertId", "CarBodyClaimExpertId"],
|
|
});
|
|
|
|
console.log("Parsian Tehran seed completed.");
|
|
console.log({
|
|
clientCode: branchesFile.clientCode,
|
|
clientKey: String(clientKey),
|
|
branchesCreated,
|
|
branchesUpdated,
|
|
fileReviewersCreated: fileReviewersResult.created,
|
|
fileReviewersUpdated: fileReviewersResult.updated,
|
|
fileReviewersSkipped: fileReviewersResult.skipped,
|
|
fileMakersCreated: fileMakersResult.created,
|
|
fileMakersUpdated: fileMakersResult.updated,
|
|
fileMakersSkipped: fileMakersResult.skipped,
|
|
defaultPassword,
|
|
loginHint:
|
|
"Use nationalCode + password on POST /actor/login with role file_reviewer or file_maker",
|
|
});
|
|
|
|
await mongoose.disconnect();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|