Files
yara724api/src/expert-insurer/expert-insurer.service.ts
SepehrYahyaee 245f98b577 YARA-711
2026-01-19 17:05:51 +03:30

530 lines
16 KiB
TypeScript

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<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 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<string, number[]> = {};
const expertRatingsByCategoryMap: Record<
string,
Record<string, number[]>
> = {};
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<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;
}
}
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<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;
}
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;
}
}),
);
}
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;
}
}