forked from Yara724/api
Merge pull request 'Fixed reports+insurer + fixed field_expert not accessing claim APIs temporarily' (#16) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#16
This commit is contained in:
@@ -14,21 +14,21 @@ export class GlobalGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
});
|
||||
if (payload.role !== RoleEnum.USER) {
|
||||
console.log(
|
||||
"🚀 ~ GlobalGuard ~ canActivate ~ request.user.role:",
|
||||
request.user.role,
|
||||
);
|
||||
|
||||
if (payload.role !== RoleEnum.USER && payload.role !== RoleEnum.FIELD_EXPERT) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
request.user = payload;
|
||||
request.identity = request.user;
|
||||
} catch {
|
||||
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
assertBlameCaseForExpertTenant,
|
||||
blameCaseAccessibleToExpert,
|
||||
requireActorClientKey,
|
||||
} from "src/helpers/tenant-scope";
|
||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import {
|
||||
@@ -185,21 +190,21 @@ export class ExpertBlameService {
|
||||
*/
|
||||
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
|
||||
try {
|
||||
requireActorClientKey(actor);
|
||||
const expertId = actor.sub;
|
||||
|
||||
// Fetch all DISAGREEMENT cases
|
||||
const allCases = await this.blameRequestDbService.find(
|
||||
{
|
||||
blameStatus: BlameStatus.DISAGREEMENT,
|
||||
},
|
||||
{ lean: true },
|
||||
);
|
||||
|
||||
const allCases = await this.blameRequestDbService.find({}, { lean: true });
|
||||
|
||||
// Filter to show only:
|
||||
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||
// 2. Requests decided by current expert
|
||||
// 3. Expert-initiated: only the initiating field expert sees them
|
||||
// 1. Same insurance tenant (party clientId or expert-initiated by this actor)
|
||||
// 2. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||
// 3. Requests decided by current expert
|
||||
// 4. Expert-initiated: only the initiating field expert sees them
|
||||
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
|
||||
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expertInitiated = doc.expertInitiated === true;
|
||||
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
||||
|
||||
@@ -520,12 +525,15 @@ export class ExpertBlameService {
|
||||
* Excludes history. Returns only non–CAR_BODY types. Builds file links for all evidence.
|
||||
* Access control: Only allows viewing fresh requests or requests decided by current expert.
|
||||
*/
|
||||
async findOneV2(requestId: string, actorId: string): Promise<Record<string, unknown>> {
|
||||
async findOneV2(requestId: string, actor: any): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
||||
if (!doc) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
assertBlameCaseForExpertTenant(doc, actor);
|
||||
const type = doc.type as string;
|
||||
if (type === BlameRequestType.CAR_BODY) {
|
||||
throw new ForbiddenException(
|
||||
@@ -668,6 +676,8 @@ export class ExpertBlameService {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
|
||||
assertBlameCaseForExpertTenant(request, actorDetail);
|
||||
|
||||
// Validate request is available for expert review
|
||||
if (
|
||||
request.status !== CaseStatus.WAITING_FOR_EXPERT ||
|
||||
@@ -696,7 +706,9 @@ export class ExpertBlameService {
|
||||
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||
if (Date.now() < lockExpiryTime) {
|
||||
// Lock is still valid
|
||||
const lockedByActorId = String(request.workflow.lockedBy?.actorId);
|
||||
const lockedByActorId = String(
|
||||
request.workflow.lockedBy?.actorId ?? "",
|
||||
);
|
||||
if (lockedByActorId === actorDetail.sub) {
|
||||
throw new BadRequestException(
|
||||
"You have already locked this request",
|
||||
@@ -755,15 +767,19 @@ export class ExpertBlameService {
|
||||
async resendRequestV2(
|
||||
requestId: string,
|
||||
resendDto: ResendRequestDto,
|
||||
actorId: string,
|
||||
actor: any,
|
||||
): Promise<{ requestId: string; status: string }> {
|
||||
try {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
|
||||
assertBlameCaseForExpertTenant(request, actor);
|
||||
|
||||
// Validate request is locked by current expert
|
||||
if (!request.workflow?.locked) {
|
||||
throw new ForbiddenException(
|
||||
@@ -872,15 +888,19 @@ export class ExpertBlameService {
|
||||
async replyRequestV2(
|
||||
requestId: string,
|
||||
reply: SubmitReplyDto,
|
||||
actorId: string,
|
||||
actor: any,
|
||||
): Promise<{ requestId: string; status: string }> {
|
||||
try {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new NotFoundException("Request not found");
|
||||
}
|
||||
|
||||
assertBlameCaseForExpertTenant(request, actor);
|
||||
|
||||
// Validate no decision exists yet
|
||||
if (request.expert?.decision) {
|
||||
throw new ForbiddenException(
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ExpertBlameV2Controller {
|
||||
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||
async findOne(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||
try {
|
||||
return await this.expertBlameService.findOneV2(id, actor.sub);
|
||||
return await this.expertBlameService.findOneV2(id, actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
@@ -73,7 +73,7 @@ export class ExpertBlameV2Controller {
|
||||
@CurrentUser() actor: any,
|
||||
) {
|
||||
try {
|
||||
return await this.expertBlameService.resendRequestV2(id, body, actor.sub);
|
||||
return await this.expertBlameService.resendRequestV2(id, body, actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
@@ -93,7 +93,7 @@ export class ExpertBlameV2Controller {
|
||||
@CurrentUser() actor: any,
|
||||
) {
|
||||
try {
|
||||
return await this.expertBlameService.replyRequestV2(id, body, actor.sub);
|
||||
return await this.expertBlameService.replyRequestV2(id, body, actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
|
||||
@@ -32,6 +32,11 @@ import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
|
||||
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||
import {
|
||||
assertClaimCaseForTenant,
|
||||
claimCaseTouchesClient,
|
||||
requireActorClientKey,
|
||||
} from "src/helpers/tenant-scope";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { GetClaimListV2ResponseDto, ClaimListItemV2Dto } from "./dto/claim-list-v2.dto";
|
||||
import { ClaimDetailV2ResponseDto } from "./dto/claim-detail-v2.dto";
|
||||
@@ -1575,12 +1580,15 @@ export class ExpertClaimService {
|
||||
* - Sets currentStep = EXPERT_DAMAGE_ASSESSMENT
|
||||
*/
|
||||
async lockClaimRequestV2(claimRequestId: string, actor: any) {
|
||||
requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException('Claim request not found');
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not available for locking. Current status: ${claim.status}`,
|
||||
@@ -1655,12 +1663,15 @@ export class ExpertClaimService {
|
||||
reply: import('./dto/expert-claim-v2.dto').SubmitExpertReplyV2Dto,
|
||||
actor: any,
|
||||
) {
|
||||
requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException('Claim request not found');
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not in a reviewable state. Current status: ${claim.status}`,
|
||||
@@ -1762,12 +1773,15 @@ export class ExpertClaimService {
|
||||
* - Unlocks the workflow so user can act
|
||||
*/
|
||||
async requestInPersonVisitV2(claimRequestId: string, actor: any, note?: string) {
|
||||
requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException('Claim request not found');
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,
|
||||
@@ -1815,7 +1829,10 @@ export class ExpertClaimService {
|
||||
* 2. WAITING_FOR_DAMAGE_EXPERT status AND not locked (claimStatus PENDING)
|
||||
* 3. Any status locked by THIS expert (their own in-progress work)
|
||||
*/
|
||||
async getClaimListV2(actorId: string): Promise<GetClaimListV2ResponseDto> {
|
||||
async getClaimListV2(actor: any): Promise<GetClaimListV2ResponseDto> {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const clientKey = actor.clientKey as string;
|
||||
const claims = await this.claimCaseDbService.find({
|
||||
$or: [
|
||||
// Available claims: waiting for expert, not locked
|
||||
@@ -1831,7 +1848,9 @@ export class ExpertClaimService {
|
||||
],
|
||||
});
|
||||
|
||||
const list = (claims as any[]).map((c) => ({
|
||||
const list = (claims as any[])
|
||||
.filter((c) => claimCaseTouchesClient(c, clientKey))
|
||||
.map((c) => ({
|
||||
claimRequestId: c._id.toString(),
|
||||
publicId: c.publicId,
|
||||
status: c.status,
|
||||
@@ -1866,14 +1885,18 @@ export class ExpertClaimService {
|
||||
*/
|
||||
async getClaimDetailV2(
|
||||
claimRequestId: string,
|
||||
actorId: string,
|
||||
actor: any,
|
||||
): Promise<ClaimDetailV2ResponseDto> {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
throw new NotFoundException('Claim request not found');
|
||||
}
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
if (claim.status !== ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
|
||||
throw new ForbiddenException(
|
||||
`This claim is not available for expert review. Current status: ${claim.status}`,
|
||||
@@ -1980,10 +2003,12 @@ export class ExpertClaimService {
|
||||
body: UpdateClaimDamagedPartsV2Dto,
|
||||
actor: any,
|
||||
) {
|
||||
requireActorClientKey(actor);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ExpertClaimV2Controller {
|
||||
"Returns claims that are WAITING_FOR_DAMAGE_EXPERT and not locked by another expert, plus this expert's own locked/in-progress claims.",
|
||||
})
|
||||
async getClaimListV2(@CurrentUser() actor) {
|
||||
return await this.expertClaimService.getClaimListV2(actor.sub);
|
||||
return await this.expertClaimService.getClaimListV2(actor);
|
||||
}
|
||||
|
||||
@Get("request/:claimRequestId")
|
||||
@@ -53,10 +53,7 @@ export class ExpertClaimV2Controller {
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@CurrentUser() actor,
|
||||
) {
|
||||
return await this.expertClaimService.getClaimDetailV2(
|
||||
claimRequestId,
|
||||
actor.sub,
|
||||
);
|
||||
return await this.expertClaimService.getClaimDetailV2(claimRequestId, actor);
|
||||
}
|
||||
|
||||
@Put("lock/:claimRequestId")
|
||||
|
||||
@@ -78,6 +78,7 @@ export class ExpertInsurerController {
|
||||
@ApiQuery({ name: "role", enum: ["claim", "blame"] })
|
||||
@Get("/:expertId")
|
||||
async requestDetail(
|
||||
@CurrentUser() insurer,
|
||||
@Param("expertId") id: string,
|
||||
@Query("role") role: "claim" | "blame",
|
||||
) {
|
||||
@@ -89,7 +90,11 @@ export class ExpertInsurerController {
|
||||
throw new BadRequestException("Invalid role");
|
||||
}
|
||||
|
||||
return await this.expertInsurerService.getAllFilesByExpertAndRole(id, role);
|
||||
return await this.expertInsurerService.getAllFilesByExpertAndRole(
|
||||
id,
|
||||
role,
|
||||
insurer.clientKey,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBody({
|
||||
@@ -153,6 +158,7 @@ export class ExpertInsurerController {
|
||||
@ApiQuery({ name: "role", enum: ["claim", "blame"] })
|
||||
@Put("/:requestId/rating")
|
||||
async rateExperts(
|
||||
@CurrentUser() insurer,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() rating: FileRating,
|
||||
@Query("role") role: "claim" | "blame",
|
||||
@@ -165,13 +171,13 @@ export class ExpertInsurerController {
|
||||
requestId,
|
||||
rating,
|
||||
role,
|
||||
insurer.clientKey,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("branches/:insuranceId")
|
||||
@ApiParam({ name: "insuranceId" })
|
||||
async getInsuranceBranches(@Param("insuranceId") insuranceId: string) {
|
||||
console.log("insuranceId", insuranceId);
|
||||
return await this.expertInsurerService.retrieveInsuranceBranches(
|
||||
insuranceId,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
66
src/helpers/tenant-scope.ts
Normal file
66
src/helpers/tenant-scope.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { BadRequestException, ForbiddenException } from "@nestjs/common";
|
||||
|
||||
/** Insurance company tenant id on the actor JWT (Mongo ObjectId string). */
|
||||
export function requireActorClientKey(actor: { clientKey?: string }): string {
|
||||
const ck = actor?.clientKey;
|
||||
if (!ck || typeof ck !== "string") {
|
||||
throw new BadRequestException(
|
||||
"Client scope is missing for this account. Insurer/expert users must be bound to an insurance company.",
|
||||
);
|
||||
}
|
||||
return ck;
|
||||
}
|
||||
|
||||
export function blameCaseTouchesClient(doc: any, clientKey: string): boolean {
|
||||
const id = String(clientKey);
|
||||
return (doc?.parties ?? []).some(
|
||||
(p: any) => String(p?.person?.clientId ?? "") === id,
|
||||
);
|
||||
}
|
||||
|
||||
export function claimCaseTouchesClient(doc: any, clientKey: string): boolean {
|
||||
return String(doc?.owner?.userClientKey ?? "") === String(clientKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Field expert–initiated cases may not yet have party clientId filled; the
|
||||
* initiating expert is always scoped to their insurer via JWT.
|
||||
*/
|
||||
export function blameCaseAccessibleToExpert(
|
||||
doc: any,
|
||||
actor: { sub: string; clientKey?: string },
|
||||
): boolean {
|
||||
if (
|
||||
doc.expertInitiated &&
|
||||
doc.initiatedByFieldExpertId &&
|
||||
String(doc.initiatedByFieldExpertId) === actor.sub
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const ck = actor?.clientKey;
|
||||
if (!ck) return false;
|
||||
return blameCaseTouchesClient(doc, ck);
|
||||
}
|
||||
|
||||
export function assertBlameCaseForExpertTenant(
|
||||
doc: any,
|
||||
actor: { sub: string; clientKey?: string },
|
||||
): void {
|
||||
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
||||
throw new ForbiddenException(
|
||||
"This blame case does not belong to your organization.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertClaimCaseForTenant(
|
||||
doc: any,
|
||||
actor: { clientKey?: string },
|
||||
): void {
|
||||
const ck = requireActorClientKey(actor);
|
||||
if (!claimCaseTouchesClient(doc, ck)) {
|
||||
throw new ForbiddenException(
|
||||
"This claim does not belong to your organization.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,28 @@
|
||||
export class DamageExpertAllRequestsCountReportDtoRs {
|
||||
all: number;
|
||||
UnChecked: number;
|
||||
CheckedRequest: number;
|
||||
CheckAgain: number;
|
||||
WaitingForUserToResend: number;
|
||||
CloseRequest: number;
|
||||
WaitingForUserCompleted: number;
|
||||
InPersonVisit: number;
|
||||
/**
|
||||
* Native workflow status keys from blameCases (`CaseStatus`) or claimCases (`ClaimCaseStatus`),
|
||||
* plus `all` = total matching documents for the tenant.
|
||||
*/
|
||||
export class BlameCaseStatusCountReportDtoRs {
|
||||
[key: string]: number;
|
||||
|
||||
constructor(report: any) {
|
||||
this.all = report.all;
|
||||
this.UnChecked = report.UnChecked;
|
||||
this.CheckedRequest = report.CheckedRequest;
|
||||
this.CheckAgain = report.CheckAgain;
|
||||
this.WaitingForUserToResend = report.WaitingForUserToResend;
|
||||
this.CloseRequest = report.CloseRequest;
|
||||
this.WaitingForUserCompleted = report.WaitingForUserCompleted;
|
||||
this.InPersonVisit = report.InPersonVisit;
|
||||
constructor(report: Record<string, number>) {
|
||||
Object.assign(this, report);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpertAllRequestsCountReportDtoRs {
|
||||
all: number;
|
||||
UnChecked: number;
|
||||
CheckedRequest: number;
|
||||
CheckAgain: number;
|
||||
WaitingForUserToResend: number;
|
||||
CloseRequest: number;
|
||||
WaitingForUserCompleted: number;
|
||||
export class ClaimCaseStatusCountReportDtoRs {
|
||||
[key: string]: number;
|
||||
|
||||
constructor(report: any) {
|
||||
this.all = report.all;
|
||||
this.UnChecked = report.UnChecked;
|
||||
this.CheckedRequest = report.CheckedRequest;
|
||||
this.CheckAgain = report.CheckAgain;
|
||||
this.WaitingForUserToResend = report.WaitingForUserToResend;
|
||||
this.CloseRequest = report.CloseRequest;
|
||||
this.WaitingForUserCompleted = report.WaitingForUserCompleted;
|
||||
constructor(report: Record<string, number>) {
|
||||
Object.assign(this, report);
|
||||
}
|
||||
}
|
||||
|
||||
export class CompanyAllRequestsCountReportDtoRs {
|
||||
blame: object;
|
||||
claim: object;
|
||||
blame: Record<string, number>;
|
||||
claim: Record<string, number>;
|
||||
|
||||
constructor(blame: object, claim: object) {
|
||||
constructor(blame: Record<string, number>, claim: Record<string, number>) {
|
||||
this.blame = blame;
|
||||
this.claim = claim;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module";
|
||||
import { RequestManagementModule } from "src/request-management/request-management.module";
|
||||
import { ClientModule } from "src/client/client.module";
|
||||
import { ReportsController } from "./reports.controller";
|
||||
import { ReportsService } from "./reports.service";
|
||||
|
||||
@@ -9,7 +8,6 @@ import { ReportsService } from "./reports.service";
|
||||
imports: [
|
||||
RequestManagementModule,
|
||||
ClaimRequestManagementModule,
|
||||
ClientModule,
|
||||
],
|
||||
controllers: [ReportsController],
|
||||
providers: [ReportsService],
|
||||
|
||||
@@ -1,413 +1,243 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { Types } from "mongoose";
|
||||
import { ClaimRequestManagementDbService } from "src/claim-request-management/entites/db-service/claim-request-management.db.service";
|
||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
|
||||
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
||||
import { ReqClaimStatus } from "src/Types&Enums/claim-request-management/status.enum";
|
||||
import { UserType } from "src/Types&Enums/userType.enum";
|
||||
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { requireActorClientKey } from "src/helpers/tenant-scope";
|
||||
import {
|
||||
BlameCaseStatusCountReportDtoRs,
|
||||
ClaimCaseStatusCountReportDtoRs,
|
||||
CompanyAllRequestsCountReportDtoRs,
|
||||
DamageExpertAllRequestsCountReportDtoRs,
|
||||
ExpertAllRequestsCountReportDtoRs,
|
||||
} from "./dto/reports.dto";
|
||||
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
private readonly logger = new Logger(ReportsService.name);
|
||||
|
||||
constructor(
|
||||
private readonly requestManagementDbService: RequestManagementDbService,
|
||||
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
|
||||
private readonly clientDbService: ClientDbService,
|
||||
private readonly blameRequestDbService: BlameRequestDbService,
|
||||
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||
) {}
|
||||
|
||||
async getAllCheckedRequestsCountFn(role: string, client: string) {
|
||||
const statuses = ["UnChecked", "CheckedRequest"];
|
||||
const data: Record<string, number> = { all: 0 };
|
||||
const clientId = new Types.ObjectId(client);
|
||||
|
||||
for (const status of statuses) {
|
||||
let count = 0;
|
||||
if (role === "damage_expert") {
|
||||
count = await this.claimRequestManagementDbService.countByFilter({
|
||||
claimStatus: status,
|
||||
userClientKey: clientId,
|
||||
});
|
||||
} else {
|
||||
count = await this.requestManagementDbService.countByFilter({
|
||||
blameStatus: status,
|
||||
$or: [
|
||||
{ "firstPartyDetails.firstPartyClient.clientId": clientId },
|
||||
{ "secondPartyDetails.secondPartyClient.clientId": clientId },
|
||||
],
|
||||
});
|
||||
}
|
||||
data[status] = count;
|
||||
data.all += count;
|
||||
}
|
||||
return data;
|
||||
private clientObjectId(client: string | undefined): Types.ObjectId {
|
||||
const ck = requireActorClientKey({ clientKey: client });
|
||||
return new Types.ObjectId(ck);
|
||||
}
|
||||
|
||||
async getDateFilteredRequestByStatus(
|
||||
role: string,
|
||||
client: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
) {
|
||||
const statuses = ["UnChecked", "CheckedRequest", "CheckAgain"];
|
||||
const data: Record<string, number> = {};
|
||||
const clientId = new Types.ObjectId(client);
|
||||
|
||||
for (const status of statuses) {
|
||||
let blameCount = 0;
|
||||
let claimCount = 0;
|
||||
|
||||
if (role === "expert" || role === "company") {
|
||||
blameCount = await this.requestManagementDbService.countByFilter({
|
||||
blameStatus: status,
|
||||
createdAt: { $gte: start, $lte: end },
|
||||
$or: [
|
||||
{ "firstPartyDetails.firstPartyClient.clientId": clientId },
|
||||
{ "secondPartyDetails.secondPartyClient.clientId": clientId },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (role === "damage_expert" || role === "company") {
|
||||
claimCount = await this.claimRequestManagementDbService.countByFilter({
|
||||
claimStatus: status,
|
||||
userClientKey: clientId,
|
||||
createdAt: { $gte: start, $lte: end },
|
||||
});
|
||||
}
|
||||
data[status] = blameCount + claimCount;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private isVisibleToClientType(client: any, actor: any): boolean {
|
||||
if (actor.userType === UserType.GENUINE) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
actor.userType === UserType.LEGAL &&
|
||||
String(client._id) === actor.clientKey
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private wasHandledByActor(request: any, actorSub: string): boolean {
|
||||
type ActorCheckerEntry = { CheckedRequest?: { actorId: string } };
|
||||
const actorChecker = request.actorsChecker as ActorCheckerEntry[];
|
||||
|
||||
if (!Array.isArray(actorChecker)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matchingEntry = actorChecker.find(
|
||||
(entry) => String(entry?.CheckedRequest?.actorId) === actorSub,
|
||||
private isBlameForClient(r: any, client: Types.ObjectId): boolean {
|
||||
const idStr = String(client);
|
||||
return (r?.parties || []).some(
|
||||
(p) => String(p?.person?.clientId || "") === idStr,
|
||||
);
|
||||
|
||||
return !!matchingEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters claim requests using the same logic as expert-claim service
|
||||
* to ensure consistency between endpoints
|
||||
*/
|
||||
private async filterClaimRequestsForExpert(
|
||||
requests: any[],
|
||||
actor: any,
|
||||
): Promise<any[]> {
|
||||
const filteredRequests = [];
|
||||
private isClaimForClient(r: any, client: Types.ObjectId): boolean {
|
||||
return String(r?.owner?.userClientKey || "") === String(client);
|
||||
}
|
||||
|
||||
for (const r of requests) {
|
||||
// For expert-initiated blame files, only show to the initiating expert
|
||||
if (r.blameFile?.expertInitiated && r.blameFile?.initiatedBy) {
|
||||
if (String(r.blameFile.initiatedBy) !== actor.sub) {
|
||||
continue; // Skip if not the initiating expert
|
||||
}
|
||||
// Expert-initiated claim files are always visible to the initiating expert
|
||||
filteredRequests.push(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
const client = await this.clientDbService.findOne({
|
||||
_id: r.userClientKey,
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
this.logger.warn(
|
||||
`Client not found for claim request with ID: ${r._id}. Skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const specialHandlingStatuses = [
|
||||
ReqClaimStatus.CheckAgain,
|
||||
ReqClaimStatus.ReviewRequest,
|
||||
ReqClaimStatus.PendingFactorValidation,
|
||||
];
|
||||
|
||||
const requiresSpecificActorCheck = specialHandlingStatuses.includes(
|
||||
r.claimStatus,
|
||||
);
|
||||
|
||||
if (requiresSpecificActorCheck) {
|
||||
if (this.wasHandledByActor(r, actor.sub)) {
|
||||
filteredRequests.push(r);
|
||||
}
|
||||
} else {
|
||||
if (this.isVisibleToClientType(client, actor)) {
|
||||
filteredRequests.push(r);
|
||||
}
|
||||
}
|
||||
private countBlameByCaseStatus(
|
||||
blames: any[],
|
||||
clientId: Types.ObjectId,
|
||||
): Record<string, number> {
|
||||
const out: Record<string, number> = { all: 0 };
|
||||
for (const s of Object.values(CaseStatus)) {
|
||||
out[s] = 0;
|
||||
}
|
||||
|
||||
return filteredRequests;
|
||||
for (const r of blames) {
|
||||
if (!this.isBlameForClient(r, clientId)) continue;
|
||||
const st = r?.status as string;
|
||||
if (st in out) out[st]++;
|
||||
else out[st] = (out[st] ?? 0) + 1;
|
||||
out.all++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async getAllRequestsCountByRole(role: string, client: string, actor?: any) {
|
||||
private countClaimByCaseStatus(
|
||||
claims: any[],
|
||||
clientId: Types.ObjectId,
|
||||
): Record<string, number> {
|
||||
const out: Record<string, number> = { all: 0 };
|
||||
for (const s of Object.values(ClaimCaseStatus)) {
|
||||
out[s] = 0;
|
||||
}
|
||||
for (const r of claims) {
|
||||
if (!this.isClaimForClient(r, clientId)) continue;
|
||||
const st = r?.status as string;
|
||||
if (st in out) out[st]++;
|
||||
else out[st] = (out[st] ?? 0) + 1;
|
||||
out.all++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async getAllRequestsCountByRole(role: string, client: string | undefined) {
|
||||
const clientId = this.clientObjectId(client);
|
||||
const [blames, claims] = await Promise.all([
|
||||
this.blameRequestDbService.find({}, { lean: true }),
|
||||
this.claimCaseDbService.find({}, { lean: true }),
|
||||
]);
|
||||
|
||||
if (role === "expert") {
|
||||
const statuses = Object.values(ReqBlameStatus);
|
||||
const data: Record<string, number> = { all: 0 };
|
||||
|
||||
for (const status of statuses) {
|
||||
const filter = {
|
||||
blameStatus: status,
|
||||
$or: [
|
||||
{
|
||||
"firstPartyDetails.firstPartyClient.clientId": new Types.ObjectId(
|
||||
client,
|
||||
),
|
||||
},
|
||||
// {
|
||||
// "secondPartyDetails.secondPartyClient.clientId":
|
||||
// new Types.ObjectId(client),
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
const count =
|
||||
await this.requestManagementDbService.countByFilter(filter);
|
||||
data[status] = count;
|
||||
data.all += count;
|
||||
}
|
||||
|
||||
return data;
|
||||
return this.countBlameByCaseStatus(blames as any[], clientId);
|
||||
}
|
||||
|
||||
if (role === "damage_expert") {
|
||||
const statuses = Object.values(ReqClaimStatus);
|
||||
const data: Record<string, number> = { all: 0 };
|
||||
|
||||
// For damage_expert, we need to apply the same filtering as expert-claim service
|
||||
if (actor) {
|
||||
// Fetch all requests with the statuses that expert-claim service shows
|
||||
// This matches the statuses in getClaimRequestsListForExpert
|
||||
const relevantStatuses = [
|
||||
ReqClaimStatus.UnChecked,
|
||||
ReqClaimStatus.ReviewRequest,
|
||||
ReqClaimStatus.CheckAgain,
|
||||
ReqClaimStatus.CloseRequest,
|
||||
ReqClaimStatus.InPersonVisit,
|
||||
ReqClaimStatus.CheckedRequest,
|
||||
ReqClaimStatus.PendingFactorValidation,
|
||||
];
|
||||
|
||||
// Fetch all requests with relevant statuses (matching expert-claim query)
|
||||
const allRequests =
|
||||
await this.claimRequestManagementDbService.findAllByStatus({
|
||||
claimStatus: { $in: relevantStatuses },
|
||||
});
|
||||
|
||||
// Filter requests using the same logic as expert-claim service
|
||||
const filteredRequests =
|
||||
await this.filterClaimRequestsForExpert(allRequests, actor);
|
||||
|
||||
// Count by status from filtered results
|
||||
for (const status of statuses) {
|
||||
const count = filteredRequests.filter(
|
||||
(r) => r.claimStatus === status,
|
||||
).length;
|
||||
data[status] = count;
|
||||
data.all += count;
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple count if actor not provided (shouldn't happen for damage_expert)
|
||||
for (const status of statuses) {
|
||||
const filter = {
|
||||
claimStatus: status,
|
||||
userClientKey: new Types.ObjectId(client),
|
||||
};
|
||||
|
||||
const count =
|
||||
await this.claimRequestManagementDbService.countByFilter(filter);
|
||||
data[status] = count;
|
||||
data.all += count;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
return this.countClaimByCaseStatus(claims as any[], clientId);
|
||||
}
|
||||
|
||||
// ✅ Company logic
|
||||
if (role === "company") {
|
||||
const blameStatuses = Object.values(ReqBlameStatus);
|
||||
const claimStatuses = Object.values(ReqClaimStatus);
|
||||
|
||||
const blameData: Record<string, number> = { all: 0 };
|
||||
const claimData: Record<string, number> = { all: 0 };
|
||||
|
||||
// Only count files where this company is FIRST party in Blame
|
||||
for (const status of blameStatuses) {
|
||||
const filter = {
|
||||
blameStatus: status,
|
||||
"firstPartyDetails.firstPartyClient.clientId": new Types.ObjectId(
|
||||
client,
|
||||
),
|
||||
};
|
||||
|
||||
const count =
|
||||
await this.requestManagementDbService.countByFilter(filter);
|
||||
blameData[status] = count;
|
||||
blameData.all += count;
|
||||
}
|
||||
|
||||
// Claims always use `userClientKey`
|
||||
for (const status of claimStatuses) {
|
||||
const filter = {
|
||||
claimStatus: status,
|
||||
userClientKey: new Types.ObjectId(client),
|
||||
};
|
||||
|
||||
const count =
|
||||
await this.claimRequestManagementDbService.countByFilter(filter);
|
||||
claimData[status] = count;
|
||||
claimData.all += count;
|
||||
}
|
||||
|
||||
return { blameData, claimData };
|
||||
return {
|
||||
blame: this.countBlameByCaseStatus(blames as any[], clientId),
|
||||
claim: this.countClaimByCaseStatus(claims as any[], clientId),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getAllRequestsReportCount(actor, client) {
|
||||
if (actor.role === "damage_expert") {
|
||||
// Pass actor to apply filtering logic for damage_expert
|
||||
const data = await this.getAllRequestsCountByRole(
|
||||
actor.role,
|
||||
client,
|
||||
actor,
|
||||
);
|
||||
return new DamageExpertAllRequestsCountReportDtoRs(data);
|
||||
} else if (actor.role === "expert") {
|
||||
const data = await this.getAllRequestsCountByRole(actor.role, client);
|
||||
return new ExpertAllRequestsCountReportDtoRs(data);
|
||||
} else {
|
||||
// company
|
||||
const damageExpertData = await this.getAllRequestsCountByRole(
|
||||
"damage_expert",
|
||||
client,
|
||||
);
|
||||
const expertData = await this.getAllRequestsCountByRole("expert", client);
|
||||
return new CompanyAllRequestsCountReportDtoRs(
|
||||
expertData,
|
||||
damageExpertData,
|
||||
);
|
||||
/**
|
||||
* Per calendar month, counts by native `CaseStatus` / `ClaimCaseStatus`.
|
||||
* Company gets both blame and claim maps; expert / damage_expert get one map.
|
||||
*/
|
||||
async getDateFilteredRequestByStatus(
|
||||
role: string,
|
||||
client: string | undefined,
|
||||
start: Date,
|
||||
end: Date,
|
||||
) {
|
||||
const clientId = this.clientObjectId(client);
|
||||
const [blames, claims] = await Promise.all([
|
||||
this.blameRequestDbService.find({}, { lean: true }),
|
||||
this.claimCaseDbService.find({}, { lean: true }),
|
||||
]);
|
||||
|
||||
const inRange = (r: any) => {
|
||||
const createdAt = new Date(r.createdAt);
|
||||
return createdAt >= start && createdAt <= end;
|
||||
};
|
||||
|
||||
if (role === "company") {
|
||||
const blameOut: Record<string, number> = { all: 0 };
|
||||
const claimOut: Record<string, number> = { all: 0 };
|
||||
for (const s of Object.values(CaseStatus)) blameOut[s] = 0;
|
||||
for (const s of Object.values(ClaimCaseStatus)) claimOut[s] = 0;
|
||||
|
||||
for (const r of blames as any[]) {
|
||||
if (!this.isBlameForClient(r, clientId) || !inRange(r)) continue;
|
||||
const st = r?.status as string;
|
||||
if (st in blameOut) blameOut[st]++;
|
||||
else blameOut[st] = (blameOut[st] ?? 0) + 1;
|
||||
blameOut.all++;
|
||||
}
|
||||
for (const r of claims as any[]) {
|
||||
if (!this.isClaimForClient(r, clientId) || !inRange(r)) continue;
|
||||
const st = r?.status as string;
|
||||
if (st in claimOut) claimOut[st]++;
|
||||
else claimOut[st] = (claimOut[st] ?? 0) + 1;
|
||||
claimOut.all++;
|
||||
}
|
||||
return { blame: blameOut, claim: claimOut };
|
||||
}
|
||||
|
||||
if (role === "expert") {
|
||||
const out: Record<string, number> = { all: 0 };
|
||||
for (const s of Object.values(CaseStatus)) out[s] = 0;
|
||||
for (const r of blames as any[]) {
|
||||
if (!this.isBlameForClient(r, clientId) || !inRange(r)) continue;
|
||||
const st = r?.status as string;
|
||||
if (st in out) out[st]++;
|
||||
else out[st] = (out[st] ?? 0) + 1;
|
||||
out.all++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (role === "damage_expert") {
|
||||
const out: Record<string, number> = { all: 0 };
|
||||
for (const s of Object.values(ClaimCaseStatus)) out[s] = 0;
|
||||
for (const r of claims as any[]) {
|
||||
if (!this.isClaimForClient(r, clientId) || !inRange(r)) continue;
|
||||
const st = r?.status as string;
|
||||
if (st in out) out[st]++;
|
||||
else out[st] = (out[st] ?? 0) + 1;
|
||||
out.all++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
throw new BadRequestException("Unsupported role for reports");
|
||||
}
|
||||
|
||||
async getAllRequestsReportPerMonth(actor, client) {
|
||||
const result = [];
|
||||
async getAllRequestsReportCount(actor: any, client: string | undefined) {
|
||||
requireActorClientKey(actor);
|
||||
|
||||
if (actor.role === "damage_expert") {
|
||||
const data = await this.getAllRequestsCountByRole(actor.role, client);
|
||||
return new ClaimCaseStatusCountReportDtoRs(data as Record<string, number>);
|
||||
}
|
||||
if (actor.role === "expert") {
|
||||
const data = await this.getAllRequestsCountByRole(actor.role, client);
|
||||
return new BlameCaseStatusCountReportDtoRs(data as Record<string, number>);
|
||||
}
|
||||
const companyPayload = await this.getAllRequestsCountByRole(
|
||||
"company",
|
||||
client,
|
||||
);
|
||||
return new CompanyAllRequestsCountReportDtoRs(
|
||||
(companyPayload as { blame: Record<string, number> }).blame,
|
||||
(companyPayload as { claim: Record<string, number> }).claim,
|
||||
);
|
||||
}
|
||||
|
||||
async getAllRequestsReportPerMonth(actor: any, client: string | undefined) {
|
||||
requireActorClientKey(actor);
|
||||
const result: any[] = [];
|
||||
const now = new Date();
|
||||
|
||||
if (actor.role === "company") {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const monthEnd = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() - i + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
const label = monthStart.toLocaleDateString("fa-IR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const monthEnd = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() - i + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
const label = monthStart.toLocaleDateString("fa-IR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const blameData = await this.getDateFilteredRequestByStatus(
|
||||
"expert",
|
||||
let data: any;
|
||||
if (actor.role === "company") {
|
||||
data = await this.getDateFilteredRequestByStatus(
|
||||
"company",
|
||||
client,
|
||||
monthStart,
|
||||
monthEnd,
|
||||
);
|
||||
const claimData = await this.getDateFilteredRequestByStatus(
|
||||
"damage_expert",
|
||||
client,
|
||||
monthStart,
|
||||
monthEnd,
|
||||
);
|
||||
|
||||
result.unshift({
|
||||
stDate: monthStart,
|
||||
enDate: monthEnd,
|
||||
faLabel: label,
|
||||
data: {
|
||||
blame: blameData,
|
||||
claim: claimData,
|
||||
},
|
||||
});
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const monthEnd = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() - i + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
const label = monthStart.toLocaleDateString("fa-IR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
const data = await this.getDateFilteredRequestByStatus(
|
||||
} else {
|
||||
data = await this.getDateFilteredRequestByStatus(
|
||||
actor.role,
|
||||
client,
|
||||
monthStart,
|
||||
monthEnd,
|
||||
);
|
||||
result.unshift({
|
||||
stDate: monthStart,
|
||||
enDate: monthEnd,
|
||||
faLabel: label,
|
||||
data,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
||||
result.unshift({
|
||||
stDate: monthStart,
|
||||
enDate: monthEnd,
|
||||
faLabel: label,
|
||||
data,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAllCheckedRequestsCount(actor, client) {
|
||||
if (actor.role === "damage_expert" || actor.role === "expert") {
|
||||
return this.getAllCheckedRequestsCountFn(actor.role, client);
|
||||
} else {
|
||||
const blame = await this.getAllCheckedRequestsCountFn("expert", client);
|
||||
const claim = await this.getAllCheckedRequestsCountFn(
|
||||
"damage_expert",
|
||||
client,
|
||||
);
|
||||
return { blame, claim };
|
||||
}
|
||||
/** Same aggregates as `GET /reports/report/requests` (native status keys). */
|
||||
async getAllCheckedRequestsCount(actor: any, client: string | undefined) {
|
||||
return this.getAllRequestsReportCount(actor, client);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user