/** * Seed a large, repeatable insurer-visible portfolio for report endpoint testing. * * What it creates * - 1000 shared publicIds by default (configurable) * - mixed V2/V3/V4/V5/V6-style blame/claim histories * - expertFileActivities for blame experts, damage experts, and field experts * - insurer-visible claim/blame status variety for unified-status reports * - ratings/user ratings so top-files/top-experts endpoints have meaningful data * * Safety / repeatability * - only deletes documents previously created by this script for the same seed tag * - seed actors/branches are deterministic and reused across reruns * * Usage * npm run seed:reports-fixtures * * Optional env * SEED_REPORTS_CLIENT_KEY= * SEED_REPORTS_CLIENT_CODE=8 * SEED_REPORTS_COUNT=1000 * SEED_REPORTS_TAG=reports-load-v1 * SEED_REPORTS_PUBLIC_PREFIX=RPT1 * SEED_REPORTS_DEFAULT_PASSWORD=Reports@724 */ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as crypto from "node:crypto"; import mongoose, { Types } from "mongoose"; type AnyDoc = Record; type ObjectId = Types.ObjectId; type FlowKind = | "V2_USER" | "V2_EXPERT_INIT" | "V3" | "V4" | "V5" | "V6"; type UnifiedBucket = | "IN_PROGRESS" | "WAITING_FOR_SECOND_PARTY" | "WAITING_FOR_BLAME_EXPERT" | "WAITING_FOR_SIGNATURES" | "WAITING_FOR_DOCUMENT_RESEND" | "WAITING_FOR_DAMAGE_EXPERT" | "EXPERT_REVIEWING" | "EXPERT_VALIDATING_FACTORS" | "INSURER_REVIEW" | "COMPLETED" | "CANCELLED" | "REJECTED" | "STOPPED"; type Scenario = { bucket: UnifiedBucket; flow: FlowKind; fileType: "THIRD_PARTY" | "CAR_BODY"; blameStatus: string; blameKind: "expert" | "field_expert" | "file_maker" | "call_center" | null; blameActivity: "handled" | "checked" | "unchecked" | null; hasClaim: boolean; claimStatus?: string; claimReviewStatus?: string; claimStage: | "none" | "user_progress" | "queue" | "reviewing" | "resend" | "factor_validation" | "insurer_review" | "file_maker_wait" | "file_maker_rejected" | "completed" | "rejected" | "cancelled"; claimActorKind: "damage_expert" | "field_expert" | null; claimActivity: "handled" | "checked" | "unchecked" | null; rateBlame: boolean; rateClaim: boolean; userRated: boolean; objection: boolean; }; type SeedActor = { _id: ObjectId; firstName: string; lastName: string; fullName: string; email?: string; branchId?: ObjectId; branchCode?: string; role: string; }; type BranchRef = { _id: ObjectId; code: string; name: string; city: string; state: string; address: string; }; type SeedContext = { client: AnyDoc; clientId: ObjectId; clientIdStr: string; count: number; seedTag: string; publicPrefix: string; defaultPassword: string; rng: () => number; branches: BranchRef[]; experts: SeedActor[]; damageExperts: SeedActor[]; fieldExperts: SeedActor[]; fileMakers: SeedActor[]; fileReviewers: SeedActor[]; callCenters: SeedActor[]; externalClientIds: ObjectId[]; externalNames: string[]; }; const SCRIPT_NAME = "seed-insurer-reports-fixtures"; const ROLE = { EXPERT: "expert", DAMAGE_EXPERT: "damage_expert", FIELD_EXPERT: "field_expert", FILE_MAKER: "file_maker", FILE_REVIEWER: "file_reviewer", CALL_CENTER: "call_center", } as const; const USER_TYPE = { LEGAL: "legal", } as const; const CASE_STATUS = { OPEN: "OPEN", WAITING_FOR_SECOND_PARTY: "WAITING_FOR_SECOND_PARTY", WAITING_FOR_EXPERT: "WAITING_FOR_EXPERT", WAITING_FOR_DOCUMENT_RESEND: "WAITING_FOR_DOCUMENT_RESEND", WAITING_FOR_SIGNATURES: "WAITING_FOR_SIGNATURES", WAITING_FOR_FILE_REVIEWER: "WAITING_FOR_FILE_REVIEWER", WAITING_FOR_FILE_MAKER_APPROVAL: "WAITING_FOR_FILE_MAKER_APPROVAL", FILE_MAKER_REJECTED: "FILE_MAKER_REJECTED", COMPLETED: "COMPLETED", CANCELLED: "CANCELLED", AUTO_CLOSED: "AUTO_CLOSED", STOPPED: "STOPPED", } as const; const CLAIM_CASE_STATUS = { CREATED: "CREATED", SELECTING_OUTER_PARTS: "SELECTING_OUTER_PARTS", SELECTING_OTHER_PARTS: "SELECTING_OTHER_PARTS", UPLOADING_REQUIRED_DOCUMENTS: "UPLOADING_REQUIRED_DOCUMENTS", CAPTURING_PART_DAMAGES: "CAPTURING_PART_DAMAGES", WAITING_FOR_USER_RESEND: "WAITING_FOR_USER_RESEND", WAITING_FOR_DAMAGE_EXPERT: "WAITING_FOR_DAMAGE_EXPERT", EXPERT_REVIEWING: "EXPERT_REVIEWING", WAITING_FOR_INSURER_APPROVAL: "WAITING_FOR_INSURER_APPROVAL", INSURER_REVIEW_AWAITING_OWNER_SIGN: "INSURER_REVIEW_AWAITING_OWNER_SIGN", INSURER_REVIEW_MIXED_FACTORS_PENDING: "INSURER_REVIEW_MIXED_FACTORS_PENDING", OWNER_REPAIR_FACTOR_UPLOAD_PENDING: "OWNER_REPAIR_FACTOR_UPLOAD_PENDING", EXPERT_VALIDATING_REPAIR_FACTORS: "EXPERT_VALIDATING_REPAIR_FACTORS", WAITING_FOR_FILE_REVIEWER: "WAITING_FOR_FILE_REVIEWER", WAITING_FOR_FILE_MAKER_APPROVAL: "WAITING_FOR_FILE_MAKER_APPROVAL", FILE_MAKER_REJECTED: "FILE_MAKER_REJECTED", COMPLETED: "COMPLETED", CANCELLED: "CANCELLED", REJECTED: "REJECTED", } as const; const CLAIM_STATUS = { PENDING: "PENDING", UNDER_REVIEW: "UNDER_REVIEW", APPROVED: "APPROVED", REJECTED: "REJECTED", NEEDS_REVISION: "NEEDS_REVISION", } as const; const CLAIM_STEP = { CLAIM_CREATED: "CLAIM_CREATED", SELECT_OUTER_PARTS: "SELECT_OUTER_PARTS", SELECT_OTHER_PARTS: "SELECT_OTHER_PARTS", CAPTURE_PART_DAMAGES: "CAPTURE_PART_DAMAGES", UPLOAD_REQUIRED_DOCUMENTS: "UPLOAD_REQUIRED_DOCUMENTS", USER_SUBMISSION_COMPLETE: "USER_SUBMISSION_COMPLETE", USER_EXPERT_RESEND: "USER_EXPERT_RESEND", EXPERT_DAMAGE_ASSESSMENT: "EXPERT_DAMAGE_ASSESSMENT", EXPERT_FINAL_REPLY: "EXPERT_FINAL_REPLY", EXPERT_COST_EVALUATION: "EXPERT_COST_EVALUATION", OWNER_UPLOAD_FACTOR_DOCUMENTS: "OWNER_UPLOAD_FACTOR_DOCUMENTS", INSURER_REVIEW: "INSURER_REVIEW", CLAIM_COMPLETED: "CLAIM_COMPLETED", } as const; const ACTIVITY = { CHECKED: "checked", HANDLED: "handled", UNCHECKED: "unchecked", } as const; const FILE_KIND = { BLAME: "blame", CLAIM: "claim", } as const; const CREATION_METHOD = { LINK: "LINK", IN_PERSON: "IN_PERSON", } as const; const FILLED_BY = { CUSTOMER: "CUSTOMER", EXPERT: "EXPERT", } as const; const BUCKET_WEIGHTS: Record = { IN_PROGRESS: 0.18, WAITING_FOR_SECOND_PARTY: 0.07, WAITING_FOR_BLAME_EXPERT: 0.08, WAITING_FOR_SIGNATURES: 0.05, WAITING_FOR_DOCUMENT_RESEND: 0.09, WAITING_FOR_DAMAGE_EXPERT: 0.11, EXPERT_REVIEWING: 0.09, EXPERT_VALIDATING_FACTORS: 0.055, INSURER_REVIEW: 0.12, COMPLETED: 0.11, CANCELLED: 0.02, REJECTED: 0.015, STOPPED: 0.01, }; const FLOWS_BY_BUCKET: Record = { IN_PROGRESS: ["V2_USER", "V2_EXPERT_INIT", "V4", "V5", "V6"], WAITING_FOR_SECOND_PARTY: ["V2_USER", "V6"], WAITING_FOR_BLAME_EXPERT: ["V2_USER", "V3", "V6"], WAITING_FOR_SIGNATURES: ["V2_USER", "V2_EXPERT_INIT"], WAITING_FOR_DOCUMENT_RESEND: ["V2_USER", "V2_EXPERT_INIT", "V3", "V6"], WAITING_FOR_DAMAGE_EXPERT: [ "V2_USER", "V2_EXPERT_INIT", "V3", "V4", "V5", "V6", ], EXPERT_REVIEWING: [ "V2_USER", "V2_EXPERT_INIT", "V3", "V4", "V5", "V6", ], EXPERT_VALIDATING_FACTORS: [ "V2_USER", "V2_EXPERT_INIT", "V3", "V4", "V5", "V6", ], INSURER_REVIEW: [ "V2_USER", "V2_EXPERT_INIT", "V3", "V4", "V5", "V6", ], COMPLETED: ["V2_USER", "V2_EXPERT_INIT", "V3", "V4", "V5", "V6"], CANCELLED: ["V2_USER", "V2_EXPERT_INIT", "V4", "V5", "V6"], REJECTED: ["V2_USER", "V2_EXPERT_INIT", "V3", "V4", "V5", "V6"], STOPPED: ["V2_USER", "V6"], }; 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); } 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, 50)}...`); } return uri; } 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); else resolve(`${salt}:${derivedKey.toString("hex")}`); }); }); } function asObjectId(value: string | ObjectId): ObjectId { return value instanceof Types.ObjectId ? value : new Types.ObjectId(value); } function seedToInt(seed: string): number { const hex = crypto.createHash("sha256").update(seed).digest("hex").slice(0, 8); return parseInt(hex, 16) >>> 0; } function mulberry32(seed: number): () => number { let t = seed >>> 0; return () => { t += 0x6d2b79f5; let x = Math.imul(t ^ (t >>> 15), t | 1); x ^= x + Math.imul(x ^ (x >>> 7), x | 61); return ((x ^ (x >>> 14)) >>> 0) / 4294967296; }; } function randInt(rng: () => number, min: number, max: number) { return Math.floor(rng() * (max - min + 1)) + min; } function pick(items: T[], rng: () => number): T { return items[Math.floor(rng() * items.length)]; } function pad(n: number, len = 5): string { return String(n).padStart(len, "0"); } function shortHash(input: string, len = 4): string { return crypto.createHash("sha1").update(input).digest("hex").slice(0, len).toUpperCase(); } function addMinutes(date: Date, minutes: number): Date { return new Date(date.getTime() + minutes * 60_000); } function addHours(date: Date, hours: number): Date { return addMinutes(date, hours * 60); } function clone(value: T): T { return JSON.parse(JSON.stringify(value)); } function maybe(condition: boolean, value: T): T | undefined { return condition ? value : undefined; } async function resolveClient(db: mongoose.mongo.Db): Promise { const clients = db.collection("clients"); const clientKey = process.env.SEED_REPORTS_CLIENT_KEY?.trim(); const clientCodeRaw = process.env.SEED_REPORTS_CLIENT_CODE?.trim() || "8"; const clientCode = Number(clientCodeRaw); if (clientKey) { if (!Types.ObjectId.isValid(clientKey)) { throw new Error("SEED_REPORTS_CLIENT_KEY must be a valid Mongo ObjectId"); } const client = await clients.findOne({ _id: new Types.ObjectId(clientKey) }); if (!client) throw new Error(`Client not found for key ${clientKey}`); return client; } if (!Number.isFinite(clientCode)) { throw new Error("SEED_REPORTS_CLIENT_CODE must be numeric when provided"); } const client = await clients.findOne({ clientCode }); if (!client) { throw new Error( `Client not found for clientCode=${clientCode}. Set SEED_REPORTS_CLIENT_KEY explicitly.`, ); } return client; } async function ensureBranches( db: mongoose.mongo.Db, clientId: ObjectId, clientName: string, ): Promise { const branches = db.collection("branches"); const desired = [ { code: "RPT-NORTH", name: `${clientName} شمال`, city: "تهران", state: "تهران" }, { code: "RPT-CENTER", name: `${clientName} مرکز`, city: "تهران", state: "تهران" }, { code: "RPT-SOUTH", name: `${clientName} جنوب`, city: "تهران", state: "تهران" }, ]; for (let i = 0; i < desired.length; i++) { const seed = desired[i]; await branches.updateOne( { clientKey: clientId, code: seed.code }, { $set: { clientKey: clientId, code: seed.code, name: seed.name, city: seed.city, state: seed.state, address: `${seed.city} - ${seed.name} - شعبه دیتای تست گزارش`, phoneNumber: `02188${pad(i + 1, 4)}`, isActive: true, activityStartDate: new Date(), updatedAt: new Date(), seedMeta: { script: SCRIPT_NAME, kind: "branch" }, }, $setOnInsert: { createdAt: new Date() }, }, { upsert: true }, ); } const out = await branches .find({ clientKey: clientId, code: { $in: desired.map((d) => d.code) } }) .sort({ code: 1 }) .toArray(); return out.map((b) => ({ _id: asObjectId(b._id), code: String(b.code), name: String(b.name), city: String(b.city), state: String(b.state), address: String(b.address), })); } async function ensureRoster(ctx: { db: mongoose.mongo.Db; clientId: ObjectId; branches: BranchRef[]; passwordHash: string; publicPrefix: string; }): Promise<{ experts: SeedActor[]; damageExperts: SeedActor[]; fieldExperts: SeedActor[]; fileMakers: SeedActor[]; fileReviewers: SeedActor[]; callCenters: SeedActor[]; }> { const { db, clientId, branches, passwordHash, publicPrefix } = ctx; const ensureActors = async (args: { collectionName: string; role: string; count: number; branchScoped: boolean; clientKeyMode: "string" | "objectId"; prefix: string; }): Promise => { const col = db.collection(args.collectionName); const out: SeedActor[] = []; for (let i = 0; i < args.count; i++) { const index = i + 1; const branch = branches[i % branches.length]; const firstName = `${args.prefix}`; const lastName = `Seed ${pad(index, 2)}`; const email = `${publicPrefix.toLowerCase()}.${args.prefix.toLowerCase()}.${pad(index, 2)}@seed.local`; const nationalCode = `${randNationalBase(args.prefix)}${pad(index, 4)}`.slice(0, 10); const payload: AnyDoc = { firstName, lastName, email, username: email, nationalCode, password: passwordHash, mobile: `0912${pad(index + args.collectionName.length * 13, 6)}`, phone: `02177${pad(index, 4)}`, role: args.role, otp: "", state: "تهران", city: "تهران", address: `آدرس تست ${args.prefix} ${index}`, updatedAt: new Date(), seedMeta: { script: SCRIPT_NAME, kind: "actor", role: args.role }, }; if (args.branchScoped) { payload.branchId = branch._id; payload.branchCode = branch.code; } if (args.collectionName === "expert") { payload.userType = USER_TYPE.LEGAL; payload.clientKey = String(clientId); payload.sheba = `IR${pad(100000000000000000 + index, 24)}`; payload.insuActivityCo = String(clientId); } else if (args.collectionName === "damage-expert") { payload.userType = USER_TYPE.LEGAL; payload.clientKey = clientId; payload.insuActivityCo = String(clientId); } else if (args.collectionName === "file-maker") { payload.clientKey = clientId; payload.locations = [{ id: branch.code, name: branch.name }]; } else if (args.collectionName === "file-reviewer") { payload.clientKey = clientId; payload.locations = [{ id: branch.code, name: branch.name }]; } else if (args.collectionName === "field-expert") { payload.clientKey = clientId; } else if (args.collectionName === "call-center-agents") { payload.clientKey = clientId; } await col.updateOne( { email }, { $set: payload, $setOnInsert: { createdAt: new Date() } }, { upsert: true }, ); const doc = await col.findOne({ email }); if (!doc) throw new Error(`Failed to load upserted actor ${email}`); out.push({ _id: asObjectId(doc._id), firstName: String(doc.firstName), lastName: String(doc.lastName), fullName: `${doc.firstName} ${doc.lastName}`.trim(), email, branchId: doc.branchId ? asObjectId(doc.branchId) : undefined, branchCode: doc.branchCode ? String(doc.branchCode) : undefined, role: args.role, }); } return out; }; return { experts: await ensureActors({ collectionName: "expert", role: ROLE.EXPERT, count: 6, branchScoped: true, clientKeyMode: "string", prefix: "BlameExpert", }), damageExperts: await ensureActors({ collectionName: "damage-expert", role: ROLE.DAMAGE_EXPERT, count: 8, branchScoped: true, clientKeyMode: "objectId", prefix: "DamageExpert", }), fieldExperts: await ensureActors({ collectionName: "field-expert", role: ROLE.FIELD_EXPERT, count: 4, branchScoped: true, clientKeyMode: "objectId", prefix: "FieldExpert", }), fileMakers: await ensureActors({ collectionName: "file-maker", role: ROLE.FILE_MAKER, count: 3, branchScoped: true, clientKeyMode: "objectId", prefix: "FileMaker", }), fileReviewers: await ensureActors({ collectionName: "file-reviewer", role: ROLE.FILE_REVIEWER, count: 3, branchScoped: true, clientKeyMode: "objectId", prefix: "FileReviewer", }), callCenters: await ensureActors({ collectionName: "call-center-agents", role: ROLE.CALL_CENTER, count: 2, branchScoped: false, clientKeyMode: "objectId", prefix: "CallCenter", }), }; } function randNationalBase(prefix: string): string { const n = prefix .split("") .reduce((acc, ch) => acc + ch.charCodeAt(0), 0) .toString() .slice(0, 6); return n.padEnd(6, "7"); } async function purgeSeedPortfolio(db: mongoose.mongo.Db, seedTag: string) { const blameCases = db.collection("blameCases"); const claimCases = db.collection("claimCases"); const expertFileActivities = db.collection("expertFileActivities"); const [existingBlames, existingClaims] = await Promise.all([ blameCases.find({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }, { projection: { _id: 1 } }).toArray(), claimCases.find({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }, { projection: { _id: 1 } }).toArray(), ]); const fileIds = [...existingBlames, ...existingClaims].map((d) => asObjectId(d._id)); const [deletedBlames, deletedClaims, deletedActivities] = await Promise.all([ blameCases.deleteMany({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }), claimCases.deleteMany({ "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }), expertFileActivities.deleteMany({ $or: [ { "seedMeta.script": SCRIPT_NAME, "seedMeta.tag": seedTag }, ...(fileIds.length ? [{ fileId: { $in: fileIds } }] : []), ], }), ]); return { deletedBlames: deletedBlames.deletedCount ?? 0, deletedClaims: deletedClaims.deletedCount ?? 0, deletedActivities: deletedActivities.deletedCount ?? 0, }; } function scaleBucketCounts(total: number): Record { const keys = Object.keys(BUCKET_WEIGHTS) as UnifiedBucket[]; const raw = keys.map((key) => ({ key, exact: BUCKET_WEIGHTS[key] * total })); const counts = Object.fromEntries(raw.map(({ key, exact }) => [key, Math.floor(exact)])) as Record< UnifiedBucket, number >; let assigned = keys.reduce((sum, key) => sum + counts[key], 0); const byRemainder = raw .map(({ key, exact }) => ({ key, remainder: exact - Math.floor(exact) })) .sort((a, b) => b.remainder - a.remainder); let i = 0; while (assigned < total) { counts[byRemainder[i % byRemainder.length].key] += 1; assigned += 1; i += 1; } return counts; } function shuffleInPlace(items: T[], rng: () => number) { for (let i = items.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [items[i], items[j]] = [items[j], items[i]]; } } function buildBucketQueue(count: number, rng: () => number): UnifiedBucket[] { const counts = scaleBucketCounts(count); const queue: UnifiedBucket[] = []; for (const [bucket, qty] of Object.entries(counts) as Array<[UnifiedBucket, number]>) { for (let i = 0; i < qty; i++) queue.push(bucket); } shuffleInPlace(queue, rng); return queue; } function selectFlow(bucket: UnifiedBucket, indexInBucket: number): FlowKind { const flows = FLOWS_BY_BUCKET[bucket]; return flows[indexInBucket % flows.length]; } function selectFileType(bucket: UnifiedBucket, flow: FlowKind, ordinal: number): "THIRD_PARTY" | "CAR_BODY" { if (bucket === "WAITING_FOR_SECOND_PARTY" || bucket === "STOPPED") return "THIRD_PARTY"; if (flow === "V6") return "THIRD_PARTY"; return ordinal % 5 === 0 ? "CAR_BODY" : "THIRD_PARTY"; } function makeScenario(bucket: UnifiedBucket, flow: FlowKind, fileType: "THIRD_PARTY" | "CAR_BODY", ordinal: number): Scenario { switch (bucket) { case "WAITING_FOR_SECOND_PARTY": return { bucket, flow, fileType, blameStatus: CASE_STATUS.WAITING_FOR_SECOND_PARTY, blameKind: flow === "V6" ? "call_center" : null, blameActivity: null, hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: false, rateClaim: false, userRated: false, objection: false, }; case "WAITING_FOR_BLAME_EXPERT": return { bucket, flow, fileType, blameStatus: CASE_STATUS.WAITING_FOR_EXPERT, blameKind: flow === "V3" ? "field_expert" : "expert", blameActivity: flow === "V3" ? "handled" : "checked", hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: flow !== "V3", rateClaim: false, userRated: false, objection: false, }; case "WAITING_FOR_SIGNATURES": return { bucket, flow, fileType, blameStatus: CASE_STATUS.WAITING_FOR_SIGNATURES, blameKind: flow === "V2_EXPERT_INIT" ? "field_expert" : "expert", blameActivity: "handled", hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: true, rateClaim: false, userRated: false, objection: false, }; case "WAITING_FOR_DOCUMENT_RESEND": { const claimSide = ordinal % 2 === 0; return claimSide ? { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : "expert", blameActivity: "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.WAITING_FOR_USER_RESEND, claimReviewStatus: CLAIM_STATUS.NEEDS_REVISION, claimStage: "resend", claimActorKind: "damage_expert", claimActivity: "handled", rateBlame: true, rateClaim: false, userRated: false, objection: true, } : { bucket, flow, fileType, blameStatus: CASE_STATUS.WAITING_FOR_DOCUMENT_RESEND, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : "expert", blameActivity: "handled", hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: true, rateClaim: false, userRated: false, objection: false, }; } case "WAITING_FOR_DAMAGE_EXPERT": return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.WAITING_FOR_DAMAGE_EXPERT, claimReviewStatus: CLAIM_STATUS.PENDING, claimStage: "queue", claimActorKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : null, claimActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : null, rateBlame: flow !== "V4" && flow !== "V5", rateClaim: false, userRated: false, objection: false, }; case "EXPERT_REVIEWING": return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.EXPERT_REVIEWING, claimReviewStatus: CLAIM_STATUS.UNDER_REVIEW, claimStage: "reviewing", claimActorKind: "damage_expert", claimActivity: "checked", rateBlame: flow !== "V4" && flow !== "V5", rateClaim: false, userRated: false, objection: false, }; case "EXPERT_VALIDATING_FACTORS": return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.EXPERT_VALIDATING_REPAIR_FACTORS, claimReviewStatus: CLAIM_STATUS.UNDER_REVIEW, claimStage: "factor_validation", claimActorKind: "damage_expert", claimActivity: "checked", rateBlame: flow !== "V4" && flow !== "V5", rateClaim: true, userRated: false, objection: ordinal % 3 === 0, }; case "INSURER_REVIEW": { const statuses = [ CLAIM_CASE_STATUS.WAITING_FOR_INSURER_APPROVAL, CLAIM_CASE_STATUS.INSURER_REVIEW_AWAITING_OWNER_SIGN, CLAIM_CASE_STATUS.INSURER_REVIEW_MIXED_FACTORS_PENDING, CLAIM_CASE_STATUS.OWNER_REPAIR_FACTOR_UPLOAD_PENDING, ]; return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: statuses[ordinal % statuses.length], claimReviewStatus: CLAIM_STATUS.APPROVED, claimStage: "insurer_review", claimActorKind: "damage_expert", claimActivity: "handled", rateBlame: flow !== "V4" && flow !== "V5", rateClaim: true, userRated: ordinal % 2 === 0, objection: ordinal % 5 === 0, }; } case "COMPLETED": { const blameOnly = ordinal % 5 === 0 && (flow === "V2_USER" || flow === "V6"); return blameOnly ? { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: "expert", blameActivity: "handled", hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: true, rateClaim: false, userRated: false, objection: false, } : { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.COMPLETED, claimReviewStatus: CLAIM_STATUS.APPROVED, claimStage: "completed", claimActorKind: "damage_expert", claimActivity: "handled", rateBlame: flow !== "V4" && flow !== "V5", rateClaim: true, userRated: true, objection: ordinal % 8 === 0, }; } case "CANCELLED": { const claimSide = ordinal % 3 !== 0; return claimSide ? { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.CANCELLED, claimReviewStatus: CLAIM_STATUS.NEEDS_REVISION, claimStage: "cancelled", claimActorKind: ordinal % 2 === 0 ? "damage_expert" : null, claimActivity: ordinal % 2 === 0 ? "unchecked" : null, rateBlame: false, rateClaim: false, userRated: false, objection: false, } : { bucket, flow, fileType, blameStatus: ordinal % 2 === 0 ? CASE_STATUS.CANCELLED : CASE_STATUS.AUTO_CLOSED, blameKind: ordinal % 2 === 0 ? "expert" : null, blameActivity: ordinal % 2 === 0 ? "unchecked" : null, hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: false, rateClaim: false, userRated: false, objection: false, }; } case "REJECTED": return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" || flow === "V3" ? "field_expert" : flow === "V4" || flow === "V5" ? "file_maker" : "expert", blameActivity: flow === "V2_EXPERT_INIT" || flow === "V3" ? "handled" : flow === "V4" || flow === "V5" ? null : "handled", hasClaim: true, claimStatus: CLAIM_CASE_STATUS.REJECTED, claimReviewStatus: CLAIM_STATUS.REJECTED, claimStage: "rejected", claimActorKind: "damage_expert", claimActivity: "handled", rateBlame: false, rateClaim: true, userRated: ordinal % 3 === 0, objection: true, }; case "STOPPED": return { bucket, flow, fileType, blameStatus: CASE_STATUS.STOPPED, blameKind: ordinal % 2 === 0 ? "expert" : null, blameActivity: ordinal % 2 === 0 ? "unchecked" : null, hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: false, rateClaim: false, userRated: false, objection: false, }; case "IN_PROGRESS": { if (flow === "V5") { return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: "file_maker", blameActivity: null, hasClaim: true, claimStatus: ordinal % 2 === 0 ? CLAIM_CASE_STATUS.WAITING_FOR_FILE_MAKER_APPROVAL : CLAIM_CASE_STATUS.FILE_MAKER_REJECTED, claimReviewStatus: CLAIM_STATUS.APPROVED, claimStage: ordinal % 2 === 0 ? "file_maker_wait" : "file_maker_rejected", claimActorKind: "damage_expert", claimActivity: "handled", rateBlame: false, rateClaim: true, userRated: false, objection: ordinal % 4 === 0, }; } if (flow === "V4") { return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: "file_maker", blameActivity: null, hasClaim: true, claimStatus: CLAIM_CASE_STATUS.WAITING_FOR_FILE_REVIEWER, claimReviewStatus: CLAIM_STATUS.PENDING, claimStage: "user_progress", claimActorKind: null, claimActivity: null, rateBlame: false, rateClaim: false, userRated: false, objection: false, }; } if (flow === "V2_USER" && ordinal % 3 === 0) { return { bucket, flow, fileType, blameStatus: CASE_STATUS.OPEN, blameKind: null, blameActivity: null, hasClaim: false, claimStage: "none", claimActorKind: null, claimActivity: null, rateBlame: false, rateClaim: false, userRated: false, objection: false, }; } const progressStatuses = [ CLAIM_CASE_STATUS.CREATED, CLAIM_CASE_STATUS.SELECTING_OUTER_PARTS, CLAIM_CASE_STATUS.SELECTING_OTHER_PARTS, CLAIM_CASE_STATUS.CAPTURING_PART_DAMAGES, CLAIM_CASE_STATUS.UPLOADING_REQUIRED_DOCUMENTS, ]; const claimActorKind = flow === "V2_EXPERT_INIT" ? "field_expert" : null; return { bucket, flow, fileType, blameStatus: CASE_STATUS.COMPLETED, blameKind: flow === "V2_EXPERT_INIT" ? "field_expert" : flow === "V6" ? "call_center" : "expert", blameActivity: flow === "V2_EXPERT_INIT" ? "handled" : flow === "V6" ? null : ordinal % 4 === 0 ? "handled" : null, hasClaim: true, claimStatus: progressStatuses[ordinal % progressStatuses.length], claimReviewStatus: CLAIM_STATUS.PENDING, claimStage: "user_progress", claimActorKind, claimActivity: claimActorKind ? "handled" : null, rateBlame: flow === "V2_EXPERT_INIT", rateClaim: false, userRated: false, objection: false, }; } } } function actorRef(actor: SeedActor | undefined, actorType: string): AnyDoc | undefined { if (!actor) return undefined; return { actorId: actor._id, actorName: actor.fullName, actorType, }; } function expertProfileSnapshot(actor: SeedActor | undefined): AnyDoc | undefined { if (!actor) return undefined; return { fullName: actor.fullName, firstName: actor.firstName, lastName: actor.lastName, email: actor.email, }; } function buildCreatedAt(index: number, rng: () => number): Date { const monthOffset = index % 5; const monthStart = new Date(); monthStart.setHours(8, 0, 0, 0); monthStart.setDate(1); monthStart.setMonth(monthStart.getMonth() - monthOffset); const day = randInt(rng, 1, 24); const hour = randInt(rng, 8, 18); const minute = randInt(rng, 0, 50); return new Date( monthStart.getFullYear(), monthStart.getMonth(), day, hour, minute, randInt(rng, 0, 59), ); } function makePlate(index: number): string { return `${10 + (index % 89)}الف${100 + (index % 900)}-${10 + (index % 89)}`; } function makeInsurerRating(index: number): AnyDoc { const base = 2.8 + ((index % 18) / 10); return { collisionMethodAccuracy: Number(Math.min(5, base + 0.3).toFixed(1)), evaluationTimeliness: Number(Math.min(5, base - 0.1).toFixed(1)), accidentCauseAccuracy: Number(Math.min(5, base + 0.2).toFixed(1)), guiltyVehicleIdentification: Number(Math.min(5, base).toFixed(1)), botRating: Number(Math.min(5, base + 0.4).toFixed(1)), }; } function makeUserRating(index: number): AnyDoc { return { progressSpeed: 3 + (index % 3), registrationEase: 2 + (index % 4), overallEvaluation: 3 + (index % 3), comment: `Synthetic satisfaction sample #${index + 1}`, }; } function makeDamageParts(index: number): AnyDoc[] { return [ { id: `part-${index % 7}-front-bumper`, name: "Front bumper", side: "CENTER", label_fa: "سپر جلو", catalogKey: `front-bumper-${index % 7}`, }, { id: `part-${index % 11}-hood`, name: "Hood", side: "CENTER", label_fa: "کاپوت", catalogKey: `hood-${index % 11}`, }, ]; } function makeRequiredDocuments(index: number, stage: Scenario["claimStage"]): Record { const base = { nationalCard: { uploaded: true, fileId: new Types.ObjectId(), fileName: `national-card-${index}.jpg` }, greenCard: { uploaded: true, fileId: new Types.ObjectId(), fileName: `green-card-${index}.jpg` }, drivingLicense: { uploaded: stage !== "user_progress", fileId: new Types.ObjectId(), fileName: `license-${index}.jpg` }, }; if (stage === "user_progress") { return { ...base, damagePhotos: { uploaded: index % 2 === 0, fileId: new Types.ObjectId(), fileName: `damage-${index}.jpg` }, }; } if (stage === "resend") { return { ...base, expertRequestedDoc: { uploaded: false, fileId: new Types.ObjectId(), fileName: `resend-${index}.jpg` }, }; } return base; } function pickActorsForCase(ctx: SeedContext, index: number) { return { expert: ctx.experts[index % ctx.experts.length], damageExpert: ctx.damageExperts[(index * 3) % ctx.damageExperts.length], fieldExpert: ctx.fieldExperts[(index * 5) % ctx.fieldExperts.length], fileMaker: ctx.fileMakers[index % ctx.fileMakers.length], fileReviewer: ctx.fileReviewers[(index * 2) % ctx.fileReviewers.length], callCenter: ctx.callCenters[index % ctx.callCenters.length], }; } function buildParties(ctx: SeedContext, scenario: Scenario, index: number) { const targetName = `کاربر مقصر ${index + 1}`; const damagedName = `زیان‌دیده ${index + 1}`; const firstUserId = new Types.ObjectId(); const secondUserId = new Types.ObjectId(); const externalClientId = ctx.externalClientIds[index % ctx.externalClientIds.length]; const externalName = ctx.externalNames[index % ctx.externalNames.length]; const firstParty: AnyDoc = { role: "FIRST", person: { userId: firstUserId, fullName: targetName, phoneNumber: `091100${pad(index, 4)}`, clientId: ctx.clientId, birthday: "1370/01/01", nationalCodeOfInsurer: `00${pad(index + 1000, 8)}`, nationalCodeOfDriver: `00${pad(index + 2000, 8)}`, }, vehicle: { carName: "Peugeot 206", carModel: `139${index % 10}`, plate: makePlate(index), plateId: makePlate(index), name: "Peugeot 206", model: `139${index % 10}`, }, insurance: { policyNumber: `POL-${ctx.publicPrefix}-${pad(index + 1, 6)}`, company: ctx.client?.clientName?.persian ?? ctx.client?.clientName?.english ?? "Client", startDate: "1403/01/01", endDate: "1404/01/01", financialCeiling: "53000000", coverages: ["THIRD_PARTY"], carBodyInsurance: { policyNumber: `BDN-${ctx.publicPrefix}-${pad(index + 1, 6)}`, insurerCompany: ctx.client?.clientName?.persian ?? "Client", startDate: "1403/01/01", endDate: "1404/01/01", coverages: ["بدنه"], }, }, statement: { admitsGuilt: true, claimsDamage: scenario.fileType === "CAR_BODY", acceptsExpertOpinion: scenario.bucket !== "WAITING_FOR_BLAME_EXPERT", description: "اعتراف به تقصیر / دیتای تست", accidentDate: new Date(), accidentTime: "10:30", weatherCondition: "CLEAR", roadCondition: "DRY", lightCondition: "DAY", }, carBodyFirstForm: scenario.fileType === "CAR_BODY" ? { car: true, object: false } : undefined, confirmation: { partyRole: "FIRST", accepted: scenario.blameStatus === CASE_STATUS.COMPLETED, }, }; const secondParty: AnyDoc = { role: "SECOND", person: { userId: secondUserId, fullName: damagedName, phoneNumber: `091200${pad(index, 4)}`, clientId: externalClientId, clientName: externalName, birthday: "1372/02/02", nationalCodeOfInsurer: `00${pad(index + 3000, 8)}`, nationalCodeOfDriver: `00${pad(index + 4000, 8)}`, }, vehicle: { carName: "Dena", carModel: `140${index % 4}`, plate: makePlate(index + 700), plateId: makePlate(index + 700), name: "Dena", model: `140${index % 4}`, }, insurance: { policyNumber: `EXT-${ctx.publicPrefix}-${pad(index + 1, 6)}`, company: externalName, startDate: "1403/01/01", endDate: "1404/01/01", financialCeiling: "53000000", coverages: ["THIRD_PARTY"], }, statement: { admitsGuilt: false, claimsDamage: true, acceptsExpertOpinion: scenario.blameStatus === CASE_STATUS.COMPLETED, description: "زیان‌دیده / دیتای تست", }, confirmation: { partyRole: "SECOND", accepted: scenario.blameStatus === CASE_STATUS.COMPLETED, }, }; return { parties: scenario.fileType === "CAR_BODY" ? [firstParty] : [firstParty, secondParty], firstPartyUserId: firstUserId, secondPartyUserId: secondUserId, ownerUserId: scenario.fileType === "CAR_BODY" ? firstUserId : secondUserId, ownerFullName: scenario.fileType === "CAR_BODY" ? targetName : damagedName, }; } function pushHistory( history: AnyDoc[], date: Date, offsetMinutes: number, type: string, actor?: AnyDoc, metadata?: AnyDoc, ) { history.push({ type, ...(actor ? { actor } : {}), timestamp: addMinutes(date, offsetMinutes), ...(metadata ? { metadata } : {}), }); } function buildBlameHistory( ctx: SeedContext, scenario: Scenario, actors: ReturnType, createdAt: Date, fileType: "THIRD_PARTY" | "CAR_BODY", ) { const history: AnyDoc[] = []; if (scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3") { pushHistory( history, createdAt, 0, "FILE_CREATED_BY_FIELD_EXPERT", actorRef(actors.fieldExpert, "field_expert"), { flow: scenario.flow }, ); pushHistory( history, createdAt, 20, fileType === "CAR_BODY" ? "EXPERT_COMPLETED_CAR_BODY_FORM_V2" : "EXPERT_COMPLETED_THIRD_PARTY_FORM_V2", actorRef(actors.fieldExpert, "field_expert"), ); if (scenario.flow === "V3") { pushHistory( history, createdAt, 35, "V3_BLAME_ACCIDENT_VIDEO_UPLOADED", actorRef(actors.fieldExpert, "field_expert"), ); } } else if (scenario.flow === "V6") { pushHistory( history, createdAt, 0, "LINK_SENT", actorRef(actors.callCenter, "call_center"), { via: "call-center" }, ); pushHistory(history, createdAt, 15, "PARTY_OTP_SENT", actorRef(actors.callCenter, "call_center")); pushHistory(history, createdAt, 35, "PARTY_OTP_VERIFIED", { actorType: "user", actorName: "User" }); } else if (scenario.flow === "V4" || scenario.flow === "V5") { pushHistory(history, createdAt, 0, "LINK_SENT", actorRef(actors.fileMaker, "file_maker"), { flow: scenario.flow, }); pushHistory(history, createdAt, 20, "PARTY_OTPS_VERIFIED", actorRef(actors.fileMaker, "file_maker")); } else { pushHistory(history, createdAt, 0, "LINK_SENT", { actorType: "system" }, { flow: scenario.flow }); pushHistory(history, createdAt, 10, "PARTY_OTP_SENT", { actorType: "system" }); pushHistory(history, createdAt, 30, "PARTY_OTPS_VERIFIED", { actorType: "user", actorName: "User" }); } if (scenario.blameStatus === CASE_STATUS.WAITING_FOR_SECOND_PARTY) { pushHistory(history, createdAt, 60, "SECOND_PARTY_INVITED", { actorType: "system" }); return history; } if (scenario.fileType === "THIRD_PARTY") { pushHistory(history, createdAt, 75, "SECOND_PARTY_OTP_VERIFIED_ADVANCED", { actorType: "user", actorName: "User", }); } if (scenario.blameKind === "expert") { pushHistory(history, createdAt, 120, "BLAME_ASSIGNED", actorRef(actors.expert, "expert"), { note: "Synthetic seed assignment", }); } if (scenario.blameStatus === CASE_STATUS.WAITING_FOR_DOCUMENT_RESEND) { pushHistory( history, createdAt, 160, "BLAME_DOCUMENT_RESEND_STARTED", scenario.blameKind === "field_expert" ? actorRef(actors.fieldExpert, "field_expert") : scenario.blameKind === "expert" ? actorRef(actors.expert, "expert") : actorRef(actors.fileMaker, "file_maker"), { requestedItems: ["video", "voice", "description"] }, ); } else if ( scenario.blameStatus === CASE_STATUS.WAITING_FOR_SIGNATURES || scenario.blameStatus === CASE_STATUS.COMPLETED || scenario.hasClaim ) { pushHistory( history, createdAt, 170, scenario.flow === "V3" ? "V3_ACCIDENT_FIELDS_SAVED" : "ACCIDENT_FIELDS_SAVED_ADVANCED_TO_SIGNATURES", scenario.blameKind === "field_expert" ? actorRef(actors.fieldExpert, "field_expert") : scenario.blameKind === "expert" ? actorRef(actors.expert, "expert") : scenario.blameKind === "file_maker" ? actorRef(actors.fileMaker, "file_maker") : actorRef(actors.callCenter, "call_center"), ); } if (scenario.blameStatus === CASE_STATUS.STOPPED) { pushHistory(history, createdAt, 200, "PARTY_REJECTED_EXPERT_DECISION", { actorType: "user", actorName: "User" }); } return history; } function buildClaimWorkflow( scenario: Scenario, actors: ReturnType, createdAt: Date, ) { const assignedDamage = actorRef(actors.damageExpert, "damage_expert"); const assignedField = actorRef(actors.fieldExpert, "field_expert"); if (!scenario.hasClaim) return undefined; const base: AnyDoc = { locked: false, completedSteps: [CLAIM_STEP.CLAIM_CREATED] }; switch (scenario.claimStage) { case "user_progress": { const statusToStep: Record = { [CLAIM_CASE_STATUS.CREATED]: CLAIM_STEP.CLAIM_CREATED, [CLAIM_CASE_STATUS.SELECTING_OUTER_PARTS]: CLAIM_STEP.SELECT_OUTER_PARTS, [CLAIM_CASE_STATUS.SELECTING_OTHER_PARTS]: CLAIM_STEP.SELECT_OTHER_PARTS, [CLAIM_CASE_STATUS.CAPTURING_PART_DAMAGES]: CLAIM_STEP.CAPTURE_PART_DAMAGES, [CLAIM_CASE_STATUS.UPLOADING_REQUIRED_DOCUMENTS]: CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, [CLAIM_CASE_STATUS.WAITING_FOR_FILE_REVIEWER]: CLAIM_STEP.CLAIM_CREATED, }; const step = statusToStep[String(scenario.claimStatus)] ?? CLAIM_STEP.SELECT_OUTER_PARTS; return { ...base, currentStep: step, nextStep: step === CLAIM_STEP.CLAIM_CREATED ? CLAIM_STEP.SELECT_OUTER_PARTS : step === CLAIM_STEP.SELECT_OUTER_PARTS ? CLAIM_STEP.SELECT_OTHER_PARTS : step === CLAIM_STEP.SELECT_OTHER_PARTS ? CLAIM_STEP.CAPTURE_PART_DAMAGES : step === CLAIM_STEP.CAPTURE_PART_DAMAGES ? CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS : CLAIM_STEP.USER_SUBMISSION_COMPLETE, }; } case "queue": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, ], currentStep: CLAIM_STEP.USER_SUBMISSION_COMPLETE, nextStep: CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, assignedForReviewBy: scenario.claimActorKind === "field_expert" ? assignedField : undefined, }; case "reviewing": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, ], currentStep: CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, nextStep: CLAIM_STEP.INSURER_REVIEW, locked: true, lockedAt: addMinutes(createdAt, 345), expiredAt: addHours(addMinutes(createdAt, 345), 1), assignedForReviewBy: assignedDamage, lockedBy: assignedDamage, }; case "resend": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, ], currentStep: CLAIM_STEP.USER_EXPERT_RESEND, nextStep: CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, assignedForReviewBy: assignedDamage, }; case "factor_validation": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, CLAIM_STEP.OWNER_UPLOAD_FACTOR_DOCUMENTS, ], currentStep: CLAIM_STEP.EXPERT_COST_EVALUATION, nextStep: CLAIM_STEP.INSURER_REVIEW, locked: true, lockedAt: addMinutes(createdAt, 510), expiredAt: addHours(addMinutes(createdAt, 510), 1), assignedForReviewBy: assignedDamage, lockedBy: assignedDamage, }; case "insurer_review": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, CLAIM_STEP.INSURER_REVIEW, ], currentStep: CLAIM_STEP.INSURER_REVIEW, nextStep: CLAIM_STEP.CLAIM_COMPLETED, assignedForReviewBy: assignedDamage, }; case "file_maker_wait": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, CLAIM_STEP.INSURER_REVIEW, CLAIM_STEP.CLAIM_COMPLETED, ], currentStep: CLAIM_STEP.CLAIM_COMPLETED, nextStep: CLAIM_STEP.CLAIM_COMPLETED, assignedForReviewBy: assignedDamage, }; case "file_maker_rejected": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, CLAIM_STEP.INSURER_REVIEW, ], currentStep: CLAIM_STEP.INSURER_REVIEW, nextStep: CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, assignedForReviewBy: assignedDamage, }; case "completed": case "rejected": case "cancelled": return { ...base, completedSteps: [ CLAIM_STEP.CLAIM_CREATED, CLAIM_STEP.SELECT_OUTER_PARTS, CLAIM_STEP.SELECT_OTHER_PARTS, CLAIM_STEP.CAPTURE_PART_DAMAGES, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS, CLAIM_STEP.USER_SUBMISSION_COMPLETE, CLAIM_STEP.EXPERT_DAMAGE_ASSESSMENT, CLAIM_STEP.INSURER_REVIEW, CLAIM_STEP.CLAIM_COMPLETED, ], currentStep: CLAIM_STEP.CLAIM_COMPLETED, nextStep: CLAIM_STEP.CLAIM_COMPLETED, assignedForReviewBy: assignedDamage, }; case "none": return undefined; } } function buildClaimEvaluation( scenario: Scenario, actors: ReturnType, index: number, ) { if (!scenario.hasClaim) return undefined; const damageExpert = actors.damageExpert; const rating = scenario.rateClaim ? makeInsurerRating(index + 200) : undefined; const reply = { description: `Synthetic damage-expert reply #${index + 1}`, actorDetail: { actorId: String(damageExpert._id), actorName: damageExpert.fullName, }, expertProfileSnapshot: expertProfileSnapshot(damageExpert), parts: [ { partId: 1, partName: "Front bumper", side: "CENTER", factorNeeded: scenario.claimStage === "factor_validation" || String(scenario.claimStatus) === CLAIM_CASE_STATUS.INSURER_REVIEW_MIXED_FACTORS_PENDING || String(scenario.claimStatus) === CLAIM_CASE_STATUS.OWNER_REPAIR_FACTOR_UPLOAD_PENDING, partPrice: `${4_500_000 + (index % 20) * 100_000}`, }, { partId: 2, partName: "Hood", side: "CENTER", factorNeeded: false, partPrice: `${6_800_000 + (index % 15) * 120_000}`, }, ], }; const out: AnyDoc = {}; if ( scenario.claimStage === "reviewing" || scenario.claimStage === "resend" || scenario.claimStage === "factor_validation" || scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "rejected" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ) { out.damageExpertReply = reply; } if ( scenario.claimStage === "factor_validation" || scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "rejected" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ) { out.damageExpertReplyFinal = reply; } if (scenario.claimStage === "resend") { out.damageExpertResend = { resendDescription: "Synthetic resend request", resendDocuments: [{ kind: "green_card", label: "Green card" }], resendCarParts: [{ id: "front-bumper", label_fa: "سپر جلو" }], expertProfileSnapshot: expertProfileSnapshot(damageExpert), }; } if (scenario.claimStage === "factor_validation") { out.factorValidationExpertProfileSnapshot = expertProfileSnapshot(damageExpert); } if (scenario.objection) { out.objection = { submittedAt: new Date(), reason: "Synthetic objection", objectionParts: [{ partId: 1, reason: "Need review" }], }; } if (scenario.rateClaim) out.rating = rating; if (scenario.claimStage === "insurer_review") { if (String(scenario.claimStatus) === CLAIM_CASE_STATUS.INSURER_REVIEW_MIXED_FACTORS_PENDING) { out.ownerPricedPartsApproval = { agree: true, branchId: actors.damageExpert.branchId, }; } else if (String(scenario.claimStatus) === CLAIM_CASE_STATUS.OWNER_REPAIR_FACTOR_UPLOAD_PENDING) { out.ownerPricedPartsApproval = { agree: true, branchId: actors.damageExpert.branchId, }; } else { out.ownerInsurerApproval = { agree: true, branchId: actors.damageExpert.branchId, }; } } if (scenario.claimStage === "completed" || scenario.claimStage === "file_maker_wait") { out.ownerInsurerApproval = { agree: true, branchId: actors.damageExpert.branchId }; } if (scenario.claimStage === "rejected") { out.ownerInsurerApproval = { agree: false, branchId: actors.damageExpert.branchId }; } if ( scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ) { out.priceDrop = { total: 0, carPrice: 0, carModel: 0, }; } return out; } function buildClaimHistory( scenario: Scenario, actors: ReturnType, createdAt: Date, index: number, blameRequestId: ObjectId, publicId: string, ) { if (!scenario.hasClaim) return undefined; const history: AnyDoc[] = []; const creatorActor = scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? actorRef(actors.fieldExpert, "field_expert") : scenario.flow === "V4" || scenario.flow === "V5" ? actorRef(actors.fileReviewer, "file_reviewer") : { actorType: "user", actorName: "User", actorId: new Types.ObjectId() }; pushHistory(history, createdAt, 210, "CLAIM_CREATED", creatorActor, { blameRequestId, blamePublicId: publicId, }); const stepActor = scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? actorRef(actors.fieldExpert, "field_expert") : scenario.flow === "V4" || scenario.flow === "V5" ? actorRef(actors.fileReviewer, "file_reviewer") : { actorType: "user", actorName: "User", actorId: new Types.ObjectId() }; const maybePushStep = (offset: number, stepKey: string) => pushHistory(history, createdAt, offset, "STEP_COMPLETED", stepActor, { stepKey }); const pushFullUserFlow = scenario.claimStage === "queue" || scenario.claimStage === "reviewing" || scenario.claimStage === "resend" || scenario.claimStage === "factor_validation" || scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "rejected" || scenario.claimStage === "cancelled" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected"; if (pushFullUserFlow) { maybePushStep(235, CLAIM_STEP.SELECT_OUTER_PARTS); maybePushStep(255, CLAIM_STEP.SELECT_OTHER_PARTS); maybePushStep(275, CLAIM_STEP.CAPTURE_PART_DAMAGES); maybePushStep(295, CLAIM_STEP.UPLOAD_REQUIRED_DOCUMENTS); } else if (scenario.claimStage === "user_progress") { const status = String(scenario.claimStatus); if (status === CLAIM_CASE_STATUS.SELECTING_OTHER_PARTS) { maybePushStep(235, CLAIM_STEP.SELECT_OUTER_PARTS); } else if (status === CLAIM_CASE_STATUS.CAPTURING_PART_DAMAGES) { maybePushStep(235, CLAIM_STEP.SELECT_OUTER_PARTS); maybePushStep(255, CLAIM_STEP.SELECT_OTHER_PARTS); } else if (status === CLAIM_CASE_STATUS.UPLOADING_REQUIRED_DOCUMENTS) { maybePushStep(235, CLAIM_STEP.SELECT_OUTER_PARTS); maybePushStep(255, CLAIM_STEP.SELECT_OTHER_PARTS); maybePushStep(275, CLAIM_STEP.CAPTURE_PART_DAMAGES); } } if ( scenario.claimStage === "queue" || scenario.claimStage === "reviewing" || scenario.claimStage === "resend" || scenario.claimStage === "factor_validation" || scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "rejected" || scenario.claimStage === "cancelled" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ) { maybePushStep(315, CLAIM_STEP.USER_SUBMISSION_COMPLETE); } if ( scenario.claimStage === "reviewing" || scenario.claimStage === "resend" || scenario.claimStage === "factor_validation" || scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "rejected" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ) { pushHistory( history, createdAt, 345, "CLAIM_ASSIGNED", actorRef(actors.damageExpert, "damage_expert"), { note: "Synthetic claim assignment" }, ); } if (scenario.claimStage === "resend") { pushHistory( history, createdAt, 390, "EXPERT_RESEND_REQUESTED", actorRef(actors.damageExpert, "damage_expert"), { documentCount: 1, carPartCount: 1 }, ); } if ( scenario.claimStage === "factor_validation" || scenario.claimStage === "insurer_review" || scenario.claimStage === "completed" || scenario.claimStage === "rejected" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ) { pushHistory( history, createdAt, 420, index % 2 === 0 ? "EXPERT_REPLY_SUBMITTED" : "EXPERT_FINAL_REPLY_SUBMITTED", actorRef(actors.damageExpert, "damage_expert"), { note: "Synthetic expert reply" }, ); } if (scenario.claimStage === "factor_validation") { pushHistory( history, createdAt, 450, "OWNER_SIGNED_PRICED_PARTS_PENDING_FACTOR_UPLOAD", { actorType: "user", actorName: "User" }, ); pushHistory( history, createdAt, 510, "ALL_FACTORS_UPLOADED_PENDING_VALIDATION", { actorType: "user", actorName: "User" }, ); } if (scenario.claimStage === "insurer_review") { const eventType = String(scenario.claimStatus) === CLAIM_CASE_STATUS.INSURER_REVIEW_MIXED_FACTORS_PENDING || String(scenario.claimStatus) === CLAIM_CASE_STATUS.OWNER_REPAIR_FACTOR_UPLOAD_PENDING ? "OWNER_SIGNED_PRICED_PARTS_PENDING_FACTOR_UPLOAD" : "OWNER_SIGNED_INSURER_APPROVAL"; pushHistory(history, createdAt, 470, eventType, { actorType: "user", actorName: "User" }); } if (scenario.claimStage === "file_maker_wait") { pushHistory(history, createdAt, 500, "OWNER_SIGNED_INSURER_APPROVAL", { actorType: "user", actorName: "User" }); pushHistory(history, createdAt, 530, "V5_HELD_FOR_FILE_MAKER_APPROVAL", { actorType: "system" }); } if (scenario.claimStage === "file_maker_rejected") { pushHistory(history, createdAt, 500, "OWNER_SIGNED_INSURER_APPROVAL", { actorType: "user", actorName: "User" }); pushHistory(history, createdAt, 530, "V5_HELD_FOR_FILE_MAKER_APPROVAL", { actorType: "system" }); pushHistory(history, createdAt, 560, "V5_FILE_MAKER_REJECTED", actorRef(actors.fileMaker, "file_maker"), { reason: "Synthetic V5 correction request", }); } if (scenario.claimStage === "completed") { pushHistory(history, createdAt, 500, "OWNER_SIGNED_INSURER_APPROVAL", { actorType: "user", actorName: "User" }); pushHistory(history, createdAt, 550, "FANAVARAN_AUTO_SUBMIT_SUCCEEDED", { actorType: "system" }, { trackingCode: `FNV-${pad(index + 1, 6)}`, }); pushHistory(history, createdAt, 610, "USER_RATING_SUBMITTED", { actorType: "user", actorName: "User" }); } if (scenario.claimStage === "rejected") { pushHistory(history, createdAt, 500, "OWNER_REJECTED_INSURER_APPROVAL_PRICING", { actorType: "user", actorName: "User" }); pushHistory(history, createdAt, 520, "USER_OBJECTION_SUBMITTED", { actorType: "user", actorName: "User" }); } if (scenario.claimStage === "cancelled") { pushHistory(history, createdAt, 410, "EXPERT_RESEND_REQUESTED", actorRef(actors.damageExpert, "damage_expert"), { note: "Cancelled after inactivity", }); } return history; } function buildActivityEvents(args: { seedTag: string; scenario: Scenario; index: number; createdAt: Date; clientId: ObjectId; blameId: ObjectId; claimId?: ObjectId; actors: ReturnType; }): AnyDoc[] { const { scenario, index, createdAt, clientId, blameId, claimId, actors, seedTag } = args; const events: AnyDoc[] = []; const pushActivity = ( expertId: ObjectId, fileId: ObjectId, fileType: string, eventType: string, occurredAt: Date, idempotencyKey: string, ) => { events.push({ expertId, tenantId: clientId, fileId, fileType, eventType, occurredAt, idempotencyKey, seedMeta: { script: SCRIPT_NAME, tag: seedTag }, }); }; const actorForBlame = scenario.blameKind === "expert" ? actors.expert : scenario.blameKind === "field_expert" ? actors.fieldExpert : undefined; if (actorForBlame && scenario.blameActivity) { const t1 = addMinutes(createdAt, 125); const base = `${SCRIPT_NAME}:${seedTag}:blame:${blameId.toHexString()}:${actorForBlame._id.toHexString()}`; pushActivity(actorForBlame._id, blameId, FILE_KIND.BLAME, ACTIVITY.CHECKED, t1, `${base}:checked`); if (scenario.blameActivity === "handled") { pushActivity( actorForBlame._id, blameId, FILE_KIND.BLAME, ACTIVITY.HANDLED, addMinutes(t1, 40), `${base}:handled`, ); } if (scenario.blameActivity === "unchecked") { pushActivity( actorForBlame._id, blameId, FILE_KIND.BLAME, ACTIVITY.UNCHECKED, addMinutes(t1, 35), `${base}:unchecked`, ); } } const actorForClaim = scenario.claimActorKind === "damage_expert" ? actors.damageExpert : scenario.claimActorKind === "field_expert" ? actors.fieldExpert : undefined; if (claimId && actorForClaim && scenario.claimActivity) { const t1 = addMinutes(createdAt, 350); const base = `${SCRIPT_NAME}:${seedTag}:claim:${claimId.toHexString()}:${actorForClaim._id.toHexString()}`; pushActivity(actorForClaim._id, claimId, FILE_KIND.CLAIM, ACTIVITY.CHECKED, t1, `${base}:checked`); if (scenario.claimActivity === "handled") { pushActivity( actorForClaim._id, claimId, FILE_KIND.CLAIM, ACTIVITY.HANDLED, addMinutes(t1, 55), `${base}:handled`, ); } if (scenario.claimActivity === "unchecked") { pushActivity( actorForClaim._id, claimId, FILE_KIND.CLAIM, ACTIVITY.UNCHECKED, addMinutes(t1, 30), `${base}:unchecked`, ); } } return events; } function buildCaseDocuments(ctx: SeedContext, scenario: Scenario, index: number) { const actors = pickActorsForCase(ctx, index); const createdAt = buildCreatedAt(index, ctx.rng); const publicId = `${ctx.publicPrefix}-${pad(index + 1)}`; const blameId = new Types.ObjectId(); const claimId = scenario.hasClaim ? new Types.ObjectId() : undefined; const partiesBundle = buildParties(ctx, scenario, index); const blameHistory = buildBlameHistory(ctx, scenario, actors, createdAt, scenario.fileType); const blameUpdatedAt = blameHistory.length ? new Date(blameHistory[blameHistory.length - 1].timestamp) : createdAt; const blameAssignee = scenario.blameKind === "expert" ? actors.expert : scenario.blameKind === "field_expert" ? actors.fieldExpert : undefined; const blameDoc: AnyDoc = { _id: blameId, requestNo: `BL-${ctx.publicPrefix}-${pad(index + 1, 6)}`, publicId, type: scenario.fileType, status: scenario.blameStatus, blameStatus: scenario.fileType === "CAR_BODY" ? "UNKNOWN" : scenario.blameStatus === CASE_STATUS.WAITING_FOR_EXPERT ? "DISAGREEMENT" : "AGREED", parties: partiesBundle.parties, inquiries: {}, history: blameHistory, workflow: { locked: scenario.blameActivity === "checked", ...(blameAssignee ? { assignedForReviewBy: { actorId: blameAssignee._id, actorName: blameAssignee.fullName, }, } : {}), ...(scenario.blameActivity === "checked" && blameAssignee ? { lockedBy: { actorId: blameAssignee._id, actorName: blameAssignee.fullName, }, lockedAt: addMinutes(createdAt, 125), expiredAt: addHours(addMinutes(createdAt, 125), 1), } : {}), }, expert: { ...(blameAssignee ? { assignedExpertId: blameAssignee._id } : {}), creationMethod: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? CREATION_METHOD.IN_PERSON : CREATION_METHOD.LINK, filledBy: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? FILLED_BY.EXPERT : FILLED_BY.CUSTOMER, decision: scenario.blameStatus === CASE_STATUS.WAITING_FOR_EXPERT || scenario.blameStatus === CASE_STATUS.WAITING_FOR_SECOND_PARTY ? undefined : (() => { const decisionActorId = scenario.blameKind === "expert" ? actors.expert._id : scenario.blameKind === "field_expert" ? actors.fieldExpert._id : undefined; return { guiltyPartyId: partiesBundle.firstPartyUserId, description: "Synthetic seed decision", decidedAt: addMinutes(createdAt, 180), ...(decisionActorId ? { decidedByExpertId: decisionActorId } : {}), }; })(), ...(scenario.rateBlame ? { rating: makeInsurerRating(index + 100) } : {}), }, snapshot: { parties: clone(partiesBundle.parties) }, expertInitiated: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3", initiatedByFieldExpertId: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? actors.fieldExpert._id : undefined, creationMethod: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? CREATION_METHOD.IN_PERSON : CREATION_METHOD.LINK, filledBy: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? FILLED_BY.EXPERT : FILLED_BY.CUSTOMER, isMadeByFileMaker: scenario.flow === "V4" || scenario.flow === "V5", requiresFileMakerApproval: scenario.flow === "V5", callCenterInitiated: scenario.flow === "V6", initiatedByCallCenterId: scenario.flow === "V6" ? actors.callCenter._id : undefined, skipInitialFormStep: scenario.flow === "V6", createdAt, updatedAt: blameUpdatedAt, seedMeta: { script: SCRIPT_NAME, tag: ctx.seedTag, flow: scenario.flow, bucket: scenario.bucket, }, }; let claimDoc: AnyDoc | undefined; if (scenario.hasClaim && claimId) { const claimHistory = buildClaimHistory( scenario, actors, createdAt, index, blameId, publicId, )!; const claimUpdatedAt = claimHistory.length ? new Date(claimHistory[claimHistory.length - 1].timestamp) : addMinutes(createdAt, 210); const claimEvaluation = buildClaimEvaluation(scenario, actors, index); claimDoc = { _id: claimId, requestNo: `CL-${ctx.publicPrefix}-${pad(index + 1, 6)}`, publicId, status: scenario.claimStatus, claimStatus: scenario.claimReviewStatus, blameDocumentResendPending: scenario.blameStatus === CASE_STATUS.WAITING_FOR_DOCUMENT_RESEND, blameRequestId: blameId, blameRequestNo: blameDoc.requestNo, initiatedByFieldExpertId: scenario.flow === "V2_EXPERT_INIT" || scenario.flow === "V3" ? actors.fieldExpert._id : undefined, damagedPartyUserId: partiesBundle.ownerUserId, workflow: buildClaimWorkflow(scenario, actors, createdAt), owner: { userId: partiesBundle.ownerUserId, fullName: partiesBundle.ownerFullName, clientId: ctx.clientId, userClientKey: ctx.clientId, mobile: `091233${pad(index, 4)}`, }, vehicle: { plate: makePlate(index + 900), carName: scenario.fileType === "CAR_BODY" ? "Tara" : "Dena", carModel: scenario.fileType === "CAR_BODY" ? "1402" : "1401", carType: "sedan", }, money: { estimated: 11_000_000 + index * 10_000 }, claimNo: 500_000 + index, claimId: 700_000 + index, dmgCaseId: 800_000 + index, expertiseId: scenario.claimStage === "completed" || scenario.claimStage === "insurer_review" || scenario.claimStage === "rejected" || scenario.claimStage === "file_maker_wait" || scenario.claimStage === "file_maker_rejected" ? 900_000 + index : undefined, fanavaranSync: scenario.claimStage === "completed" ? { baseClaim: { status: "success", submittedAt: addMinutes(createdAt, 240) }, damageCase: { status: "success", submittedAt: addMinutes(createdAt, 260) }, attachments: { status: "success", submittedAt: addMinutes(createdAt, 280) }, expertise: { status: "success", submittedAt: addMinutes(createdAt, 300) }, } : undefined, damage: { selectedParts: makeDamageParts(index), otherParts: index % 4 === 0 ? ["windshield"] : [], expertAddedParts: index % 7 === 0 ? [{ partName: "Mirror", side: "LEFT" }] : [], }, media: {}, evaluation: claimEvaluation, inquiries: {}, snapshot: { accident: { type: scenario.fileType }, parties: clone(partiesBundle.parties), }, requiredDocuments: makeRequiredDocuments(index, scenario.claimStage), history: claimHistory, userRating: scenario.userRated ? makeUserRating(index) : undefined, requiresFileMakerApproval: scenario.flow === "V5", fileMakerApprovalActorId: scenario.flow === "V5" ? actors.fileMaker._id : undefined, fileMakerRejectionCount: scenario.claimStage === "file_maker_rejected" ? 1 : 0, fileMakerRejectionReason: scenario.claimStage === "file_maker_rejected" ? "Synthetic file-maker rejection" : undefined, createdAt: addMinutes(createdAt, 210), updatedAt: claimUpdatedAt, seedMeta: { script: SCRIPT_NAME, tag: ctx.seedTag, flow: scenario.flow, bucket: scenario.bucket, }, }; } const activityEvents = buildActivityEvents({ seedTag: ctx.seedTag, scenario, index, createdAt, clientId: ctx.clientId, blameId, claimId, actors, }); return { blameDoc, claimDoc, activityEvents, publicId }; } async function main() { loadEnvFile(); const mongoUri = resolveMongoUri(); const count = Math.max(1, Number(process.env.SEED_REPORTS_COUNT?.trim() || "1000")); const seedTag = process.env.SEED_REPORTS_TAG?.trim() || "reports-load-v1"; const publicPrefix = process.env.SEED_REPORTS_PUBLIC_PREFIX?.trim() || `RPT${shortHash(seedTag, 3)}`; const defaultPassword = process.env.SEED_REPORTS_DEFAULT_PASSWORD?.trim() || "Reports@724"; await mongoose.connect(mongoUri, { tls: process.env.MONGO_TLS === "true", tlsAllowInvalidCertificates: process.env.MONGO_TLS_ALLOW_INVALID_CERTS === "true", }); const db = mongoose.connection.db; if (!db) throw new Error("Mongo connection has no db handle"); const client = await resolveClient(db); const clientId = asObjectId(client._id); const clientLabel = client?.clientName?.persian || client?.clientName?.english || String(client._id); const rng = mulberry32(seedToInt(`${seedTag}:${clientId.toHexString()}:${count}`)); const passwordHash = await hashPassword(defaultPassword); const branches = await ensureBranches(db, clientId, String(clientLabel)); const roster = await ensureRoster({ db, clientId, branches, passwordHash, publicPrefix, }); const purge = await purgeSeedPortfolio(db, seedTag); const ctx: SeedContext = { client, clientId, clientIdStr: clientId.toHexString(), count, seedTag, publicPrefix, defaultPassword, rng, branches, experts: roster.experts, damageExperts: roster.damageExperts, fieldExperts: roster.fieldExperts, fileMakers: roster.fileMakers, fileReviewers: roster.fileReviewers, callCenters: roster.callCenters, externalClientIds: Array.from({ length: 6 }, () => new Types.ObjectId()), externalNames: [ "بیمه تست الف", "بیمه تست ب", "بیمه تست ج", "بیمه نمونه د", "بیمه نمونه ه", "بیمه نمونه و", ], }; const bucketQueue = buildBucketQueue(count, rng); const bucketSeen: Record = {}; const blameDocs: AnyDoc[] = []; const claimDocs: AnyDoc[] = []; const activityDocs: AnyDoc[] = []; const summaryByBucket: Record = {}; const summaryByFlow: Record = {}; for (let index = 0; index < bucketQueue.length; index++) { const bucket = bucketQueue[index]; bucketSeen[bucket] = (bucketSeen[bucket] ?? 0) + 1; const flow = selectFlow(bucket, bucketSeen[bucket] - 1); const fileType = selectFileType(bucket, flow, index); const scenario = makeScenario(bucket, flow, fileType, index); const { blameDoc, claimDoc, activityEvents } = buildCaseDocuments(ctx, scenario, index); blameDocs.push(blameDoc); if (claimDoc) claimDocs.push(claimDoc); activityDocs.push(...activityEvents); summaryByBucket[bucket] = (summaryByBucket[bucket] ?? 0) + 1; summaryByFlow[flow] = (summaryByFlow[flow] ?? 0) + 1; } if (blameDocs.length) await db.collection("blameCases").insertMany(blameDocs, { ordered: false }); if (claimDocs.length) await db.collection("claimCases").insertMany(claimDocs, { ordered: false }); if (activityDocs.length) { await db.collection("expertFileActivities").insertMany(activityDocs, { ordered: false }); } console.log("\n=== Insurer reports seed complete ==="); console.log(`Client : ${clientLabel} (${clientId.toHexString()})`); console.log(`Seed tag : ${seedTag}`); console.log(`Public prefix : ${publicPrefix}`); console.log(`Password : ${defaultPassword}`); console.log(`Branches : ${branches.length}`); console.log(`Experts : ${roster.experts.length} blame / ${roster.damageExperts.length} damage / ${roster.fieldExperts.length} field`); console.log(`Extra actors : ${roster.fileMakers.length} file-makers / ${roster.fileReviewers.length} file-reviewers / ${roster.callCenters.length} call-center`); console.log(`Purged : ${purge.deletedBlames} blame, ${purge.deletedClaims} claim, ${purge.deletedActivities} activities`); console.log(`Inserted : ${blameDocs.length} blame, ${claimDocs.length} claim, ${activityDocs.length} activities`); console.log("\nBy unified bucket:"); for (const key of Object.keys(summaryByBucket).sort()) { console.log(`- ${key}: ${summaryByBucket[key]}`); } console.log("\nBy flow:"); for (const key of Object.keys(summaryByFlow).sort()) { console.log(`- ${key}: ${summaryByFlow[key]}`); } } main() .catch((error) => { console.error("Seed failed:", error); process.exitCode = 1; }) .finally(async () => { try { await mongoose.disconnect(); } catch { // ignore disconnect errors on exit } });