diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index eed9b1a..3a66091 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -263,14 +263,30 @@ export class ExpertBlameService { } // 2. Initial validation to ensure the expert has access - if (String(request?.actorLocked?.actorId) === actorId && request.lockFile) { + // Check if locked by current expert and lock is still active + const isLockedByCurrentExpert = + String(request?.actorLocked?.actorId) === actorId && request.lockFile; + + // Check if lock has expired + let isLockExpired = false; + if (request.unlockTime) { + const unlockTime = new Date(request.unlockTime).getTime(); + const now = Date.now(); + isLockExpired = now >= unlockTime; + } + + if (isLockedByCurrentExpert && !isLockExpired) { // This is the correct expert, and the file is locked to them, which is fine. + // They can access it even if they closed the browser and came back. } else if ( - request.lockFile || + (request.lockFile && !isLockExpired) || request.blameStatus === ReqBlameStatus.ReviewRequest ) { - // The file is locked, but not by the current expert. - throw new BadRequestException("Request is locked by another expert"); + // The file is locked by someone else, or lock expired but status hasn't updated yet + // Only block if lock is still active and not by current expert + if (request.lockFile && !isLockExpired && !isLockedByCurrentExpert) { + throw new BadRequestException("Request is locked by another expert"); + } } // 3. Populate the resend links if the data exists @@ -474,9 +490,18 @@ export class ExpertBlameService { "Access denied to this request. You are not the locked expert.", ); } - if (request.unlockTime == null) { + + // Check if lock has expired (unlockTime has passed) + if (request.unlockTime) { + const unlockTime = new Date(request.unlockTime).getTime(); + const now = Date.now(); + if (now >= unlockTime) { + throw new ForbiddenException("Your lock time has expired."); + } + } else if (request.unlockTime == null) { throw new ForbiddenException("Your lock time has expired."); } + if (!request.lockFile) { throw new ForbiddenException( "You must lock the request before submitting a reply.", diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 956f35a..f12700b 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -818,7 +818,8 @@ export class ExpertClaimService { return new ClaimPerIdRs(requestUpdated); } - if (this.isRequestLocked(requestUpdated)) { + // Check if locked by someone else (allow if locked by current user) + if (this.isRequestLocked(requestUpdated, currentUser)) { throw new HttpException( { responseCode: 1007, message: "Request is locked" }, HttpStatus.FORBIDDEN, @@ -846,7 +847,7 @@ export class ExpertClaimService { if (this.isCurrentUserAllowed(request, currentUser)) { return (await this.claimRequestManagementDbService.findOne(requestId)) .imageRequired; - } else if (this.isRequestLocked(requestId)) { + } else if (this.isRequestLocked(request, currentUser)) { throw new HttpException( { responseCode: 1007, message: "Request is locked" }, HttpStatus.FORBIDDEN, @@ -889,8 +890,8 @@ export class ExpertClaimService { ); } - // Perform authorization checks - if (this.isRequestLocked(request) && !this.isCurrentUserAllowed(request, currentUser)) { + // Perform authorization checks - allow if locked by current user + if (this.isRequestLocked(request, currentUser)) { throw new HttpException( { responseCode: 1007, message: "Request is locked" }, HttpStatus.FORBIDDEN, @@ -933,16 +934,54 @@ export class ExpertClaimService { } private isCurrentUserAllowed(request: any, currentUser: any): boolean { - return ( + // Check if locked by current user and lock is still active + const isLockedByCurrentUser = String(request?.actorLocked?.actorId) === currentUser.sub && - request.lockFile - ); + request.lockFile; + + if (!isLockedByCurrentUser) { + return false; + } + + // Also check if lock has expired (unlockTime has passed) + if (request.unlockTime) { + const unlockTime = new Date(request.unlockTime).getTime(); + const now = Date.now(); + if (now >= unlockTime) { + // Lock has expired, but still allow access if they were the one who locked it + // The unlockApi will handle the cleanup + return true; + } + } + + return true; } - private isRequestLocked(request: any): boolean { - return ( - request.lockFile && request.claimStatus === ReqClaimStatus.ReviewRequest - ); + private isRequestLocked(request: any, currentUser?: any): boolean { + if (!request.lockFile || request.claimStatus !== ReqClaimStatus.ReviewRequest) { + return false; + } + + // Check if lock has expired + if (request.unlockTime) { + const unlockTime = new Date(request.unlockTime).getTime(); + const now = Date.now(); + if (now >= unlockTime) { + // Lock has expired, treat as not locked + return false; + } + } + + // If currentUser is provided, allow access if they are the one who locked it + if (currentUser) { + const isLockedByCurrentUser = + String(request?.actorLocked?.actorId) === currentUser.sub; + // Return false (not locked) if locked by current user, true if locked by someone else + return !isLockedByCurrentUser; + } + + // If no currentUser provided, treat as locked + return true; } async submitReplyRequest( @@ -963,9 +1002,18 @@ export class ExpertClaimService { ) { throw new ForbiddenException("Access denied to this request"); } - if (request?.unlockTime == null && !request?.objection) { + + // Check if lock has expired (unlockTime has passed) + if (request?.unlockTime && !request?.objection) { + const unlockTime = new Date(request.unlockTime).getTime(); + const now = Date.now(); + if (now >= unlockTime) { + throw new ForbiddenException("Your time has expired"); + } + } else if (request?.unlockTime == null && !request?.objection) { throw new ForbiddenException("Your time has expired"); } + if (!request.lockFile && !request?.objection) { throw new ForbiddenException( "For submit reply you must lock the request", diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index 12d8e99..9eb1b4d 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -9,6 +9,7 @@ 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 { CreateBranchDto } from "src/client/dto/create-branch.dto"; import { BranchDbService } from "src/client/entities/db-service/branch.db.service"; @@ -44,6 +45,7 @@ export class ExpertInsurerService { private readonly claimVideoCaptureDbService: VideoCaptureDbService, private readonly branchDbService: BranchDbService, private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService, + private readonly damageImageDbService: DamageImageDbService, ) {} async retrieveAllExpertsOfClient( @@ -189,7 +191,7 @@ export class ExpertInsurerService { : this.requestManagementModel; const expertCollection = role === "claim" ? "damage-expert" : "expert"; - return await model.aggregate([ + const results = await model.aggregate([ { $match: { "actorLocked.actorId": expertObjectId } }, { $lookup: { @@ -232,6 +234,7 @@ export class ExpertInsurerService { currentStep: 1, rating: 1, averageRating: 1, + imageRequired: 1, expertInfo: { _id: 1, fullName: { @@ -244,6 +247,15 @@ export class ExpertInsurerService { }, }, ]); + + // Process imageRequired for claim files + if (role === "claim") { + return await Promise.all( + results.map((file) => this.processImageRequired(file)), + ); + } + + return results; } async rateExpertOnFile( @@ -334,6 +346,7 @@ export class ExpertInsurerService { nationalCodeOfInsurer: 1, carGreenCard: 1, aiImages: 1, + imageRequired: 1, videoCaptureId: 1, damageExpertReply: 1, damageExpertReplyFinal: 1, @@ -353,7 +366,10 @@ export class ExpertInsurerService { blameFilesRaw.map((file) => this.populateBlameFileLinks(file)), ); const populatedClaimFiles = await Promise.all( - claimFilesRaw.map((file) => this.populateClaimFileLinks(file)), + claimFilesRaw.map(async (file) => { + const populated = await this.populateClaimFileLinks(file); + return await this.processImageRequired(populated); + }), ); return { blameFiles: populatedBlameFiles, claimFiles: populatedClaimFiles }; @@ -442,6 +458,99 @@ export class ExpertInsurerService { 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 { + 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 --- diff --git a/src/reports/reports.module.ts b/src/reports/reports.module.ts index 4f4b5db..0fb42c3 100644 --- a/src/reports/reports.module.ts +++ b/src/reports/reports.module.ts @@ -1,11 +1,16 @@ 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"; @Module({ - imports: [RequestManagementModule, ClaimRequestManagementModule], + imports: [ + RequestManagementModule, + ClaimRequestManagementModule, + ClientModule, + ], controllers: [ReportsController], providers: [ReportsService], }) diff --git a/src/reports/reports.service.ts b/src/reports/reports.service.ts index decfbb2..131559b 100644 --- a/src/reports/reports.service.ts +++ b/src/reports/reports.service.ts @@ -1,9 +1,11 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } 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 { CompanyAllRequestsCountReportDtoRs, DamageExpertAllRequestsCountReportDtoRs, @@ -12,9 +14,12 @@ import { @Injectable() export class ReportsService { + private readonly logger = new Logger(ReportsService.name); + constructor( private readonly requestManagementDbService: RequestManagementDbService, private readonly claimRequestManagementDbService: ClaimRequestManagementDbService, + private readonly clientDbService: ClientDbService, ) {} async getAllCheckedRequestsCountFn(role: string, client: string) { @@ -81,7 +86,91 @@ export class ReportsService { return data; } - async getAllRequestsCountByRole(role: string, client: string) { + 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, + ); + + 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 { + const filteredRequests = []; + + 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); + } + } + } + + return filteredRequests; + } + + async getAllRequestsCountByRole(role: string, client: string, actor?: any) { if (role === "expert") { const statuses = Object.values(ReqBlameStatus); const data: Record = { all: 0 }; @@ -115,16 +204,51 @@ export class ReportsService { const statuses = Object.values(ReqClaimStatus); const data: Record = { all: 0 }; - for (const status of statuses) { - const filter = { - claimStatus: status, - userClientKey: new Types.ObjectId(client), - }; + // 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, + ]; - const count = - await this.claimRequestManagementDbService.countByFilter(filter); - data[status] = count; - data.all += count; + // 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; @@ -174,7 +298,12 @@ export class ReportsService { async getAllRequestsReportCount(actor, client) { if (actor.role === "damage_expert") { - const data = await this.getAllRequestsCountByRole(actor.role, client); + // 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);