1
0
forked from Yara724/api

Rework the reports and insurer part

This commit is contained in:
2026-04-11 00:12:10 +03:30
parent 7ef057dc1f
commit 9afb079786
10 changed files with 564 additions and 1052 deletions

View File

@@ -1,163 +1,159 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
Logger,
NotFoundException,
} from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { ClaimRequiredDocumentDbService } from "src/claim-request-management/entites/db-service/claim-required-document.db.service";
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service";
import { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema";
import { Types } from "mongoose";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
import { buildFileLink } from "src/helpers/urlCreator";
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
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 { UserSignDbService } from "src/request-management/entities/db-service/sign.db.service";
import {
FileRating,
RequestManagementModel,
} from "src/request-management/entities/schema/request-management.schema";
blameCaseTouchesClient,
claimCaseTouchesClient,
} from "src/helpers/tenant-scope";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
@Injectable()
export class ExpertInsurerService {
private readonly logger = new Logger(ExpertInsurerService.name);
constructor(
private readonly client: ClientDbService,
private readonly expertDbService: ExpertDbService,
private readonly damageExpertDbService: DamageExpertDbService,
@InjectModel(ClaimRequestManagementModel.name)
private readonly claimRequestManagementModel: Model<ClaimRequestManagementModel>,
@InjectModel(RequestManagementModel.name)
private readonly requestManagementModel: Model<RequestManagementModel>,
private readonly blameVoiceDbService: BlameVoiceDbService,
private readonly blameVideoDbService: BlameVideoDbService,
private readonly blameDocumentDbService: BlameDocumentDbService,
private readonly userSignDbService: UserSignDbService,
private readonly claimVideoCaptureDbService: VideoCaptureDbService,
private readonly blameRequestDbService: BlameRequestDbService,
private readonly claimCaseDbService: ClaimCaseDbService,
private readonly branchDbService: BranchDbService,
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
private readonly damageImageDbService: DamageImageDbService,
) {}
async retrieveAllExpertsOfClient(
actor,
currentPage: number,
countPerPage: number,
) {
const { clientKey } = actor;
if (!clientKey) return;
private getClientId(actorOrId: any): Types.ObjectId {
const raw = typeof actorOrId === "string" ? actorOrId : actorOrId?.clientKey;
if (!raw || !Types.ObjectId.isValid(raw)) {
throw new BadRequestException("Client key is required");
}
return new Types.ObjectId(raw);
}
const clientObjectId = new Types.ObjectId(clientKey);
private parseObjectId(value: string, label: string): Types.ObjectId {
if (!Types.ObjectId.isValid(value)) {
throw new BadRequestException(`Invalid ${label}`);
}
return new Types.ObjectId(value);
}
const [experts, damageExperts] = await Promise.all([
this.expertDbService.findAll({ clientKey: clientKey }),
private normalizeClaimCase(claim: any): any {
return {
...claim,
requestNumber: claim.requestNo,
userClientKey: claim.owner?.userClientKey,
fullName: claim.owner?.fullName,
carDetail: claim.vehicle,
carPlate: claim.vehicle?.plate,
claimStatus: claim.status,
rating: claim.evaluation?.rating,
userRating: claim.evaluation?.userRating,
damageExpertReply: claim.evaluation?.damageExpertReply,
damageExpertReplyFinal: claim.evaluation?.damageExpertReplyFinal,
damageExpertResend: claim.evaluation?.damageExpertResend,
objection: claim.evaluation?.objection,
userResendDocuments: claim.evaluation?.userResendDocuments,
priceDrop: claim.evaluation?.priceDrop,
visitLocation: claim.evaluation?.visitLocation,
currentStep: claim.workflow?.currentStep,
nextStep: claim.workflow?.nextStep,
};
}
private normalizeBlameCase(blame: any): any {
const firstParty = blame?.parties?.find((p) => p?.role === "FIRST");
const secondParty = blame?.parties?.find((p) => p?.role === "SECOND");
return {
...blame,
requestNumber: blame.requestNo,
rating: blame?.expert?.rating,
actorLocked: { actorId: blame?.expert?.assignedExpertId },
firstPartyDetails: {
firstPartyClient: { clientId: firstParty?.person?.clientId },
},
secondPartyDetails: {
secondPartyClient: { clientId: secondParty?.person?.clientId },
},
};
}
private getCombinedFileScore(file: any): number | null {
const insurerRating = file?.rating;
const userRating = file?.userRating;
const insurerValues = insurerRating
? Object.values(insurerRating).filter(
(v): v is number => typeof v === "number" && !isNaN(v),
)
: [];
const userValues = userRating
? [userRating.progressSpeed, userRating.registrationEase, userRating.overallEvaluation].filter(
(v) => typeof v === "number" && !isNaN(v),
)
: [];
const insurerAvg = insurerValues.length
? insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length
: null;
const userAvg = userValues.length
? userValues.reduce((a, b) => a + b, 0) / userValues.length
: null;
const scores = [insurerAvg, userAvg].filter((v): v is number => typeof v === "number");
if (!scores.length) return null;
return parseFloat((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2));
}
private async getClientBlameFiles(clientObjectId: Types.ObjectId): Promise<any[]> {
const all = (await this.blameRequestDbService.find({}, { lean: true })) as any[];
const idStr = String(clientObjectId);
return all
.filter((f) =>
(f?.parties || []).some((p) => String(p?.person?.clientId || "") === idStr),
)
.filter(
(f) =>
f?.status !== CaseStatus.OPEN &&
f?.status !== CaseStatus.WAITING_FOR_SECOND_PARTY,
)
.map((f) => this.normalizeBlameCase(f));
}
private async getClientClaimFiles(clientObjectId: Types.ObjectId): Promise<any[]> {
const all = (await this.claimCaseDbService.find({}, { lean: true })) as any[];
const idStr = String(clientObjectId);
return all
.filter((f) => String(f?.owner?.userClientKey || "") === idStr)
.map((f) => this.normalizeClaimCase(f));
}
async retrieveAllExpertsOfClient(actor, currentPage: number, countPerPage: number) {
const clientObjectId = this.getClientId(actor);
const [experts, damageExperts, blameFiles, claimFiles] = await Promise.all([
this.expertDbService.findAll({ clientKey: String(clientObjectId) }),
this.damageExpertDbService.findAll({ clientKey: clientObjectId }),
this.getClientBlameFiles(clientObjectId),
this.getClientClaimFiles(clientObjectId),
]);
const allExpertsRaw = [...experts, ...damageExperts];
const expertIds = allExpertsRaw.map((expert) => expert._id.toString());
if (expertIds.length === 0)
return { total: 0, page: currentPage, countPerPage, experts: [] };
const expertObjectIds = expertIds.map((id) => new Types.ObjectId(id));
const blameFileQuery = {
$and: [
{
$or: [
{ "firstPartyDetails.firstPartyClient.clientId": clientObjectId },
{ "secondPartyDetails.secondPartyClient.clientId": clientObjectId },
],
},
{ "actorLocked.actorId": { $in: expertObjectIds } },
],
};
const claimFileQuery = {
$and: [
{ userClientKey: clientObjectId },
{ "damageExpertReply.actorDetail.actorId": { $in: expertIds } },
],
};
const [blameFiles, claimFiles] = await Promise.all([
this.requestManagementModel.find(blameFileQuery).lean(),
this.claimRequestManagementModel.find(claimFileQuery).lean(),
]);
const expertTotalRatingsMap: Record<string, number[]> = {};
const expertRatingsByCategoryMap: Record<
string,
Record<string, number[]>
> = {};
const expertRatingsByCategoryMap: Record<string, Record<string, number[]>> = {};
/**
* Converts insurer FileRating and optional userRating into a single
* combined score for a file.
*/
const getCombinedFileScore = (file: any): number | null => {
const insurerRating = file?.rating;
const userRating = file?.userRating;
const insurerValues = insurerRating
? Object.values(insurerRating).filter(
(val): val is number => typeof val === "number" && !isNaN(val),
)
: [];
const insurerAvg =
insurerValues.length > 0
? insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length
: null;
const userValues = userRating
? [
userRating.progressSpeed,
userRating.registrationEase,
userRating.overallEvaluation,
].filter((val) => typeof val === "number" && !isNaN(val))
: [];
const userAvg =
userValues.length > 0
? userValues.reduce((a, b) => a + b, 0) / userValues.length
: null;
const scores = [insurerAvg, userAvg].filter(
(v): v is number => typeof v === "number" && !isNaN(v),
);
if (scores.length === 0) return null;
return parseFloat(
(scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2),
);
};
const processRatings = (expertId: string, file: any) => {
const processRatings = (expertId: string | undefined, file: any) => {
if (!expertId) return;
const rating = file?.rating;
const combinedScore = getCombinedFileScore(file);
// Aggregate overall combined score for this expert
const combinedScore = this.getCombinedFileScore(file);
if (combinedScore !== null) {
if (!expertTotalRatingsMap[expertId])
expertTotalRatingsMap[expertId] = [];
if (!expertTotalRatingsMap[expertId]) expertTotalRatingsMap[expertId] = [];
expertTotalRatingsMap[expertId].push(combinedScore);
}
// Keep backward-compatible per-category insurer ratings
if (!rating || typeof rating !== "object") return;
if (!expertRatingsByCategoryMap[expertId]) {
expertRatingsByCategoryMap[expertId] = {};
}
if (!expertRatingsByCategoryMap[expertId]) expertRatingsByCategoryMap[expertId] = {};
for (const [category, value] of Object.entries(rating)) {
if (typeof value === "number" && !isNaN(value)) {
if (!expertRatingsByCategoryMap[expertId][category]) {
@@ -169,37 +165,29 @@ export class ExpertInsurerService {
};
for (const file of blameFiles) {
const expertId = file?.actorLocked?.actorId?.toString();
processRatings(expertId, file);
processRatings(file?.actorLocked?.actorId?.toString?.(), file);
}
for (const file of claimFiles) {
const expertId = file?.damageExpertReply?.actorDetail?.actorId;
processRatings(expertId, file);
processRatings(file?.damageExpertReply?.actorDetail?.actorId, file);
}
const allExperts = allExpertsRaw.map((expert) => {
const expertIdStr = expert._id.toString();
const totalRatings = expertTotalRatingsMap[expertIdStr] || [];
const overallAverageRating = totalRatings.length
? parseFloat(
(
totalRatings.reduce((a, b) => a + b, 0) / totalRatings.length
).toFixed(2),
(totalRatings.reduce((a, b) => a + b, 0) / totalRatings.length).toFixed(2),
)
: null;
const averageRatingsByCategory: Record<string, number> = {};
const ratingsByCat = expertRatingsByCategoryMap[expertIdStr];
if (ratingsByCat) {
for (const [category, values] of Object.entries(ratingsByCat)) {
const sum = values.reduce((a, b) => a + b, 0);
const average = parseFloat((sum / values.length).toFixed(2));
averageRatingsByCategory[category] = average;
averageRatingsByCategory[category] = parseFloat(
(values.reduce((a, b) => a + b, 0) / values.length).toFixed(2),
);
}
}
return {
_id: expert._id,
fullName: `${expert.firstName} ${expert.lastName}`,
@@ -212,14 +200,14 @@ export class ExpertInsurerService {
};
});
const start = (currentPage - 1) * countPerPage;
const paginated = allExperts.slice(start, start + countPerPage);
const page = Number(currentPage) > 0 ? Number(currentPage) : 1;
const perPage = Number(countPerPage) > 0 ? Number(countPerPage) : 20;
const start = (page - 1) * perPage;
return {
total: allExperts.length,
page: currentPage,
countPerPage,
experts: paginated,
page,
countPerPage: perPage,
experts: allExperts.slice(start, start + perPage),
};
}
@@ -245,510 +233,125 @@ export class ExpertInsurerService {
* combined insurer + user ratings.
*/
async getTopFilesForClient(insurerId: string): Promise<any[]> {
const id = new Types.ObjectId(insurerId);
const claimFiles = await this.claimRequestManagementModel
.find(
{
userClientKey: id,
},
{
requestNumber: 1,
userClientKey: 1,
userId: 1,
fullName: 1,
carDetail: 1,
carPlate: 1,
claimStatus: 1,
rating: 1,
userRating: 1,
damageExpertReply: 1,
damageExpertReplyFinal: 1,
createdAt: 1,
},
)
.lean();
const claimFiles = await this.getClientClaimFiles(this.getClientId(insurerId));
const scored = claimFiles
.map((file) => {
const insurerRating = file?.rating;
const userRating = file?.userRating;
const insurerValues = insurerRating
? Object.values(insurerRating).filter(
(val): val is number => typeof val === "number" && !isNaN(val),
)
: [];
const insurerAvg =
insurerValues.length > 0
? insurerValues.reduce((a, b) => a + b, 0) / insurerValues.length
: null;
const userValues = userRating
? [
userRating.progressSpeed,
userRating.registrationEase,
userRating.overallEvaluation,
].filter((val) => typeof val === "number" && !isNaN(val))
: [];
const userAvg =
userValues.length > 0
? userValues.reduce((a, b) => a + b, 0) / userValues.length
: null;
const scores = [insurerAvg, userAvg].filter(
(v): v is number => typeof v === "number" && !isNaN(v),
);
if (scores.length === 0) return null;
const combinedScore =
scores.reduce((a, b) => a + b, 0) / scores.length;
return {
...file,
combinedScore: parseFloat(combinedScore.toFixed(2)),
};
const combinedScore = this.getCombinedFileScore(file);
if (combinedScore === null) return null;
return { ...file, combinedScore };
})
.filter((f) => f !== null);
const sorted = scored.sort((a, b) => b.combinedScore - a.combinedScore);
return sorted.slice(0, 10);
return scored.sort((a, b) => b.combinedScore - a.combinedScore).slice(0, 10);
}
async getAllFilesByExpertAndRole(expertId: string, role: "claim" | "blame") {
const expertObjectId = new Types.ObjectId(expertId);
const model =
role === "claim"
? this.claimRequestManagementModel
: this.requestManagementModel;
const expertCollection = role === "claim" ? "damage-expert" : "expert";
const results = await model.aggregate([
{ $match: { "actorLocked.actorId": expertObjectId } },
{
$lookup: {
from: expertCollection,
localField: "actorLocked.actorId",
foreignField: "_id",
as: "expertInfo",
},
},
{ $unwind: { path: "$expertInfo", preserveNullAndEmptyArrays: true } },
{
$addFields: {
averageRating: {
$cond: [
{ $ne: ["$rating", null] },
{
$avg: [
"$rating.collisionMethodAccuracy",
"$rating.evaluationTimeliness",
"$rating.accidentCauseAccuracy",
"$rating.guiltyVehicleIdentification",
],
},
null,
],
},
},
},
{
$project: {
requestNumber: 1,
userClientKey: 1,
userId: 1,
fullName: 1,
carDetail: 1,
claimStatus: 1,
steps: 1,
currentStep: 1,
rating: 1,
averageRating: 1,
imageRequired: 1,
expertInfo: {
_id: 1,
fullName: {
$concat: ["$expertInfo.firstName", " ", "$expertInfo.lastName"],
},
requestStats: 1,
userType: 1,
createdAt: 1,
},
},
},
private async assertExpertBelongsToInsurer(
expertObjectId: Types.ObjectId,
insurerClientKey: string,
) {
const ck = String(insurerClientKey);
const clientOid = new Types.ObjectId(ck);
const [experts, damageExperts] = await Promise.all([
this.expertDbService.findAll({ clientKey: ck }),
this.damageExpertDbService.findAll({ clientKey: clientOid }),
]);
// Process imageRequired for claim files
if (role === "claim") {
return await Promise.all(
results.map((file) => this.processImageRequired(file)),
const id = String(expertObjectId);
const ok = [...experts, ...damageExperts].some((e) => String(e._id) === id);
if (!ok) {
throw new ForbiddenException(
"This expert is not registered under your insurance company.",
);
}
}
return results;
async getAllFilesByExpertAndRole(
expertId: string,
role: "claim" | "blame",
insurerClientKey: string,
) {
const expertObjectId = this.parseObjectId(expertId, "expert id");
await this.assertExpertBelongsToInsurer(expertObjectId, insurerClientKey);
const clientKeyStr = String(this.getClientId(insurerClientKey));
if (role === "claim") {
const claims = (await this.claimCaseDbService.find({}, { lean: true })) as any[];
return claims
.filter(
(c) =>
claimCaseTouchesClient(c, clientKeyStr) &&
c?.evaluation?.damageExpertReply?.actorDetail?.actorId ===
String(expertObjectId),
)
.map((c) => this.normalizeClaimCase(c));
}
const blames = (await this.blameRequestDbService.find({}, { lean: true })) as any[];
return blames
.filter(
(b) =>
blameCaseTouchesClient(b, clientKeyStr) &&
String(b?.expert?.assignedExpertId ?? "") === String(expertObjectId),
)
.map((b) => this.normalizeBlameCase(b));
}
async rateExpertOnFile(
requestId: string,
rating: FileRating,
role: "claim" | "blame",
insurerClientKey: string,
) {
const _id = new Types.ObjectId(requestId);
// Validate each rating factor
const fields = Object.entries(rating);
for (const [key, value] of fields) {
const _id = this.parseObjectId(requestId, "request id");
const clientKeyStr = String(this.getClientId(insurerClientKey));
for (const [key, value] of Object.entries(rating || {})) {
if (typeof value !== "number" || value < 0 || value > 5) {
throw new BadRequestException(
`${key} must be a number between 0 and 5`,
);
throw new BadRequestException(`${key} must be a number between 0 and 5`);
}
}
if (role === "claim") {
const updated = await this.claimRequestManagementModel.findByIdAndUpdate(
_id,
{ $set: { rating } },
{ new: true },
);
if (!updated) {
throw new NotFoundException("Claim file not found");
const existing = await this.claimCaseDbService.findById(_id);
if (!existing) throw new NotFoundException("Claim file not found");
if (!claimCaseTouchesClient(existing as any, clientKeyStr)) {
throw new ForbiddenException("This claim does not belong to your organization.");
}
const updated = await this.claimCaseDbService.findByIdAndUpdate(_id, {
$set: { "evaluation.rating": rating },
});
if (!updated) throw new NotFoundException("Claim file not found");
return {
message: "Claim expert rated successfully",
updatedRating: updated.rating,
updatedRating: (updated as any)?.evaluation?.rating || rating,
requestId: updated._id,
};
}
if (role === "blame") {
const updated = await this.requestManagementModel.findByIdAndUpdate(
_id,
{ $set: { rating } },
{ new: true },
);
if (!updated) {
throw new NotFoundException("Blame file not found");
const existing = await this.blameRequestDbService.findById(_id);
if (!existing) throw new NotFoundException("Blame file not found");
if (!blameCaseTouchesClient(existing as any, clientKeyStr)) {
throw new ForbiddenException("This blame case does not belong to your organization.");
}
const updated = await this.blameRequestDbService.findByIdAndUpdate(_id, {
$set: { "expert.rating": rating },
});
if (!updated) throw new NotFoundException("Blame file not found");
return {
message: "Blame expert rated successfully",
updatedRating: updated.rating,
updatedRating: (updated as any)?.expert?.rating || rating,
requestId: updated._id,
};
}
throw new BadRequestException("Invalid role");
}
async retrieveAllFilesOfClient(insurerId: string) {
const id = new Types.ObjectId(insurerId);
const blameFilesRaw = await this.requestManagementModel
.find({
"firstPartyDetails.firstPartyClient.clientId": id,
blameStatus: {
$nin: ["PendingForFirstParty", "PendingForSecondParty"],
},
})
.lean();
const claimFilesRaw = await this.claimRequestManagementModel
.find({
userClientKey: id,
}, {
_id: 1,
requestNumber: 1,
userClientKey: 1,
userId: 1,
fullName: 1,
carDetail: 1,
carPlate: 1,
claimStatus: 1,
currentStep: 1,
nextStep: 1,
carPartDamage: 1,
otherParts: 1,
sheba: 1,
nationalCodeOfInsurer: 1,
carGreenCard: 1,
aiImages: 1,
imageRequired: 1,
videoCaptureId: 1,
damageExpertReply: 1,
damageExpertReplyFinal: 1,
damageExpertResend: 1,
objection: 1,
userResendDocuments: 1,
priceDrop: 1,
requiredDocuments: 1,
createdAt: 1,
updatedAt: 1,
rating: 1,
visitLocation: 1,
})
.lean();
const populatedBlameFiles = await Promise.all(
blameFilesRaw.map((file) => this.populateBlameFileLinks(file)),
);
const populatedClaimFiles = await Promise.all(
claimFilesRaw.map(async (file) => {
const populated = await this.populateClaimFileLinks(file);
return await this.processImageRequired(populated);
}),
);
return { blameFiles: populatedBlameFiles, claimFiles: populatedClaimFiles };
}
private async populateBlameFileLinks(blameFile: any): Promise<any> {
if (!blameFile) return blameFile;
// --- FIX: Consistently use findById ---
if (blameFile.firstPartyDetails?.firstPartyFile?.firstPartyVideoId) {
const videoDoc = await this.blameVideoDbService.findById(
blameFile.firstPartyDetails.firstPartyFile.firstPartyVideoId.toString(),
);
if (videoDoc)
blameFile.firstPartyDetails.firstPartyFile.firstPartyVideoId =
buildFileLink(videoDoc.path);
}
if (Array.isArray(blameFile.firstPartyDetails?.firstPartyFile?.voices)) {
blameFile.firstPartyDetails.firstPartyFile.voices =
await this.populateIdArray(
this.blameVoiceDbService,
blameFile.firstPartyDetails.firstPartyFile.voices,
);
}
if (Array.isArray(blameFile.secondPartyDetails?.secondPartyFiles?.voices)) {
blameFile.secondPartyDetails.secondPartyFiles.voices =
await this.populateIdArray(
this.blameVoiceDbService,
blameFile.secondPartyDetails.secondPartyFiles.voices,
);
}
if (blameFile.expertResendReply) {
await this.populatePartyReplyLinks(
blameFile.expertResendReply.firstParty,
);
await this.populatePartyReplyLinks(
blameFile.expertResendReply.secondParty,
);
}
const finalReply =
blameFile.expertSubmitReplyFinal || blameFile.expertSubmitReply;
if (finalReply) {
await this.populateSignatureLink(finalReply.firstPartyComment);
await this.populateSignatureLink(finalReply.secondPartyComment);
}
return blameFile;
}
private async populateClaimFileLinks(claimFile: any): Promise<any> {
if (!claimFile) return claimFile;
if (claimFile.blameFile) {
claimFile.blameFile = await this.populateBlameFileLinks(
claimFile.blameFile,
);
}
// --- FIX: Consistently use findById ---
if (claimFile.videoCaptureId) {
const videoDoc = await this.claimVideoCaptureDbService.findById(
claimFile.videoCaptureId._id.toString(),
);
if (videoDoc) claimFile.videoCaptureId = buildFileLink(videoDoc.path);
}
if (claimFile.requiredDocuments) {
const documents = await this.claimRequiredDocumentDbService.findByClaimId(
claimFile._id.toString(),
);
// Populate with file URLs
const populatedDocuments = documents.map((doc) => ({
_id: doc._id,
documentType: doc.documentType,
fileName: doc.fileName,
fileUrl: buildFileLink(doc.path),
uploadedAt: doc.uploadedAt,
}));
claimFile.requiredDocuments = populatedDocuments;
}
return claimFile;
}
/**
* Processes imageRequired field:
* 1. Removes part_segments from aiReport.distinct_damaged_parts_report.parts[]
* 2. Populates imageId fields with file links
*/
private async processImageRequired(claimFile: any): Promise<any> {
if (!claimFile || !claimFile.imageRequired) {
return claimFile;
}
const imageRequired = claimFile.imageRequired;
// Process aroundTheCar array
if (Array.isArray(imageRequired.aroundTheCar)) {
imageRequired.aroundTheCar = await Promise.all(
imageRequired.aroundTheCar.map(async (item: any) => {
// Remove part_segments from aiReport.distinct_damaged_parts_report.parts[]
if (
item?.aiReport?.distinct_damaged_parts_report?.parts &&
Array.isArray(item.aiReport.distinct_damaged_parts_report.parts)
) {
item.aiReport.distinct_damaged_parts_report.parts =
item.aiReport.distinct_damaged_parts_report.parts.map(
(part: any) => {
const { part_segments, ...partWithoutSegments } = part;
return partWithoutSegments;
},
);
}
// Populate imageId with file link
if (item?.imageId) {
try {
const imageDoc = await this.damageImageDbService.findOne(
item.imageId.toString(),
);
if (imageDoc && imageDoc.path) {
item.imageId = buildFileLink(imageDoc.path);
}
} catch (error) {
this.logger.warn(
`Failed to populate imageId for aroundTheCar item: ${error.message}`,
);
}
}
return item;
}),
);
}
// Process selectPartOfCar array
if (Array.isArray(imageRequired.selectPartOfCar)) {
imageRequired.selectPartOfCar = await Promise.all(
imageRequired.selectPartOfCar.map(async (item: any) => {
// Remove part_segments from aiReport.distinct_damaged_parts_report.parts[]
if (
item?.aiReport?.distinct_damaged_parts_report?.parts &&
Array.isArray(item.aiReport.distinct_damaged_parts_report.parts)
) {
item.aiReport.distinct_damaged_parts_report.parts =
item.aiReport.distinct_damaged_parts_report.parts.map(
(part: any) => {
const { part_segments, ...partWithoutSegments } = part;
return partWithoutSegments;
},
);
}
// Populate imageId with file link
if (item?.imageId) {
try {
const imageDoc = await this.damageImageDbService.findOne(
item.imageId.toString(),
);
if (imageDoc && imageDoc.path) {
item.imageId = buildFileLink(imageDoc.path);
}
} catch (error) {
this.logger.warn(
`Failed to populate imageId for selectPartOfCar item: ${error.message}`,
);
}
}
return item;
}),
);
}
return claimFile;
}
private async populatePartyReplyLinks(partyReply: any) {
if (!partyReply) return;
// --- FIX: Consistently use findById ---
if (partyReply.voice) {
const voiceDoc = await this.blameVoiceDbService.findById(
partyReply.voice.toString(),
);
if (voiceDoc) partyReply.voice = buildFileLink(voiceDoc.path);
}
if (partyReply.documents) {
for (const docType in partyReply.documents) {
const docId = partyReply.documents[docType];
if (docId) {
const doc = await this.blameDocumentDbService.findById(
docId.toString(),
);
if (doc) partyReply.documents[docType] = buildFileLink(doc.path);
}
}
}
}
private async populateSignatureLink(comment: any) {
if (comment?.signDetail?.fileId) {
// --- FIX: Consistently use findById ---
const signDoc = await this.userSignDbService.findById(
comment.signDetail.fileId.toString(),
);
if (signDoc)
(comment.signDetail as any).fileUrl = buildFileLink(signDoc.path);
}
}
private async populateIdArray(dbService: any, ids: any[]): Promise<string[]> {
return Promise.all(
ids.map(async (id) => {
if (!id) return id;
if (typeof id === "object" && id.path) {
return buildFileLink(id.path);
}
const idString = id.toString();
if (idString.includes("[object Object]")) {
this.logger.warn(
"Invalid object found in ID array, skipping population.",
id,
);
return id;
}
try {
// --- FIX: Consistently use findById ---
const doc = await dbService.findById(idString);
return doc ? buildFileLink(doc.path) : id;
} catch (error) {
this.logger.error(`Failed to populate ID: ${idString}`, error);
return id;
}
}),
);
const id = this.getClientId(insurerId);
const [blameFiles, claimFiles] = await Promise.all([
this.getClientBlameFiles(id),
this.getClientClaimFiles(id),
]);
return { blameFiles, claimFiles };
}
async addBranch(clientKey: string, branchDto: CreateBranchDto) {
const clientId = new Types.ObjectId(clientKey);
const clientId = this.getClientId(clientKey);
const existingBranch = await this.branchDbService.findOne({
clientKey: clientId,
@@ -778,19 +381,8 @@ export class ExpertInsurerService {
* - Number of files created in the current month
*/
async getExpertStatisticsReport(actor: any) {
const { clientKey } = actor;
if (!clientKey) {
throw new BadRequestException("Client key is required");
}
const clientObjectId = new Types.ObjectId(clientKey);
// Get all claim files for this client
const claimFiles = await this.claimRequestManagementModel
.find({
userClientKey: clientObjectId,
})
.lean();
const clientObjectId = this.getClientId(actor);
const claimFiles = await this.getClientClaimFiles(clientObjectId);
// Calculate current month date range
const now = new Date();
@@ -907,6 +499,6 @@ export class ExpertInsurerService {
}
async retrieveInsuranceBranches(insuranceId: string) {
return await this.branchDbService.findAll(insuranceId);
return this.branchDbService.findAll(insuranceId);
}
}