From 2df7a889d35c9f793b875192c8fd1dd6a581dcfe Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 24 Aug 2026 11:11:31 +0330 Subject: [PATCH 1/2] YARA-1244 --- scripts/data/parsian-tehran/branches.json | 182 ++-------- scripts/seed-parsian-tehran.ts | 343 ++++++++++++++---- .../entities/schema/file-maker.schema.ts | 22 ++ .../entities/schema/file-reviewer.schema.ts | 22 ++ 4 files changed, 341 insertions(+), 228 deletions(-) diff --git a/scripts/data/parsian-tehran/branches.json b/scripts/data/parsian-tehran/branches.json index edb9711..14c424d 100644 --- a/scripts/data/parsian-tehran/branches.json +++ b/scripts/data/parsian-tehran/branches.json @@ -2,143 +2,13 @@ "clientCode": 8, "branches": [ { - "code": "100100", - "name": "واحدصدورالکترونيکي", - "fullName": "واحدصدورالکترونيکي(100100)", + "code": "210120", + "name": "شعبه والفجر", + "fullName": "شعبه والفجر(210120)", "city": "تهران", "state": "تهران", - "address": "خيابان وليعصر_بلوار ميرداماد_پلاک 22", - "phoneNumber": "8259", - "isActive": true - }, - { - "code": "110011", - "name": "ستاد مرکزي", - "fullName": "ستاد مرکزي(110011)", - "city": "تهران", - "state": "تهران", - "address": "تهران، خيابان وليعصر، بالاتراز ميرداماد، خيابان قباديان غربي، پلاك22", - "phoneNumber": "8259", - "isActive": true - }, - { - "code": "111130", - "name": "شعبه ويژه ميرداماد", - "fullName": "شعبه ويژه ميرداماد(111130)", - "city": "تهران", - "state": "تهران", - "address": "تهران، خيابان وليعصر، بالاتراز ميرداماد، خيابان قباديان غربي، پلاك22", - "phoneNumber": "8259", - "isActive": true - }, - { - "code": "120021", - "name": "سرپرستي منطقه يک كشور", - "fullName": "سرپرستي منطقه يک كشور(120021)", - "city": "تهران", - "state": "تهران", - "address": "تهران،خيابان وليعصر ،خيابان قباديان غربي ،پلاک 22 ، طبقه همکف", - "phoneNumber": "0218259", - "isActive": true - }, - { - "code": "130031", - "name": "سرپرستي منطقه مركزي كشور", - "fullName": "سرپرستي منطقه مركزي كشور(130031)", - "city": "تهران", - "state": "تهران", - "address": "اصفهان، خيابان امام خميني (ره) - بعد از چهارراه شريف - کوچه شهيد احمدي (85)", - "phoneNumber": "03133328257", - "isActive": true - }, - { - "code": "140041", - "name": "سرپرستي منطقه شمالغرب کشور", - "fullName": "سرپرستي منطقه شمالغرب کشور(140041)", - "city": "تهران", - "state": "تهران", - "address": "تبريز- خيابان ائل گلي - فلکه خيام - نبش فلکه رجائي - بيمه پارسيان", - "phoneNumber": "04133832289", - "isActive": true - }, - { - "code": "150051", - "name": "سرپرستي منطقه جنوب كشور", - "fullName": "سرپرستي منطقه جنوب كشور(150051)", - "city": "تهران", - "state": "تهران", - "address": "شيراز ـ فلکه فرودگاه (ميدان بسيج) ـ ابتداي بلوار سياحتگر", - "phoneNumber": "01738315473", - "isActive": true - }, - { - "code": "150053", - "name": "سرپرست منطقه جنوب شرقي کشور", - "fullName": "سرپرست منطقه جنوب شرقي کشور(150053)", - "city": "کرمان", - "state": "کرمان", - "address": "کرمان، حافظ، بعد از چهارراه جامي، پلاک 153", - "phoneNumber": "03432718000", - "isActive": true - }, - { - "code": "160061", - "name": "سرپرستي منطقه شرق كشور", - "fullName": "سرپرستي منطقه شرق كشور(160061)", - "city": "تهران", - "state": "تهران", - "address": "مشهد ـ خيام شمالي ـ نبش خيام شمالي 36", - "phoneNumber": "05137659005", - "isActive": true - }, - { - "code": "170071", - "name": "سرپرستي منطقه غرب كشور", - "fullName": "سرپرستي منطقه غرب كشور(170071)", - "city": "تهران", - "state": "تهران", - "address": "کرمانشاه . ميدان مرکزي خيابان خرم نبش کوي بسيج ساختمان عرفان", - "phoneNumber": "08338431017", - "isActive": true - }, - { - "code": "180081", - "name": "سرپرستي منطقه شمال شرق کشور", - "fullName": "سرپرستي منطقه شمال شرق کشور(180081)", - "city": "ساري", - "state": "مازندران", - "address": "ساري، شعبه ساري", - "phoneNumber": "01133207241", - "isActive": true - }, - { - "code": "180083", - "name": "سرپرست منطقه شمال کشوري", - "fullName": "سرپرست منطقه شمال کشوري(180083)", - "city": "رشت", - "state": "گيلان", - "address": "رشت، بلوار آيت اله رودباري،کدپستي:4144761893", - "phoneNumber": "01333512135", - "isActive": true - }, - { - "code": "190092", - "name": "سرپرستي جنوب غربي کشور", - "fullName": "سرپرستي جنوب غربي کشور(190092)", - "city": "اهواز", - "state": "خوزستان", - "address": "اهواز،تقاطع بلوار ساحلي گلستان (خيابان فروردين)،نبش خيابان نصرت شمالي ،پلاک 701 کد پستي 6155977139", - "phoneNumber": "06133743877", - "isActive": true - }, - { - "code": "210040", - "name": "شعبه شرق تهران", - "fullName": "شعبه شرق تهران(210040)", - "city": "تهران", - "state": "تهران", - "address": "تهران، خيابان دماوند، بعداز چهارراه تهرانپارس، روبروي تعميرگاه مرکزي شماره يک سايپا، پلاک129", - "phoneNumber": "77393783-4", + "address": "تهران، اميرآبادشمالي، شهرک والفجر، ضلع جنوب غربي ميدان استادخسرو سينايي", + "phoneNumber": "86051332", "isActive": true }, { @@ -151,6 +21,26 @@ "phoneNumber": "66021968", "isActive": true }, + { + "code": "110011", + "name": "ستاد مرکزي", + "fullName": "ستاد مرکزي(110011)", + "city": "تهران", + "state": "تهران", + "address": "تهران، خيابان وليعصر، بالاتراز ميرداماد، خيابان قباديان غربي، پلاك22", + "phoneNumber": "8259", + "isActive": true + }, + { + "code": "210040", + "name": "شعبه شرق تهران", + "fullName": "شعبه شرق تهران(210040)", + "city": "تهران", + "state": "تهران", + "address": "تهران، خيابان دماوند، بعداز چهارراه تهرانپارس، روبروي تعميرگاه مرکزي شماره يک سايپا، پلاک129", + "phoneNumber": "77393783-4", + "isActive": true + }, { "code": "210110", "name": "شعبه پونک", @@ -162,24 +52,14 @@ "isActive": true }, { - "code": "210120", - "name": "شعبه والفجر", - "fullName": "شعبه والفجر(210120)", + "code": "111130", + "name": "شعبه ويژه ميرداماد", + "fullName": "شعبه ويژه ميرداماد(111130)", "city": "تهران", "state": "تهران", - "address": "تهران، اميرآبادشمالي، شهرک والفجر، ضلع جنوب غربي ميدان استادخسرو سينايي", - "phoneNumber": "86051332", - "isActive": true - }, - { - "code": "210150", - "name": "شعبه شمال شرق تهران", - "fullName": "شعبه شمال شرق تهران(210150)", - "city": "تهران", - "state": "تهران", - "address": "تهران، ضلع شمال غربي ميدان بني هاشم، نبش خيابان كشوري، پلاك13", - "phoneNumber": "26244319", + "address": "تهران، خيابان وليعصر، بالاتراز ميرداماد، خيابان قباديان غربي، پلاك22", + "phoneNumber": "8259", "isActive": true } ] -} \ No newline at end of file +} diff --git a/scripts/seed-parsian-tehran.ts b/scripts/seed-parsian-tehran.ts index 141ecc7..f39a6b6 100644 --- a/scripts/seed-parsian-tehran.ts +++ b/scripts/seed-parsian-tehran.ts @@ -1,13 +1,16 @@ /** - * One-time seed for Parsian (clientCode=8) Tehran branches + field experts. + * One-time seed for Parsian (clientCode=8) Tehran branches + file reviewers + file makers. * * Usage (before starting the app): * npm run seed:parsian-tehran * * Optional env: + * SEED_PARSIAN_TEHRAN_DEFAULT_PASSWORD=Parsian@724 + * + * Backward-compatible env alias: * SEED_FIELD_EXPERT_DEFAULT_PASSWORD=Parsian@724 */ -import { readFileSync, existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import * as crypto from "node:crypto"; import mongoose, { Schema, Types } from "mongoose"; @@ -23,17 +26,29 @@ type BranchSeed = { isActive?: boolean; }; -type FieldExpertSeed = { +type ExpertLocationSeed = { + id: string; + name: string; +}; + +type FileReviewerSeed = { nationalCode: string; mobile?: string; firstName: string; lastName: string; - branchCode: string; - branchName?: string; - city?: string; - state?: string; - title?: string; - expertCode?: string; + locations: ExpertLocationSeed[]; + ThirdPartyExpertiseClaim?: string; + CarBodyExpertiseClaim?: string; +}; + +type FileMakerSeed = { + nationalCode: string; + mobile?: string; + firstName: string; + lastName: string; + locations: ExpertLocationSeed[]; + ThirdPartyClaimExpertId?: string; + CarBodyClaimExpertId?: string; }; function stripQuotes(value: string): string { @@ -76,7 +91,6 @@ function loadEnvFile() { process.env[key] = stripQuotes(value); } - // Expand ${VAR} placeholders (same as Nest ConfigModule expandVariables). for (let pass = 0; pass < 5; pass++) { let changed = false; for (const key of Object.keys(process.env)) { @@ -110,7 +124,7 @@ function resolveMongoUri(): string { return uri; } -async function ensureFieldExpertIndexes(collection: mongoose.Collection) { +async function ensureUserIndexes(collection: mongoose.Collection) { const indexes = await collection.indexes(); const emailIndex = indexes.find((idx) => idx.key?.email === 1); if (emailIndex && !emailIndex.sparse) { @@ -134,6 +148,17 @@ function hashPassword(password: string): Promise { }); } +function normalizeLocations(locations: ExpertLocationSeed[]): ExpertLocationSeed[] { + const deduped = new Map(); + for (const location of locations ?? []) { + const id = String(location?.id ?? "").trim(); + const name = String(location?.name ?? "").trim(); + if (!id || !name || deduped.has(id)) continue; + deduped.set(id, { id, name }); + } + return [...deduped.values()]; +} + const ClientSchema = new Schema( { clientName: { type: Object, required: true }, @@ -159,7 +184,15 @@ const BranchSchema = new Schema( BranchSchema.index({ clientKey: 1, code: 1 }, { unique: true }); -const FieldExpertSchema = new Schema( +const ExpertLocationSchema = new Schema( + { + id: { type: String, required: true }, + name: { type: String, required: true }, + }, + { _id: false, id: false, versionKey: false }, +); + +const FileReviewerSchema = new Schema( { firstName: { type: String, required: true }, lastName: { type: String, required: true }, @@ -168,21 +201,180 @@ const FieldExpertSchema = new Schema( nationalCode: { type: String, index: true, sparse: true }, clientKey: { type: Schema.Types.ObjectId, index: true }, branchId: { type: Schema.Types.ObjectId, index: true }, + locations: { type: [ExpertLocationSchema], default: [] }, password: { type: String, required: true }, mobile: { type: String }, phone: { type: String }, - role: { type: String, default: "field_expert" }, + role: { type: String, default: "file_reviewer" }, otp: { type: String, default: "" }, expertCode: { type: String, required: false }, + ThirdPartyExpertiseClaim: { type: String, required: false }, + CarBodyExpertiseClaim: { type: String, required: false }, }, - { collection: "field-expert", versionKey: false, timestamps: true }, + { collection: "file-reviewer", versionKey: false, timestamps: true }, ); -FieldExpertSchema.index( +FileReviewerSchema.index( { clientKey: 1, nationalCode: 1 }, { unique: true, sparse: true }, ); +const FileMakerSchema = new Schema( + { + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + email: { type: String, unique: true, sparse: true }, + username: { type: String }, + nationalCode: { type: String, index: true, sparse: true }, + clientKey: { type: Schema.Types.ObjectId, index: true }, + branchId: { type: Schema.Types.ObjectId, index: true }, + locations: { type: [ExpertLocationSchema], default: [] }, + password: { type: String, required: true }, + mobile: { type: String }, + phone: { type: String }, + role: { type: String, default: "file_maker" }, + otp: { type: String, default: "" }, + expertCode: { type: String, required: false }, + ThirdPartyClaimExpertId: { type: String, required: false }, + CarBodyClaimExpertId: { type: String, required: false }, + }, + { collection: "file-maker", versionKey: false, timestamps: true }, +); + +FileMakerSchema.index( + { clientKey: 1, nationalCode: 1 }, + { unique: true, sparse: true }, +); + +function resolveSeedLocations( + seedLocations: ExpertLocationSeed[], + branchMetaByCode: Map, +) { + const resolved: { id: string; name: string; branchId: Types.ObjectId }[] = []; + const skippedCodes: string[] = []; + + for (const location of normalizeLocations(seedLocations)) { + const branch = branchMetaByCode.get(location.id); + if (!branch) { + skippedCodes.push(location.id); + continue; + } + resolved.push({ + id: location.id, + name: branch.name || location.name, + branchId: branch._id, + }); + } + + return { + resolved, + skippedCodes, + primaryBranchId: resolved[0]?.branchId, + }; +} + +async function upsertRoleUsers({ + label, + seeds, + model, + clientKey, + hashedPassword, + branchMetaByCode, + role, + codeFields, +}: { + label: string; + seeds: Array>; + model: mongoose.Model; + clientKey: Types.ObjectId; + hashedPassword: string; + branchMetaByCode: Map; + role: string; + codeFields: string[]; +}) { + let created = 0; + let updated = 0; + let skipped = 0; + + for (const seed of seeds) { + const { resolved, skippedCodes, primaryBranchId } = resolveSeedLocations( + seed.locations, + branchMetaByCode, + ); + + if (skippedCodes.length > 0) { + console.warn( + `Skipping unknown ${label} locations for ${seed.nationalCode}: ${skippedCodes.join( + ", ", + )}`, + ); + } + + if (!primaryBranchId || resolved.length === 0) { + console.warn( + `Skipping ${label} ${seed.nationalCode}: no valid branch locations remained`, + ); + skipped++; + continue; + } + + const setPayload: Record = { + firstName: seed.firstName, + lastName: seed.lastName, + username: seed.nationalCode, + nationalCode: seed.nationalCode, + clientKey, + branchId: primaryBranchId, + locations: resolved.map(({ id, name }) => ({ id, name })), + role, + otp: "", + }; + + if (seed.mobile) { + setPayload.mobile = seed.mobile; + } + + const unsetPayload: Record = { + expertCode: "", + }; + + for (const field of codeFields) { + if (seed[field]) { + setPayload[field] = seed[field]; + } else { + unsetPayload[field] = ""; + } + } + + const existing = await model.findOne({ + clientKey, + nationalCode: seed.nationalCode, + }); + + if (existing) { + await model.updateOne( + { _id: existing._id }, + { + $set: { + ...setPayload, + password: existing.password, + }, + $unset: unsetPayload, + }, + ); + updated++; + } else { + await model.create({ + ...setPayload, + password: hashedPassword, + }); + created++; + } + } + + return { created, updated, skipped }; +} + async function main() { loadEnvFile(); const mongoUri = resolveMongoUri(); @@ -191,12 +383,24 @@ async function main() { const branchesFile = JSON.parse( readFileSync(join(dataDir, "branches.json"), "utf8"), ) as { clientCode: number; branches: BranchSeed[] }; - const expertsFile = JSON.parse( - readFileSync(join(dataDir, "field-experts.json"), "utf8"), - ) as { clientCode: number; fieldExperts: FieldExpertSeed[] }; + const fileReviewersFile = JSON.parse( + readFileSync(join(dataDir, "file-reviewers.json"), "utf8"), + ) as { clientCode: number; fileReviewers: FileReviewerSeed[] }; + const fileMakersFile = JSON.parse( + readFileSync(join(dataDir, "file-makers.json"), "utf8"), + ) as { clientCode: number; fileMakers: FileMakerSeed[] }; + + if ( + branchesFile.clientCode !== fileReviewersFile.clientCode || + branchesFile.clientCode !== fileMakersFile.clientCode + ) { + throw new Error("Seed data clientCode mismatch between branch/reviewer/maker files"); + } const defaultPassword = - process.env.SEED_FIELD_EXPERT_DEFAULT_PASSWORD ?? "123321"; + process.env.SEED_PARSIAN_TEHRAN_DEFAULT_PASSWORD ?? + process.env.SEED_FIELD_EXPERT_DEFAULT_PASSWORD ?? + "123321"; const hashedPassword = await hashPassword(defaultPassword); await mongoose.connect(mongoUri, { @@ -204,11 +408,14 @@ async function main() { tlsAllowInvalidCertificates: process.env.MONGO_TLS_ALLOW_INVALID_CERTS === "true", }); + const Client = mongoose.model("ClientSeedClient", ClientSchema); const Branch = mongoose.model("ClientSeedBranch", BranchSchema); - const FieldExpert = mongoose.model("ClientSeedFieldExpert", FieldExpertSchema); + const FileReviewer = mongoose.model("ClientSeedFileReviewer", FileReviewerSchema); + const FileMaker = mongoose.model("ClientSeedFileMaker", FileMakerSchema); - await ensureFieldExpertIndexes(FieldExpert.collection); + await ensureUserIndexes(FileReviewer.collection); + await ensureUserIndexes(FileMaker.collection); const client = await Client.findOne({ clientCode: branchesFile.clientCode, @@ -220,7 +427,7 @@ async function main() { } const clientKey = new Types.ObjectId(String(client._id)); - const branchIdByCode = new Map(); + const branchMetaByCode = new Map(); let branchesCreated = 0; let branchesUpdated = 0; @@ -239,67 +446,45 @@ async function main() { phoneNumber: branch.phoneNumber, isActive: branch.isActive ?? true, }; + if (existing) { await Branch.updateOne({ _id: existing._id }, { $set: payload }); - branchIdByCode.set(branch.code, existing._id as Types.ObjectId); + branchMetaByCode.set(branch.code, { + _id: existing._id as Types.ObjectId, + name: branch.name, + }); branchesUpdated++; } else { const created = await Branch.create(payload); - branchIdByCode.set(branch.code, created._id as Types.ObjectId); + branchMetaByCode.set(branch.code, { + _id: created._id as Types.ObjectId, + name: branch.name, + }); branchesCreated++; } } - let expertsCreated = 0; - let expertsUpdated = 0; - let expertsSkipped = 0; + const fileReviewersResult = await upsertRoleUsers({ + label: "file-reviewer", + seeds: fileReviewersFile.fileReviewers, + model: FileReviewer, + clientKey, + hashedPassword, + branchMetaByCode, + role: "file_reviewer", + codeFields: ["ThirdPartyExpertiseClaim", "CarBodyExpertiseClaim"], + }); - for (const expert of expertsFile.fieldExperts) { - const branchId = branchIdByCode.get(expert.branchCode); - if (!branchId) { - console.warn( - `Skipping ${expert.nationalCode}: unknown branch ${expert.branchCode}`, - ); - expertsSkipped++; - continue; - } - - const payload = { - firstName: expert.firstName, - lastName: expert.lastName, - username: expert.nationalCode, - nationalCode: expert.nationalCode, - clientKey, - branchId, - password: hashedPassword, - mobile: expert.mobile, - role: "field_expert", - otp: "", - expertCode: expert.expertCode, - }; - - const existing = await FieldExpert.findOne({ - clientKey, - nationalCode: expert.nationalCode, - }); - - if (existing) { - await FieldExpert.updateOne( - { _id: existing._id }, - { - $set: { - ...payload, - // Do not rotate password on re-seed unless explicitly desired. - password: existing.password, - }, - }, - ); - expertsUpdated++; - } else { - await FieldExpert.create(payload); - expertsCreated++; - } - } + const fileMakersResult = await upsertRoleUsers({ + label: "file-maker", + seeds: fileMakersFile.fileMakers, + model: FileMaker, + clientKey, + hashedPassword, + branchMetaByCode, + role: "file_maker", + codeFields: ["ThirdPartyClaimExpertId", "CarBodyClaimExpertId"], + }); console.log("Parsian Tehran seed completed."); console.log({ @@ -307,11 +492,15 @@ async function main() { clientKey: String(clientKey), branchesCreated, branchesUpdated, - expertsCreated, - expertsUpdated, - expertsSkipped, + fileReviewersCreated: fileReviewersResult.created, + fileReviewersUpdated: fileReviewersResult.updated, + fileReviewersSkipped: fileReviewersResult.skipped, + fileMakersCreated: fileMakersResult.created, + fileMakersUpdated: fileMakersResult.updated, + fileMakersSkipped: fileMakersResult.skipped, defaultPassword, - loginHint: "Use nationalCode + password on POST /actor/login with role field_expert", + loginHint: + "Use nationalCode + password on POST /actor/login with role file_reviewer or file_maker", }); await mongoose.disconnect(); diff --git a/src/users/entities/schema/file-maker.schema.ts b/src/users/entities/schema/file-maker.schema.ts index ab03e32..c05da2d 100644 --- a/src/users/entities/schema/file-maker.schema.ts +++ b/src/users/entities/schema/file-maker.schema.ts @@ -2,6 +2,19 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { Types } from "mongoose"; import { RoleEnum } from "src/Types&Enums/role.enum"; +@Schema({ _id: false, versionKey: false }) +export class FileMakerLocationModel { + @Prop({ type: String, required: true }) + id: string; + + @Prop({ type: String, required: true }) + name: string; +} + +export const FileMakerLocationDbSchema = SchemaFactory.createForClass( + FileMakerLocationModel, +); + @Schema({ collection: "file-maker", versionKey: false, @@ -31,6 +44,9 @@ export class FileMakerModel { @Prop({ type: Types.ObjectId, index: true }) branchId?: Types.ObjectId; + @Prop({ type: [FileMakerLocationDbSchema], default: [] }) + locations?: FileMakerLocationModel[]; + @Prop({ required: true }) password: string; @@ -49,6 +65,12 @@ export class FileMakerModel { @Prop({ type: "string", required: false }) expertCode?: string; + @Prop({ type: String, required: false }) + ThirdPartyClaimExpertId?: string; + + @Prop({ type: String, required: false }) + CarBodyClaimExpertId?: string; + createdAt: Date; } diff --git a/src/users/entities/schema/file-reviewer.schema.ts b/src/users/entities/schema/file-reviewer.schema.ts index d8a8b06..d91519b 100644 --- a/src/users/entities/schema/file-reviewer.schema.ts +++ b/src/users/entities/schema/file-reviewer.schema.ts @@ -2,6 +2,19 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { Types } from "mongoose"; import { RoleEnum } from "src/Types&Enums/role.enum"; +@Schema({ _id: false, versionKey: false }) +export class FileReviewerLocationModel { + @Prop({ type: String, required: true }) + id: string; + + @Prop({ type: String, required: true }) + name: string; +} + +export const FileReviewerLocationDbSchema = SchemaFactory.createForClass( + FileReviewerLocationModel, +); + @Schema({ collection: "file-reviewer", versionKey: false, @@ -31,6 +44,9 @@ export class FileReviewerModel { @Prop({ type: Types.ObjectId, index: true }) branchId?: Types.ObjectId; + @Prop({ type: [FileReviewerLocationDbSchema], default: [] }) + locations?: FileReviewerLocationModel[]; + @Prop({ required: true }) password: string; @@ -49,6 +65,12 @@ export class FileReviewerModel { @Prop({ type: "string", required: false }) expertCode?: string; + @Prop({ type: String, required: false }) + ThirdPartyExpertiseClaim?: string; + + @Prop({ type: String, required: false }) + CarBodyExpertiseClaim?: string; + createdAt: Date; } From f6f4429ce7fb24fc6757a55a82c683d1be01b708 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 24 Aug 2026 16:33:18 +0330 Subject: [PATCH 2/2] YARA-985 --- .../expert-insurer.controller.ts | 2 +- src/expert-insurer/expert-insurer.service.ts | 357 +++++++++++++++++- .../helper/timeline-fa-labels.ts | 9 + .../request-management.service.ts | 81 ++-- 4 files changed, 402 insertions(+), 47 deletions(-) diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index 1cfcb83..e3fcc09 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -309,7 +309,7 @@ export class ExpertInsurerController { @ApiOperation({ summary: "Activity timeline for a case", description: - "Returns a chronological list of all history events for the blame and/or claim associated with the given publicId. Each event has: source, type, timestamp, actor, metadata.", + "Returns a chronological list of all history events for the blame and/or claim associated with the given publicId. Each event includes localized `faLabel`, enriched `actor`/`actorName`, the specific `performedBy` role, plus `performedByCategory` and `performedByFaLabel` for UI rendering.", }) async getFileTimeline(@CurrentUser() insurer, @Param("publicId") publicId: string) { return await this.expertInsurerService.getFileTimeline(insurer.clientKey, publicId); diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index 83a4b84..eff8d9d 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -29,6 +29,9 @@ import { ExpertDbService } from "src/users/entities/db-service/expert.db.service import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service"; import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service"; import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service"; +import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service"; +import { CallCenterAgentDbService } from "src/users/entities/db-service/call-center-agent.db.service"; +import { UserDbService } from "src/users/entities/db-service/user.db.service"; import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service"; import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema"; import { HashService } from "src/utils/hash/hash.service"; @@ -96,6 +99,9 @@ export class ExpertInsurerService { private readonly claimSignDbService: ClaimSignDbService, private readonly fileMakerDbService: FileMakerDbService, private readonly fileReviewerDbService: FileReviewerDbService, + private readonly registrarDbService: RegistrarDbService, + private readonly callCenterAgentDbService: CallCenterAgentDbService, + private readonly userDbService: UserDbService, ) {} /** @@ -2150,18 +2156,308 @@ export class ExpertInsurerService { }; } + private static readonly TIMELINE_PERFORMER_FA_LABELS: Record = { + user: "کاربر", + field_expert: "کارشناس میدانی", + damage_expert: "کارشناس خسارت", + file_maker: "پرونده‌ساز", + file_reviewer: "بازبین پرونده", + registrar: "ثبت‌کننده", + call_center: "اپراتور کال‌سنتر", + system: "سیستم", + expert: "کارشناس", + }; + /** - * Maps a raw history `actorType` string to either `"user"` or `"expert"`. - * - `"user"` → the action was taken by the car-owner / damaged party - * - `"expert"` → taken by any expert, system, or back-office actor - * - `null` → actorType was absent (unknown) + * Returns the most specific actor role recorded in history so the frontend can + * distinguish between user/system/back-office/expert actions. */ private static resolveTimelinePerformedBy( actorType: string | undefined, - ): "user" | "expert" | null { + ): string | null { + if (!actorType?.trim()) return null; + return actorType.trim().toLowerCase(); + } + + private static resolveTimelinePerformedByCategory( + actorType: string | undefined, + ): "user" | "expert" | "staff" | "system" | null { + const performedBy = ExpertInsurerService.resolveTimelinePerformedBy(actorType); + if (!performedBy) return null; + if (performedBy === "user") return "user"; + if (performedBy === "system") return "system"; + if ( + performedBy === "field_expert" || + performedBy === "damage_expert" || + performedBy === "expert" + ) { + return "expert"; + } + return "staff"; + } + + private static resolveTimelinePerformedByFaLabel( + actorType: string | undefined, + ): string | null { + const performedBy = ExpertInsurerService.resolveTimelinePerformedBy(actorType); + if (!performedBy) return null; + return ( + ExpertInsurerService.TIMELINE_PERFORMER_FA_LABELS[performedBy] ?? + performedBy + ); + } + + private static readonly TIMELINE_PLACEHOLDER_ACTOR_NAMES = new Set([ + "user", + "actor", + "expert", + "unknown expert", + "field_expert", + "damage_expert", + "file_maker", + "file_reviewer", + "registrar", + "call_center", + "system", + ]); + + private static getDisplayNameFromRecord(record: any): string | null { + if (!record) return null; + + const fullName = + typeof record.fullName === "string" ? record.fullName.trim() : ""; + if (fullName) return fullName; + + const firstName = + typeof record.firstName === "string" ? record.firstName.trim() : ""; + const lastName = + typeof record.lastName === "string" ? record.lastName.trim() : ""; + const combined = [firstName, lastName].filter(Boolean).join(" ").trim(); + return combined || null; + } + + private static isMeaningfulTimelineActorName(name: string | undefined): boolean { + const trimmed = typeof name === "string" ? name.trim() : ""; + if (!trimmed) return false; + return !ExpertInsurerService.TIMELINE_PLACEHOLDER_ACTOR_NAMES.has( + trimmed.toLowerCase(), + ); + } + + private static resolveBlamePartyName(sourceDoc: any, partyRole?: string): string | null { + const parties = Array.isArray(sourceDoc?.parties) ? sourceDoc.parties : []; + const normalizedPartyRole = + typeof partyRole === "string" ? partyRole.trim().toUpperCase() : ""; + + const matchedParty = normalizedPartyRole + ? parties.find( + (party: any) => + String(party?.role ?? "").trim().toUpperCase() === normalizedPartyRole, + ) + : null; + + const matchedName = matchedParty?.person?.fullName?.trim?.(); + if (matchedName) return matchedName; + + if (parties.length === 1) { + return parties[0]?.person?.fullName?.trim?.() || null; + } + + return null; + } + + private async resolveTimelineActualActorType( + actor: any, + cache: Map = new Map(), + ): Promise { + const rawActorType = ExpertInsurerService.resolveTimelinePerformedBy( + actor?.actorType, + ); + const actorId = actor?.actorId ? String(actor.actorId).trim() : ""; + + if (!actorId || !Types.ObjectId.isValid(actorId)) { + return rawActorType; + } + + if (cache.has(actorId)) { + return cache.get(actorId) ?? rawActorType ?? null; + } + + const objectId = new Types.ObjectId(actorId); + const [fileMaker, fileReviewer, registrar, callCenter, fieldExpert, damageExpert, expert, user] = await Promise.all([ + this.fileMakerDbService.findById(actorId), + this.fileReviewerDbService.findById(actorId), + this.registrarDbService.findById(actorId), + this.callCenterAgentDbService.findById(actorId), + this.fieldExpertDbService.findById(actorId), + this.damageExpertDbService.findById(actorId), + this.expertDbService.findOne({ _id: objectId }), + this.userDbService.findOne({ _id: objectId }), + ]); + + const resolvedActorType = fileMaker + ? "file_maker" + : fileReviewer + ? "file_reviewer" + : registrar + ? "registrar" + : callCenter + ? "call_center" + : fieldExpert + ? "field_expert" + : damageExpert + ? "damage_expert" + : expert + ? "expert" + : user + ? "user" + : rawActorType; + + cache.set(actorId, resolvedActorType ?? null); + return resolvedActorType ?? null; + } + + private async resolveTimelineActorNameFromId( + actorType: string, + actorId: string, + ): Promise { + if (!Types.ObjectId.isValid(actorId)) return null; + + const objectId = new Types.ObjectId(actorId); + + switch (actorType) { + case "user": { + const user = await this.userDbService.findOne({ _id: objectId }); + return ExpertInsurerService.getDisplayNameFromRecord(user); + } + case "field_expert": { + const [fieldExpert, expert] = await Promise.all([ + this.fieldExpertDbService.findById(actorId), + this.expertDbService.findOne({ _id: objectId }), + ]); + return ( + ExpertInsurerService.getDisplayNameFromRecord(fieldExpert) ?? + ExpertInsurerService.getDisplayNameFromRecord(expert) + ); + } + case "damage_expert": { + const [damageExpert, expert] = await Promise.all([ + this.damageExpertDbService.findById(actorId), + this.expertDbService.findOne({ _id: objectId }), + ]); + return ( + ExpertInsurerService.getDisplayNameFromRecord(damageExpert) ?? + ExpertInsurerService.getDisplayNameFromRecord(expert) + ); + } + case "file_maker": { + const fileMaker = await this.fileMakerDbService.findById(actorId); + return ExpertInsurerService.getDisplayNameFromRecord(fileMaker); + } + case "file_reviewer": { + const [fileReviewer, expert] = await Promise.all([ + this.fileReviewerDbService.findById(actorId), + this.expertDbService.findOne({ _id: objectId }), + ]); + return ( + ExpertInsurerService.getDisplayNameFromRecord(fileReviewer) ?? + ExpertInsurerService.getDisplayNameFromRecord(expert) + ); + } + case "registrar": { + const registrar = await this.registrarDbService.findById(actorId); + return ExpertInsurerService.getDisplayNameFromRecord(registrar); + } + case "call_center": { + const agent = await this.callCenterAgentDbService.findById(actorId); + return ExpertInsurerService.getDisplayNameFromRecord(agent); + } + case "expert": { + const expert = await this.expertDbService.findOne({ _id: objectId }); + return ExpertInsurerService.getDisplayNameFromRecord(expert); + } + default: + return null; + } + } + + private buildTimelineActor( + actor: any, + actorName: string | null, + actorType: string | null, + ): { actorId?: string; actorName?: string; actorType?: string } | null { + const actorId = actor?.actorId ? String(actor.actorId).trim() : ""; + + if (!actorType && !actorId && !actorName) return null; + + return { + ...(actorId ? { actorId } : {}), + ...(actorName ? { actorName } : {}), + ...(actorType ? { actorType } : {}), + }; + } + + private async resolveTimelineActorName( + actor: any, + actorType: string | null, + sourceDoc?: any, + event?: any, + cache: Map = new Map(), + ): Promise { + const explicitName = + typeof actor?.actorName === "string" ? actor.actorName.trim() : ""; + if (ExpertInsurerService.isMeaningfulTimelineActorName(explicitName)) { + return explicitName; + } + if (!actorType) return null; - if (actorType === "user") return "user"; - return "expert"; + + if (actorType === "system") { + return ExpertInsurerService.resolveTimelinePerformedByFaLabel(actorType); + } + + if (actorType === "user") { + const ownerName = sourceDoc?.owner?.fullName?.trim?.(); + if (ownerName) return ownerName; + + const blamePartyName = ExpertInsurerService.resolveBlamePartyName( + sourceDoc, + event?.metadata?.partyRole, + ); + if (blamePartyName) return blamePartyName; + } + + const actorId = actor?.actorId ? String(actor.actorId).trim() : ""; + if (actorId) { + const cacheKey = `${actorType}:${actorId}`; + if (cache.has(cacheKey)) { + return cache.get(cacheKey) ?? null; + } + + let resolvedName = await this.resolveTimelineActorNameFromId( + actorType, + actorId, + ); + + if (!resolvedName && actorType === "user") { + resolvedName = + sourceDoc?.owner?.fullName?.trim?.() ?? + ExpertInsurerService.resolveBlamePartyName( + sourceDoc, + event?.metadata?.partyRole, + ); + } + + if (!resolvedName) { + resolvedName = + ExpertInsurerService.resolveTimelinePerformedByFaLabel(actorType); + } + + cache.set(cacheKey, resolvedName ?? null); + return resolvedName ?? null; + } + + return ExpertInsurerService.resolveTimelinePerformedByFaLabel(actorType); } async getFileTimeline( @@ -2190,6 +2486,8 @@ export class ExpertInsurerService { } const events: object[] = []; + const actorNameCache = new Map(); + const actorTypeCache = new Map(); if (blame) { const blameDoc = await this.blameRequestDbService.findById( @@ -2197,13 +2495,29 @@ export class ExpertInsurerService { ); const blameHistory: any[] = (blameDoc as any)?.history ?? []; for (const ev of blameHistory) { + const performedBy = await this.resolveTimelineActualActorType( + ev.actor, + actorTypeCache, + ); + const actorName = await this.resolveTimelineActorName( + ev.actor, + performedBy, + blameDoc, + ev, + actorNameCache, + ); events.push({ source: "blame", type: ev.type, faLabel: getEventFaLabel(ev), timestamp: ev.timestamp, - actor: ev.actor ?? null, - performedBy: ExpertInsurerService.resolveTimelinePerformedBy(ev.actor?.actorType), + actor: this.buildTimelineActor(ev.actor, actorName, performedBy), + actorName, + performedBy, + performedByCategory: + ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy), + performedByFaLabel: + ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy), metadata: ev.metadata ?? null, }); } @@ -2213,17 +2527,34 @@ export class ExpertInsurerService { const claimId = String((claim as any)._id); const rows = (await this.claimCaseDbService.find( { _id: new Types.ObjectId(claimId) }, - { lean: true, select: "history createdAt" }, + { lean: true, select: "history createdAt owner" }, )) as Record[]; - const claimHistory: any[] = (rows[0] as any)?.history ?? []; + const claimDoc = rows[0] as any; + const claimHistory: any[] = claimDoc?.history ?? []; for (const ev of claimHistory) { + const performedBy = await this.resolveTimelineActualActorType( + ev.actor, + actorTypeCache, + ); + const actorName = await this.resolveTimelineActorName( + ev.actor, + performedBy, + claimDoc, + ev, + actorNameCache, + ); events.push({ source: "claim", type: ev.type, faLabel: getEventFaLabel(ev), timestamp: ev.timestamp, - actor: ev.actor ?? null, - performedBy: ExpertInsurerService.resolveTimelinePerformedBy(ev.actor?.actorType), + actor: this.buildTimelineActor(ev.actor, actorName, performedBy), + actorName, + performedBy, + performedByCategory: + ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy), + performedByFaLabel: + ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy), metadata: ev.metadata ?? null, }); } diff --git a/src/expert-insurer/helper/timeline-fa-labels.ts b/src/expert-insurer/helper/timeline-fa-labels.ts index 2176f53..d6767fa 100644 --- a/src/expert-insurer/helper/timeline-fa-labels.ts +++ b/src/expert-insurer/helper/timeline-fa-labels.ts @@ -19,9 +19,11 @@ export const EVENT_TYPE_FA_LABELS: Record = { // File creation FILE_CREATED_BY_REGISTRAR: "پرونده توسط ثبت‌کننده ایجاد شد", FILE_CREATED_BY_FIELD_EXPERT: "پرونده توسط کارشناس میدانی ایجاد شد", + FILE_CREATED_BY_CALL_CENTER: "پرونده توسط اپراتور کال‌سنتر ایجاد شد", // Expert link / OTP flow LINK_SENT: "لینک ارسال شد", + CALL_CENTER_LINK_SENT: "لینک توسط اپراتور کال‌سنتر ارسال شد", PARTY_OTP_SENT: "کد تأیید ارسال شد", PARTY_OTP_VERIFIED: "کد تأیید تأیید شد", PARTY_OTPS_VERIFIED: "کدهای تأیید هر دو طرف تأیید شدند", @@ -30,6 +32,7 @@ export const EVENT_TYPE_FA_LABELS: Record = { // Confession / accident type FIRST_BLAME_CONFESSION_SUBMITTED: "اقرار اولیه طرف اول ثبت شد", + CALL_CENTER_INQUIRY_COMPLETED: "استعلام کال‌سنتر تکمیل شد", CAR_BODY_ACCIDENT_TYPE_SUBMITTED: "نوع تصادف بدنه خودرو ثبت شد", AUTO_ADVANCED_TO_CAR_BODY_FORM: "پیشرفت خودکار به فرم بدنه خودرو", AUTO_CONFESSION_SKIPPED: "مرحله اقرار به‌صورت خودکار رد شد", @@ -41,6 +44,7 @@ export const EVENT_TYPE_FA_LABELS: Record = { SECOND_PARTY_INVITED: "طرف دوم دعوت شد", // Accident fields / expert in-person completion + ACCIDENT_FIELDS_SAVED: "اطلاعات تصادف ثبت شد", ACCIDENT_FIELDS_SAVED_ADVANCED_TO_SIGNATURES: "اطلاعات تصادف ذخیره و به مرحله امضاها پیشرفت شد", EXPERT_COMPLETED_CAR_BODY_FORM_V2: "کارشناس فرم بدنه خودرو را تکمیل کرد", @@ -72,12 +76,15 @@ export const EVENT_TYPE_FA_LABELS: Record = { V3_PARTY_VOICE_UPLOADED: "صدای طرف بارگذاری شد", V3_PARTY_LOCATION_SAVED: "موقعیت مکانی طرف ذخیره شد", V3_PARTY_DESCRIPTION_SAVED: "توضیحات طرف ذخیره شد", + V4_PARTY_DESCRIPTION_SAVED: "توضیحات طرف ذخیره شد", V3_CAR_BODY_ACCIDENT_TYPE_SAVED: "نوع تصادف بدنه خودرو ذخیره شد", // V5 blame steps V5_BLAME_ACCIDENT_VIDEO_UPLOADED: "ویدیوی تصادف (نسخه ۵) بارگذاری شد", V5_FILE_MAKER_APPROVED: "پرونده‌ساز پرونده را تأیید کرد", V5_FILE_MAKER_REJECTED: "پرونده‌ساز پرونده را رد کرد", + V5_FILE_MAKER_REJECTED_BLAME_RESET: + "پرونده‌ساز پرونده را رد کرد و پرونده به مرحله قبل بازگردانده شد", // --------------------------------------------------------------------------- // Claim-phase events (recorded on ClaimCase.history) @@ -102,6 +109,8 @@ export const EVENT_TYPE_FA_LABELS: Record = { // Documents & media DOCUMENT_UPLOADED: "مدرک بارگذاری شد", VIDEO_CAPTURE_UPLOADED: "تصویر ویدیویی بارگذاری شد", + PART_CAPTURED: "تصویر قطعه آسیب‌دیده بارگذاری شد", + ANGLE_CAPTURED: "تصویر زاویه خودرو بارگذاری شد", ALL_FACTORS_UPLOADED_PENDING_VALIDATION: "تمام فاکتورها بارگذاری شدند و در انتظار تأیید هستند", diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index f02f4a4..3c48ac1 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -135,6 +135,26 @@ export class RequestManagementService { throw new BadRequestException(`Step ${stepKey} is not a party-scoped step`); } + private resolveBlameHistoryActorType(actor: any): string { + switch (actor?.role) { + case RoleEnum.FILE_MAKER: + return "file_maker"; + case RoleEnum.FILE_REVIEWER: + return "file_reviewer"; + case RoleEnum.REGISTRAR: + return "registrar"; + case RoleEnum.CALL_CENTER: + return "call_center"; + case RoleEnum.DAMAGE_EXPERT: + return "damage_expert"; + case RoleEnum.EXPERT: + return "expert"; + case RoleEnum.FIELD_EXPERT: + default: + return "field_expert"; + } + } + /** * Reverse map: Fanavaran/Tejarat numeric letter code → Persian plate letter. * Tejarat inquiry stores Plk2 as a numeric code (e.g. 12 → "م", 5 → "د"). @@ -4992,7 +5012,7 @@ export class RequestManagementService { actor: { actorId: expertId, actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { creationMethod: dto.creationMethod, type: dto.type }, }); @@ -5093,7 +5113,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { sentTo, template: "yara-field-expert-link" }, }); @@ -5279,8 +5299,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: - expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { firstPartyVerified: true, @@ -5375,7 +5394,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { phoneNumber: phone }, } as any); @@ -5516,7 +5535,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { partyRole: role, phoneNumber: phone }, } as any); @@ -5558,8 +5577,7 @@ export class RequestManagementService { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: - actor?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { advancedTo: WorkflowStep.CAR_BODY_ACCIDENT_TYPE }, } as any); @@ -5593,10 +5611,8 @@ export class RequestManagementService { type: "AUTO_CONFESSION_SKIPPED", actor: { actorId: new Types.ObjectId(actor.sub), - actorName: - `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: - actor?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", + actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { reason: @@ -5621,9 +5637,9 @@ export class RequestManagementService { type: "SECOND_PARTY_OTP_VERIFIED_ADVANCED", actor: { actorId: new Types.ObjectId(actor.sub), - actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: - actor?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", + actorName: + `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { phoneNumber: phone, @@ -6383,7 +6399,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: {}, } as any); @@ -6589,7 +6605,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { guiltyPartyPhoneNumber: formData.guiltyPartyPhoneNumber }, } as any); @@ -6653,8 +6669,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: - expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { hasLocation: true, @@ -6714,7 +6729,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { videoId: firstParty.evidence.videoId }, } as any); @@ -6771,7 +6786,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: {}, } as any); @@ -7001,7 +7016,7 @@ export class RequestManagementService { actorId: new Types.ObjectId(String(expert.sub)), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { accidentWay: fields.accidentWay, @@ -9065,7 +9080,7 @@ export class RequestManagementService { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, timestamp: new Date(), metadata: { @@ -9904,7 +9919,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { partyRole, accepted: isAccept }, } as any); @@ -9971,7 +9986,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(String(expert.sub)), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { accidentWay: fields.accidentWay }, } as any); @@ -10089,11 +10104,11 @@ export class RequestManagementService { if (!Array.isArray(req.history)) req.history = []; req.history.push({ - type: "V3_BLAME_ACCIDENT_VIDEO_UPLOADED", + type: "V5_BLAME_ACCIDENT_VIDEO_UPLOADED", actor: { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, metadata: { videoId: videoId ?? null }, } as any); @@ -10118,7 +10133,7 @@ export class RequestManagementService { actorId: new Types.ObjectId(expert.sub), actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(expert), }, timestamp: new Date(), metadata: { @@ -10232,7 +10247,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { partyRole: role, voiceId: String((voiceDoc as any)._id) }, } as any); @@ -10275,7 +10290,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { partyRole: role, location: body }, } as any); @@ -10309,7 +10324,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { partyRole: role }, } as any); @@ -10369,7 +10384,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { partyRole: role, @@ -10425,7 +10440,7 @@ export class RequestManagementService { actor: { actorId: new Types.ObjectId(actor.sub), actorName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(), - actorType: "field_expert", + actorType: this.resolveBlameHistoryActorType(actor), }, metadata: { car: body.car, object: body.object }, } as any);