1
0
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:
2026-01-26 12:16:49 +03:30
5 changed files with 348 additions and 32 deletions

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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 ---

View File

@@ -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],
})

View File

@@ -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);