This commit is contained in:
SepehrYahyaee
2026-08-24 16:33:18 +03:30
parent 2df7a889d3
commit f6f4429ce7
4 changed files with 402 additions and 47 deletions

View File

@@ -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<string, string> = {
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<string, string | null> = new Map(),
): Promise<string | null> {
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<string | null> {
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<string, string | null> = new Map(),
): Promise<string | null> {
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<string, string | null>();
const actorTypeCache = new Map<string, string | null>();
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<string, unknown>[];
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,
});
}