Files
yara724-api/scripts/seed-insurer-reports-fixtures.ts
2026-08-25 10:11:19 +03:30

2428 lines
79 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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=<mongo object id>
* 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<string, any>;
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<UnifiedBucket, number> = {
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<UnifiedBucket, FlowKind[]> = {
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<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, 50)}...`);
}
return uri;
}
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);
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<T>(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<T>(value: T): T {
return JSON.parse(JSON.stringify(value));
}
function maybe<T>(condition: boolean, value: T): T | undefined {
return condition ? value : undefined;
}
async function resolveClient(db: mongoose.mongo.Db): Promise<AnyDoc> {
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<BranchRef[]> {
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<SeedActor[]> => {
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<UnifiedBucket, number> {
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<T>(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<string, AnyDoc> {
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<typeof pickActorsForCase>,
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<typeof pickActorsForCase>,
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<string, string> = {
[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<typeof pickActorsForCase>,
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<typeof pickActorsForCase>,
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<typeof pickActorsForCase>;
}): 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<string, number> = {};
const blameDocs: AnyDoc[] = [];
const claimDocs: AnyDoc[] = [];
const activityDocs: AnyDoc[] = [];
const summaryByBucket: Record<string, number> = {};
const summaryByFlow: Record<string, number> = {};
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
}
});