forked from Yara724/api
2428 lines
79 KiB
TypeScript
2428 lines
79 KiB
TypeScript
/**
|
||
* 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
|
||
}
|
||
});
|