From 6be9ff16e18c2aeee943271d4fd7b8db3699b90c Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 1 Jul 2026 16:54:24 +0330 Subject: [PATCH] FIXED FILE REVIEWER GET ALL --- .../caseStatus.enum.ts | 6 + src/expert-blame/dto/all-request.dto.ts | 6 + src/expert-blame/expert-blame.service.ts | 44 +++++++ src/expert-claim/expert-claim.service.ts | 123 +++++++++++++++++- .../request-management.service.ts | 46 +++++-- 5 files changed, 213 insertions(+), 12 deletions(-) diff --git a/src/Types&Enums/blame-request-management/caseStatus.enum.ts b/src/Types&Enums/blame-request-management/caseStatus.enum.ts index 6c665f2..8ad2c3b 100644 --- a/src/Types&Enums/blame-request-management/caseStatus.enum.ts +++ b/src/Types&Enums/blame-request-management/caseStatus.enum.ts @@ -11,6 +11,12 @@ export enum CaseStatus { WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES", + /** + * FileMaker has collected all signatures; the file is sealed and waiting + * for a FileReviewer to complete it (accident fields → capture → video). + */ + WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER", + COMPLETED = "COMPLETED", CANCELLED = "CANCELLED", diff --git a/src/expert-blame/dto/all-request.dto.ts b/src/expert-blame/dto/all-request.dto.ts index 0664bc3..c2fb6a3 100644 --- a/src/expert-blame/dto/all-request.dto.ts +++ b/src/expert-blame/dto/all-request.dto.ts @@ -121,6 +121,12 @@ export class AllRequestDtoV2 { unifiedFileStatus?: string; partiesInitialForms: { firstParty: string; secondParty: string }; partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string }; + /** + * True when the file is in WAITING_FOR_FILE_REVIEWER status — the FileReviewer + * must complete accident fields, capture, and upload-video via v4 endpoints + * before the file enters the normal expert-blame review queue. + */ + needsFileReviewerCompletion?: boolean; } export class AllRequestDtoRsV2 { diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 753f543..8c409db 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -12,6 +12,7 @@ import { assertBlameCaseForExpertTenant, blameCaseAccessibleToExpert, blameCaseInitiatedByFieldExpert, + blameCaseTouchesClient, requireActorClientKey, } from "src/helpers/tenant-scope"; import { @@ -438,6 +439,9 @@ export class ExpertBlameService { if (actor.role === RoleEnum.FIELD_EXPERT) { return this.getFieldExpertBlameListV2(actor, query); } + if (actor.role === RoleEnum.FILE_REVIEWER) { + return this.getFileReviewerBlameListV2(actor, query); + } requireActorClientKey(actor); const expertId = actor.sub; @@ -579,6 +583,44 @@ export class ExpertBlameService { return pagedResult; } + /** + * Blame inbox for FILE_REVIEWER — all expert-initiated IN_PERSON files that + * have been sealed by a FileMaker (WAITING_FOR_FILE_REVIEWER) and belong to + * this reviewer's insurance company (clientKey). + * + * Once the FileReviewer completes their steps the file moves to + * WAITING_FOR_EXPERT (via upload-video), after which it is visible in the + * standard expert-blame panel as usual. + */ + private async getFileReviewerBlameListV2( + actor: { sub: string; clientKey?: string }, + query: ListQueryV2Dto = {}, + ): Promise { + const clientKey = requireActorClientKey(actor); + + const allSealed = (await this.blameRequestDbService.find( + { + expertInitiated: true, + status: { + $in: [ + CaseStatus.WAITING_FOR_FILE_REVIEWER, + CaseStatus.WAITING_FOR_EXPERT, + CaseStatus.COMPLETED, + ], + }, + }, + { lean: true }, + )) as Record[]; + + // Scope to this reviewer's insurance company via blame party clientId + const visibleCases = allSealed.filter((doc) => + blameCaseTouchesClient(doc, clientKey), + ); + + const pagedResult = await this.paginateBlameListV2(visibleCases, query); + return pagedResult; + } + private async paginateBlameListV2( visibleCases: Record[], query: ListQueryV2Dto, @@ -727,6 +769,8 @@ export class ExpertBlameService { secondParty?.vehicle?.inquiry?.mapped?.CarName || "", }, + needsFileReviewerCompletion: + String(doc.status ?? "") === CaseStatus.WAITING_FOR_FILE_REVIEWER, }; } diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 2b1fb7c..84c5d5f 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -58,6 +58,7 @@ import { RoleEnum } from "src/Types&Enums/role.enum"; import { assertClaimCaseForTenant, assertClaimCaseForExpertActor, + blameCaseTouchesClient, claimCaseInitiatedByFieldExpert, claimCaseTouchesClient, requireActorClientKey, @@ -3618,6 +3619,9 @@ export class ExpertClaimService { if (actor.role === RoleEnum.FIELD_EXPERT) { return this.getFieldExpertClaimListV2(actor, query); } + if (actor.role === RoleEnum.FILE_REVIEWER) { + return this.getFileReviewerClaimListV2(actor, query); + } requireActorClientKey(actor); const actorId = actor.sub; const clientKey = actor.clientKey as string; @@ -3885,6 +3889,96 @@ export class ExpertClaimService { return this.paginateClaimListV2(list, query); } + /** + * Claim list for FILE_REVIEWER — returns claims linked to sealed expert-initiated + * IN_PERSON blame files (WAITING_FOR_FILE_REVIEWER, WAITING_FOR_EXPERT, COMPLETED) + * that belong to this reviewer's insurance company. + */ + private async getFileReviewerClaimListV2( + actor: any, + query: ListQueryV2Dto = {}, + ): Promise { + const clientKey = requireActorClientKey(actor); + + // Find all sealed blame files (across statuses the reviewer cares about) + const sealedBlames = (await this.blameRequestDbService.find( + { + expertInitiated: true, + creationMethod: "IN_PERSON", + status: { + $in: [ + "WAITING_FOR_FILE_REVIEWER", + "WAITING_FOR_EXPERT", + "COMPLETED", + ], + }, + }, + { lean: true, select: "_id type parties blameStatus status expert.decision" }, + )) as any[]; + + if (sealedBlames.length === 0) { + return this.paginateClaimListV2([], query); + } + + // Scope to this reviewer's insurer via blame party clientId + const scopedBlameIds = sealedBlames + .filter((b) => blameCaseTouchesClient(b, clientKey)) + .map((b) => b._id); + + if (scopedBlameIds.length === 0) { + return this.paginateClaimListV2([], query); + } + + const claims = (await this.claimCaseDbService.find({ + blameRequestId: { $in: scopedBlameIds }, + })) as any[]; + + const blameById = new Map( + sealedBlames.map((b) => [String(b._id), b]), + ); + + const list = claims.map((c) => { + const blame = c.blameRequestId + ? blameById.get(c.blameRequestId.toString()) + : undefined; + const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById); + const fileCtx = blame ? this.blameFileContextForExpert(blame) : {}; + const lockActive = !!( + c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c) + ); + return { + claimRequestId: c._id.toString(), + publicId: c.publicId, + status: c.status, + unifiedFileStatus: resolveUnifiedFileStatus({ + blameStatus: blame?.status, + claimStatus: c.status, + }), + currentStep: c.workflow?.currentStep || "", + locked: lockActive, + lockedBy: + lockActive && c.workflow?.lockedBy + ? { + actorId: c.workflow.lockedBy.actorId?.toString(), + actorName: c.workflow.lockedBy.actorName, + lockedAt: (c.workflow as any).lockedAt?.toISOString?.(), + expiredAt: (c.workflow as any).expiredAt?.toISOString?.(), + } + : undefined, + vehicle: v + ? { carName: v.carName, carModel: v.carModel, carType: v.carType } + : undefined, + ...fileCtx, + createdAt: c.createdAt, + awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), + needsFileReviewerCompletion: + String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER", + }; + }) as ClaimListItemV2Dto[]; + + return this.paginateClaimListV2(list, query); + } + /** * Load linked blame case for damage-expert claim detail (same enrichment as expert-blame `findOneV2`: * party evidence `videoUrl` / `voiceUrls`, Jalali date strings). @@ -4062,7 +4156,10 @@ export class ExpertClaimService { clientKey?: string; role?: string; }): Promise { - if ((actor as any).role === RoleEnum.FIELD_EXPERT) return; + if ( + (actor as any).role === RoleEnum.FIELD_EXPERT || + (actor as any).role === RoleEnum.FILE_REVIEWER + ) return; const clientKey = requireActorClientKey(actor); const lockTtlMs = EXPERT_WORKFLOW_LOCK_TTL_MS; const now = new Date(); @@ -4242,10 +4339,28 @@ export class ExpertClaimService { const isFactorValidationPending = claimIsAwaitingExpertFactorValidationV2(claim); - // Field experts can always view their own initiated claims regardless of status - if (actor.role === RoleEnum.FIELD_EXPERT) { + // Field experts and FileReviewers can always view IN_PERSON expert-initiated + // claims regardless of claim status (they work across multiple non-standard steps). + if ( + actor.role === RoleEnum.FIELD_EXPERT || + actor.role === RoleEnum.FILE_REVIEWER + ) { if (!claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame)) { - throw new ForbiddenException("This claim was not initiated by you."); + // For FILE_REVIEWER: also accept if the linked blame is sealed for them + const isFileReviewerEligible = + actor.role === RoleEnum.FILE_REVIEWER && + linkedBlame?.expertInitiated && + linkedBlame?.creationMethod === "IN_PERSON" && + [ + "WAITING_FOR_FILE_REVIEWER", + "WAITING_FOR_EXPERT", + "COMPLETED", + ].includes(String(linkedBlame?.status ?? "")); + if (!isFileReviewerEligible) { + throw new ForbiddenException( + "This claim is not accessible to you.", + ); + } } // Fall through to the detail build below (skip the damage-expert gate) } else { diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 49572d0..13493ad 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -4129,6 +4129,28 @@ export class RequestManagementService { * Verify the current user (field expert) can access this BlameRequest (v2). */ private verifyExpertAccessForBlameV2(req: any, expert: any): void { + // FILE_REVIEWER can access any sealed expert-initiated IN_PERSON file + // (status WAITING_FOR_FILE_REVIEWER or beyond) scoped to their clientKey. + if (expert?.role === RoleEnum.FILE_REVIEWER) { + if ( + !req?.expertInitiated || + req?.creationMethod !== CreationMethod.IN_PERSON + ) { + throw new ForbiddenException( + "FileReviewer can only access expert-initiated IN_PERSON files.", + ); + } + if ( + req.status !== CaseStatus.WAITING_FOR_FILE_REVIEWER && + req.status !== CaseStatus.WAITING_FOR_EXPERT && + req.status !== CaseStatus.COMPLETED + ) { + throw new ForbiddenException( + "This file has not been sealed by a FileMaker yet.", + ); + } + return; + } if (req?.expertInitiated && req?.initiatedByFieldExpertId) { if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) { throw new ForbiddenException( @@ -8815,6 +8837,7 @@ export class RequestManagementService { const fresh = await this.blameRequestDbService.findById(requestId); if (fresh) { + const isFileMakerActor = expert?.role === RoleEnum.FILE_MAKER; if (partyRole === PartyRole.FIRST) { this.pushWorkflowSteps(fresh, [ WorkflowStep.FIRST_LOCATION, @@ -8823,14 +8846,17 @@ export class RequestManagementService { WorkflowStep.FIRST_SIGN, WorkflowStep.FIRST_COMPLETED, ]); - fresh.workflow.currentStep = - fresh.type === BlameRequestType.THIRD_PARTY - ? WorkflowStep.FIRST_INVITE_SECOND - : WorkflowStep.FIRST_COMPLETED; - fresh.workflow.nextStep = - fresh.type === BlameRequestType.THIRD_PARTY - ? WorkflowStep.SECOND_INITIAL_FORM - : WorkflowStep.WAITING_FOR_GUILT_DECISION; + if (fresh.type === BlameRequestType.THIRD_PARTY) { + fresh.workflow.currentStep = WorkflowStep.FIRST_INVITE_SECOND; + fresh.workflow.nextStep = WorkflowStep.SECOND_INITIAL_FORM; + } else { + // CAR_BODY: FileMaker is done — seal the file for FileReviewer pickup + fresh.workflow.currentStep = WorkflowStep.FIRST_COMPLETED; + fresh.workflow.nextStep = WorkflowStep.WAITING_FOR_GUILT_DECISION; + if (isFileMakerActor) { + fresh.status = CaseStatus.WAITING_FOR_FILE_REVIEWER; + } + } } else { this.pushWorkflowSteps(fresh, [ WorkflowStep.SECOND_LOCATION, @@ -8841,6 +8867,10 @@ export class RequestManagementService { ]); fresh.workflow.currentStep = WorkflowStep.SECOND_COMPLETED; fresh.workflow.nextStep = WorkflowStep.WAITING_FOR_GUILT_DECISION; + // THIRD_PARTY: second party sign is FileMaker's last step — seal for FileReviewer + if (isFileMakerActor) { + fresh.status = CaseStatus.WAITING_FOR_FILE_REVIEWER; + } } await (fresh as any).save(); }