forked from Yara724/api
530 lines
16 KiB
TypeScript
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;
|
|
}
|
|
}
|