forked from Yara724/api
Merge pull request 'YARA-720, Yara-721 and others' (#1) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#1
This commit is contained in:
@@ -263,15 +263,31 @@ 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.
|
||||
// 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
|
||||
if (request.expertResendReply) {
|
||||
@@ -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.",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
private isRequestLocked(request: any): boolean {
|
||||
return (
|
||||
request.lockFile && request.claimStatus === ReqClaimStatus.ReviewRequest
|
||||
);
|
||||
// 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, 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",
|
||||
|
||||
@@ -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<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 ---
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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<any[]> {
|
||||
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<string, number> = { all: 0 };
|
||||
@@ -115,6 +204,40 @@ export class ReportsService {
|
||||
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,
|
||||
@@ -126,6 +249,7 @@ export class ReportsService {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user