import { BadRequestException, ConflictException, 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 { ClaimRequestManagementModel } from "src/claim-request-management/entites/schema/claim-request-management.schema"; 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"; 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, @InjectModel(RequestManagementModel.name) private readonly requestManagementModel: Model, private readonly blameVoiceDbService: BlameVoiceDbService, private readonly blameVideoDbService: BlameVideoDbService, private readonly blameDocumentDbService: BlameDocumentDbService, private readonly userSignDbService: UserSignDbService, private readonly claimVideoCaptureDbService: VideoCaptureDbService, private readonly branchDbService: BranchDbService, private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService, ) {} async retrieveAllExpertsOfClient( actor, currentPage: number, countPerPage: number, ) { const { clientKey } = actor; if (!clientKey) return; const clientObjectId = new Types.ObjectId(clientKey); const [experts, damageExperts] = await Promise.all([ this.expertDbService.findAll({ clientKey: clientKey }), this.damageExpertDbService.findAll({ clientKey: 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 = {}; const expertRatingsByCategoryMap: Record< string, Record > = {}; const processRatings = (expertId: string, rating: any) => { if (!expertId || !rating || typeof rating !== "object") { return; } const allRatingValues = Object.values(rating).filter( (val): val is number => typeof val === "number" && !isNaN(val), ); if (allRatingValues.length > 0) { if (!expertTotalRatingsMap[expertId]) expertTotalRatingsMap[expertId] = []; expertTotalRatingsMap[expertId].push(...allRatingValues); } if (!expertRatingsByCategoryMap[expertId]) { expertRatingsByCategoryMap[expertId] = {}; } for (const [category, value] of Object.entries(rating)) { if (typeof value === "number" && !isNaN(value)) { if (!expertRatingsByCategoryMap[expertId][category]) { expertRatingsByCategoryMap[expertId][category] = []; } expertRatingsByCategoryMap[expertId][category].push(value); } } }; for (const file of blameFiles) { const expertId = file?.actorLocked?.actorId?.toString(); processRatings(expertId, file.rating); } for (const file of claimFiles) { const expertId = file?.damageExpertReply?.actorDetail?.actorId; processRatings(expertId, file.rating); } 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), ) : null; const averageRatingsByCategory: Record = {}; 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; } } return { _id: expert._id, fullName: `${expert.firstName} ${expert.lastName}`, role: expert.role, type: expert.userType, requestStats: expert.requestStats, createdAt: expert.createdAt, overallAverageRating, averageRatingsByCategory, }; }); const start = (currentPage - 1) * countPerPage; const paginated = allExperts.slice(start, start + countPerPage); return { total: allExperts.length, page: currentPage, countPerPage, experts: paginated, }; } 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"; return 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, expertInfo: { _id: 1, fullName: { $concat: ["$expertInfo.firstName", " ", "$expertInfo.lastName"], }, requestStats: 1, userType: 1, createdAt: 1, }, }, }, ]); } async rateExpertOnFile( requestId: string, rating: FileRating, role: "claim" | "blame", ) { const _id = new Types.ObjectId(requestId); // Validate each rating factor const fields = Object.entries(rating); for (const [key, value] of fields) { if (typeof value !== "number" || value < 0 || value > 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"); } return { message: "Claim expert rated successfully", updatedRating: updated.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"); } return { message: "Blame expert rated successfully", updatedRating: updated.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, 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((file) => this.populateClaimFileLinks(file)), ); return { blameFiles: populatedBlameFiles, claimFiles: populatedClaimFiles }; } private async populateBlameFileLinks(blameFile: any): Promise { 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 { 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; } 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 { 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; } }), ); } async addBranch(clientKey: string, branchDto: CreateBranchDto) { const clientId = new Types.ObjectId(clientKey); const existingBranch = await this.branchDbService.findOne({ clientKey: clientId, code: branchDto.code, }); if (existingBranch) { throw new ConflictException( `A branch with code '${branchDto.code}' already exists for this client.`, ); } const newBranch = await this.branchDbService.create({ ...branchDto, clientKey: clientId, }); return newBranch; } }