Added extra fields for insurer

This commit is contained in:
SepehrYahyaee
2026-05-09 09:59:01 +03:30
parent f8193b6622
commit c1a54baaf0
2 changed files with 499 additions and 88 deletions

View File

@@ -98,6 +98,7 @@ import { JwtModule } from "@nestjs/jwt";
DamageImageDbService,
VideoCaptureDbService,
ClaimRequiredDocumentDbService,
ClaimSignDbService,
],
})
export class ClaimRequestManagementModule {}

View File

@@ -29,6 +29,28 @@ import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-ac
import { HashService } from "src/utils/hash/hash.service";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.db.service";
import { BlameVoiceDbService } from "src/request-management/entities/db-service/blame.voice.db.service";
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { buildFileLink } from "src/helpers/urlCreator";
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import {
getClaimCarAngleCaptureBlob,
getDamagedPartCaptureBlob,
hasDamagedPartCapture,
type ClaimCarAngleKey,
} from "src/helpers/claim-car-angle-media";
import {
catalogLikeKeyFromPart,
normalizeDamageSelectedParts,
type DamageSelectedPartV2,
} from "src/helpers/outer-damage-parts";
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
@Injectable()
export class ExpertInsurerService {
@@ -40,6 +62,11 @@ export class ExpertInsurerService {
private readonly claimCaseDbService: ClaimCaseDbService,
private readonly branchDbService: BranchDbService,
private readonly hashService: HashService,
private readonly blameVideoDbService: BlameVideoDbService,
private readonly blameVoiceDbService: BlameVoiceDbService,
private readonly videoCaptureDbService: VideoCaptureDbService,
private readonly clientDbService: ClientDbService,
private readonly claimSignDbService: ClaimSignDbService,
) {}
private getClientId(actorOrId: any): Types.ObjectId {
@@ -225,6 +252,445 @@ export class ExpertInsurerService {
};
}
private serializeBlameExpertDecisionForInsurer(
decision: unknown,
): Record<string, unknown> | undefined {
if (!decision || typeof decision !== "object") {
return undefined;
}
const d = decision as Record<string, unknown>;
const guilty = d.guiltyPartyId;
const decidedBy = d.decidedByExpertId;
const decidedAt = d.decidedAt;
return {
...d,
guiltyPartyId:
guilty != null && typeof (guilty as { toString?: () => string }).toString === "function"
? String(guilty)
: guilty,
decidedByExpertId:
decidedBy != null &&
typeof (decidedBy as { toString?: () => string }).toString === "function"
? String(decidedBy)
: decidedBy,
decidedAt:
decidedAt instanceof Date
? decidedAt.toISOString()
: decidedAt,
};
}
/** Same semantics as damage-expert claim detail `blameFileContext`. */
private blameFileContextForInsurer(blame: Record<string, unknown> | null): {
blameRequestType?: BlameRequestType;
carBodyFirstForm?: { car?: boolean; object?: boolean };
blameStatus?: string;
} {
if (!blame?.type) return {};
const blameRequestType = blame.type as BlameRequestType;
const out: {
blameRequestType?: BlameRequestType;
carBodyFirstForm?: { car?: boolean; object?: boolean };
blameStatus?: string;
} = { blameRequestType };
out.blameStatus = blame.blameStatus as string | undefined;
if (blameRequestType !== BlameRequestType.CAR_BODY) return out;
const parties = blame.parties;
if (!Array.isArray(parties) || parties.length === 0) return out;
const first =
(parties as any[]).find((p: any) => p?.role === PartyRole.FIRST) ?? (parties as any[])[0];
const cbf = first?.carBodyFirstForm;
delete out.blameStatus;
if (cbf && typeof cbf === "object") {
out.carBodyFirstForm = {
...(typeof cbf.car === "boolean" ? { car: cbf.car } : {}),
...(typeof cbf.object === "boolean" ? { object: cbf.object } : {}),
};
}
return out;
}
private async clientNamesByClientIds(
ids: Array<string | undefined | null>,
): Promise<Record<string, { persian: string; english: string }>> {
const unique = [
...new Set(
ids
.filter((x): x is string => !!x && Types.ObjectId.isValid(String(x)))
.map(String),
),
];
const out: Record<string, { persian: string; english: string }> = {};
await Promise.all(
unique.map(async (id) => {
const doc = await this.clientDbService.find({
_id: new Types.ObjectId(id),
});
if (doc?.clientName) out[id] = doc.clientName;
}),
);
return out;
}
private mapPersonForInsurer(
person: any,
clientNames: Record<string, { persian: string; english: string }>,
) {
if (!person || typeof person !== "object") return person;
const cid =
person.clientId?.toString?.() ?? (person.clientId != null ? String(person.clientId) : undefined);
return {
...person,
userId: person.userId?.toString?.() ?? undefined,
clientId: cid,
clientName: cid ? clientNames[cid] : undefined,
};
}
private mapPartyForInsurerFull(
party: any,
clientNames: Record<string, { persian: string; english: string }>,
) {
if (!party || typeof party !== "object") return party;
return {
...party,
person: this.mapPersonForInsurer(party.person, clientNames),
vehicle: this.sanitizeVehicleInquiry(party.vehicle),
};
}
/** Public URL for a stored relative path or pass-through if already absolute. */
private linkFromPathOrUrl(stored: string | undefined): string | undefined {
if (stored == null || stored === "") return undefined;
const s = String(stored);
if (/^https?:\/\//i.test(s)) return s;
return buildFileLink(s);
}
private enrichSignatureRecord(
sig: Record<string, unknown> | undefined,
): Record<string, unknown> | undefined {
if (!sig || typeof sig !== "object") return sig;
const fileUrl = sig.fileUrl != null ? String(sig.fileUrl) : undefined;
const link = this.linkFromPathOrUrl(fileUrl);
return { ...sig, link };
}
private enrichPartiesConfirmationSignatures(parties: any[]): void {
if (!Array.isArray(parties)) return;
for (const p of parties) {
const sig = p?.confirmation?.signature;
if (sig && typeof sig === "object") {
p.confirmation = {
...p.confirmation,
signature: this.enrichSignatureRecord(sig as Record<string, unknown>),
};
}
}
}
private insurerDamagedPartRowFromCapture(
sp: DamageSelectedPartV2,
index: number,
damagedPartsDataExpert: unknown,
selectedNormExpert: DamageSelectedPartV2[],
) {
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
const cap = getDamagedPartCaptureBlob(
damagedPartsDataExpert,
ck,
selectedNormExpert,
) as { url?: string; path?: string; fileName?: string } | undefined;
const url = cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined);
const link = url;
return {
index,
id: sp.id,
name: sp.name,
side: sp.side,
label_fa: sp.label_fa,
catalogKey: sp.catalogKey,
captured: hasDamagedPartCapture(damagedPartsDataExpert, ck, selectedNormExpert),
path: cap?.path,
fileName: cap?.fileName,
url,
link,
};
}
private sanitizeDamageForInsurerDetail(damage: unknown): unknown {
if (!damage || typeof damage !== "object") return damage;
const d = { ...(damage as Record<string, unknown>) };
delete d.selectedOuterParts;
delete d.selectedPartIds;
return d;
}
private stripCarTypeFromPartyVehicles(parties: any[]): any[] {
if (!Array.isArray(parties)) return parties;
return parties.map((p) => {
const v = p?.vehicle;
if (!v || typeof v !== "object") return p;
const { carType: _omit, ...rest } = v as Record<string, unknown>;
const nextV = Object.keys(rest).length ? rest : undefined;
return { ...p, vehicle: nextV };
});
}
private async claimSignLinkFromId(signDetailId: unknown): Promise<string | undefined> {
if (signDetailId == null || signDetailId === "") return undefined;
const id = String(signDetailId);
if (!Types.ObjectId.isValid(id)) return undefined;
const doc = await this.claimSignDbService.findOne({
_id: new Types.ObjectId(id),
});
const path = (doc as any)?.path;
return path ? buildFileLink(String(path)) : undefined;
}
private async enrichClaimEvaluationForInsurer(
evaluation: Record<string, unknown> | undefined,
): Promise<Record<string, unknown> | undefined> {
if (!evaluation || typeof evaluation !== "object") return evaluation;
const ev = JSON.parse(JSON.stringify(evaluation)) as Record<string, unknown>;
const enrichReply = async (reply: any) => {
if (!reply || typeof reply !== "object") return;
const uc = reply.userComment;
if (uc?.signDetailId != null) {
const signLink = await this.claimSignLinkFromId(uc.signDetailId);
reply.userComment = { ...uc, signLink };
}
};
await enrichReply(ev.damageExpertReply);
await enrichReply(ev.damageExpertReplyFinal);
const enrichApproval = async (key: string) => {
const o = ev[key] as Record<string, unknown> | undefined;
if (o?.signDetailId != null) {
const signLink = await this.claimSignLinkFromId(o.signDetailId);
ev[key] = { ...o, signLink };
}
};
await enrichApproval("ownerInsurerApproval");
await enrichApproval("ownerPricedPartsApproval");
return ev;
}
private async enrichBlamePartyEvidenceUrls(parties: any[]): Promise<void> {
if (!Array.isArray(parties)) return;
for (const party of parties) {
if (!party?.evidence) continue;
const evidence = party.evidence as Record<string, unknown>;
if (evidence.videoId) {
const videoDoc = await this.blameVideoDbService.findById(String(evidence.videoId));
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
}
if (evidence.voices && Array.isArray(evidence.voices)) {
const voiceUrls: string[] = [];
for (const voiceId of evidence.voices) {
const voiceDoc = await this.blameVoiceDbService.findById(String(voiceId));
if (voiceDoc?.path) voiceUrls.push(buildFileLink(voiceDoc.path));
}
evidence.voiceUrls = voiceUrls;
}
}
}
private async buildInsurerBlameDetail(
blameLean: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const doc: Record<string, unknown> = { ...blameLean };
const parties = (Array.isArray(doc.parties) ? doc.parties : []) as any[];
await this.enrichBlamePartyEvidenceUrls(parties);
doc.parties = enrichBlamePartiesForAgreementView(doc);
const mergedParties = doc.parties as any[];
const clientIds = mergedParties.map((p) => p?.person?.clientId?.toString?.()).filter(Boolean);
const clientNames = await this.clientNamesByClientIds(clientIds);
doc.parties = mergedParties.map((p) => this.mapPartyForInsurerFull(p, clientNames));
this.enrichPartiesConfirmationSignatures(doc.parties as any[]);
if (doc.expert && typeof doc.expert === "object") {
const ex = { ...(doc.expert as Record<string, unknown>) };
const serialized = this.serializeBlameExpertDecisionForInsurer(ex.decision);
if (serialized !== undefined) ex.decision = serialized;
doc.expert = ex;
}
const createdAt = doc.createdAt
? new Date(doc.createdAt as string | number | Date)
: new Date();
const updatedAt = doc.updatedAt
? new Date(doc.updatedAt as string | number | Date)
: new Date();
const [cd, ct] = toJalaliDateAndTime(createdAt);
const [ud, ut] = toJalaliDateAndTime(updatedAt);
doc.createdAtFormatted = `${cd} ${ct}`;
doc.updatedAtFormatted = `${ud} ${ut}`;
delete doc.history;
return doc;
}
private async buildInsurerClaimDetail(
claim: Record<string, unknown>,
blameForContext: Record<string, unknown> | null,
): Promise<Record<string, unknown>> {
const requiredDocs = claim.requiredDocuments as any;
const requiredDocumentsStatus: Record<string, any> = {};
if (requiredDocs) {
const keys =
requiredDocs instanceof Map
? Array.from(requiredDocs.keys())
: Object.keys(requiredDocs);
for (const k of keys as string[]) {
const row = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
requiredDocumentsStatus[k] = {
uploaded: !!row?.uploaded,
fileId: row?.fileId?.toString?.(),
fileName: row?.fileName,
fileUrl: row?.filePath ? buildFileLink(row.filePath) : undefined,
};
}
}
const carAnglesData = (claim.media as any)?.carAngles;
const damagedPartsDataForAngles = (claim.media as any)?.damagedParts;
const carAngles: Record<string, { captured: boolean; url?: string }> = {};
for (const k of ["front", "back", "left", "right"] as ClaimCarAngleKey[]) {
const cap = getClaimCarAngleCaptureBlob(
carAnglesData,
damagedPartsDataForAngles,
k,
) as { url?: string; path?: string } | undefined;
carAngles[k] = {
captured: !!cap,
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
};
}
const carTypeExpert = (claim.vehicle as any)?.carType as ClaimVehicleTypeV2 | undefined;
const selectedNormExpert = normalizeDamageSelectedParts(
(claim.damage as any)?.selectedParts,
carTypeExpert,
(claim.damage as any)?.selectedOuterParts,
);
const damagedPartsDataExpert = (claim.media as any)?.damagedParts;
const damagedParts = selectedNormExpert.map((sp, index) =>
this.insurerDamagedPartRowFromCapture(
sp,
index,
damagedPartsDataExpert,
selectedNormExpert,
),
);
const selectedPartsNormalized = selectedNormExpert.map((sp, index) =>
this.insurerDamagedPartRowFromCapture(
sp,
index,
damagedPartsDataExpert,
selectedNormExpert,
),
);
const videoCaptureIdRaw = (claim.media as any)?.videoCaptureId;
let videoCapture:
| { id: string; fileName?: string; path?: string; url?: string }
| undefined;
if (videoCaptureIdRaw) {
const vc = (await this.videoCaptureDbService.findById(
String(videoCaptureIdRaw),
)) as any;
if (vc) {
videoCapture = {
id: String(vc._id ?? videoCaptureIdRaw),
fileName: vc.fileName,
path: vc.path,
url: vc.path ? buildFileLink(vc.path) : undefined,
};
}
}
const evaluation = claim.evaluation as Record<string, unknown> | undefined;
const owner = claim.owner as any;
const ownerOut = owner
? {
...owner,
userId: owner.userId?.toString?.(),
userClientKey: owner.userClientKey?.toString?.(),
}
: undefined;
let snapshotOut = claim.snapshot as any;
if (snapshotOut && Array.isArray(snapshotOut.parties)) {
const snapIds = snapshotOut.parties
.map((p: any) => p?.person?.clientId?.toString?.())
.filter(Boolean);
const snapNames = await this.clientNamesByClientIds(snapIds);
const snapParties = this.stripCarTypeFromPartyVehicles(
snapshotOut.parties.map((p: any) =>
this.mapPartyForInsurerFull(p, snapNames),
),
);
this.enrichPartiesConfirmationSignatures(snapParties);
snapshotOut = {
...snapshotOut,
parties: snapParties,
};
}
const evaluationEnriched = await this.enrichClaimEvaluationForInsurer(evaluation);
const vehicleSanitized = claim.vehicle
? this.sanitizeVehicleInquiry(claim.vehicle as any)
: undefined;
const vehicleOut = vehicleSanitized
? (() => {
const { carType: _omit, ...rest } = vehicleSanitized as Record<string, unknown>;
return rest;
})()
: undefined;
return {
claimRequestId: (claim._id as any)?.toString?.(),
publicId: claim.publicId,
status: claim.status,
claimStatus: claim.claimStatus,
blameDocumentResendPending: claim.blameDocumentResendPending,
workflow: claim.workflow,
owner: ownerOut,
vehicle: vehicleOut,
blameRequestId: (claim as any).blameRequestId?.toString?.(),
blameRequestNo: claim.blameRequestNo,
blameFileContext: blameForContext
? this.blameFileContextForInsurer(blameForContext)
: undefined,
money: claim.money,
damage: this.sanitizeDamageForInsurerDetail(claim.damage),
media: claim.media,
requiredDocuments:
Object.keys(requiredDocumentsStatus).length > 0
? requiredDocumentsStatus
: undefined,
carAngles,
damagedParts,
selectedPartsNormalized,
videoCapture,
evaluation: evaluationEnriched,
userRating: claim.userRating,
insurerRating: (evaluationEnriched as any)?.rating,
snapshot: snapshotOut,
createdAt: claim.createdAt,
updatedAt: claim.updatedAt,
};
}
private getCombinedFileScore(file: any): number | null {
const insurerRating = file?.rating;
const userRating = file?.userRating;
@@ -655,6 +1121,7 @@ export class ExpertInsurerService {
}
const id = this.getClientId(insurerId);
const idStr = String(id);
const [blameFiles, claimFiles] = await Promise.all([
this.getClientBlameFiles(id),
this.getClientClaimFiles(id),
@@ -667,12 +1134,31 @@ export class ExpertInsurerService {
throw new NotFoundException("File not found for this publicId");
}
const parties = Array.isArray((blame as any)?.parties) ? (blame as any).parties : [];
const firstParty = parties.find((p: any) => p?.role === "FIRST");
const secondParty = parties.find((p: any) => p?.role === "SECOND");
const claimSnapshotParties = Array.isArray((claim as any)?.snapshot?.parties)
? (claim as any).snapshot.parties
: undefined;
let blameFull: Record<string, unknown> | null = null;
if (blame) {
blameFull = (await this.blameRequestDbService.findByIdWithoutHistory(
String((blame as any)._id),
)) as Record<string, unknown> | null;
}
let claimFull: Record<string, unknown> | null = null;
if (claim) {
const rows = (await this.claimCaseDbService.find(
{ _id: new Types.ObjectId(String((claim as any)._id)) },
{ lean: true },
)) as Record<string, unknown>[];
claimFull = rows[0] ?? null;
}
let blameForClaimContext: Record<string, unknown> | null = blameFull;
if (!blameForClaimContext && claimFull?.blameRequestId) {
const linked = (await this.blameRequestDbService.findByIdWithoutHistory(
String(claimFull.blameRequestId),
)) as Record<string, unknown> | null;
if (linked && blameCaseTouchesClient(linked, idStr)) {
blameForClaimContext = linked;
}
}
const overview = {
publicId,
@@ -681,97 +1167,21 @@ export class ExpertInsurerService {
(claim as any)?.requestNo ||
(blame as any)?.requestNumber ||
(claim as any)?.requestNumber,
type: (blame as any)?.type,
type: (blame as any)?.type ?? (blameFull as any)?.type,
hasClaim: !!claim,
blameStatus: (blame as any)?.status,
blameStatus: (blame as any)?.status ?? (blameFull as any)?.status,
claimStatus: (claim as any)?.status,
claimReviewStatus: (claim as any)?.claimStatus,
createdAt: (blame as any)?.createdAt ?? (claim as any)?.createdAt,
updatedAt: (claim as any)?.updatedAt ?? (blame as any)?.updatedAt,
};
const blameDetails = blame
? {
requestId:
(blame as any)?._id?.toString?.() || String((blame as any)?._id || ""),
requestNo:
(blame as any).requestNo || String((blame as any).requestNumber || ""),
status: (blame as any).status,
blameStatus: (blame as any).blameStatus,
workflow: (blame as any).workflow,
parties: {
first: firstParty
? {
fullName: firstParty?.person?.fullName,
phoneNumber: firstParty?.person?.phoneNumber,
role: firstParty?.role,
vehicle: this.sanitizeVehicleInquiry(firstParty?.vehicle),
}
: undefined,
second: secondParty
? {
fullName: secondParty?.person?.fullName,
phoneNumber: secondParty?.person?.phoneNumber,
role: secondParty?.role,
vehicle: this.sanitizeVehicleInquiry(secondParty?.vehicle),
}
: undefined,
},
expert: (blame as any).expert
? {
assignedExpertId: (blame as any).expert?.assignedExpertId,
decision: (blame as any).expert?.decision,
resend: (blame as any).expert?.resend,
rating: (blame as any).expert?.rating,
}
: undefined,
createdAt: (blame as any).createdAt,
updatedAt: (blame as any).updatedAt,
}
const blameDetails = blameFull
? await this.buildInsurerBlameDetail(blameFull)
: undefined;
// Keep claim section claim-specific to avoid duplicating shared data already in blame/overview.
const claimDetails = claim
? {
requestId:
(claim as any)?._id?.toString?.() || String((claim as any)?._id || ""),
requestNo:
(claim as any).requestNo || String((claim as any).requestNumber || ""),
status: (claim as any).status,
claimStatus: (claim as any).claimStatus,
owner: (claim as any).owner,
vehicle: (claim as any).vehicle,
money: (claim as any).money,
damage: (claim as any).damage,
media: (claim as any).media,
requiredDocuments: (claim as any).requiredDocuments,
snapshot: (claim as any).snapshot
? {
...(claim as any).snapshot,
parties: claimSnapshotParties?.map((p: any) =>
this.sanitizePartyForInsurerView(p),
),
}
: undefined,
workflow: {
currentStep: (claim as any).currentStep,
nextStep: (claim as any).nextStep,
},
evaluation: {
damageExpertReply: (claim as any).damageExpertReply,
damageExpertReplyFinal: (claim as any).damageExpertReplyFinal,
damageExpertResend: (claim as any).damageExpertResend,
objection: (claim as any).objection,
priceDrop: (claim as any).priceDrop,
visitLocation: (claim as any).visitLocation,
factors: (claim as any).evaluation?.factors,
},
userResendDocuments: (claim as any).userResendDocuments,
userRating: (claim as any).userRating,
insurerRating: (claim as any).rating,
createdAt: (claim as any).createdAt,
updatedAt: (claim as any).updatedAt,
}
const claimDetails = claimFull
? await this.buildInsurerClaimDetail(claimFull, blameForClaimContext)
: undefined;
return {