From 891b1b2bbcd1a95e53d75af457450ad94e87cda6 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 28 Jul 2026 16:43:29 +0330 Subject: [PATCH] YARA-1135 --- .../entites/schema/claim-cases.schema.ts | 15 ++++ src/expert-claim/dto/claim-list-v2.dto.ts | 13 ++++ .../exceptions/business-rule.exception.ts | 1 + src/expert-claim/expert-claim.service.ts | 5 ++ ...file-maker-claim-approval-v5.controller.ts | 26 ++++++- .../request-management.service.ts | 71 ++++++++++++++++--- 6 files changed, 119 insertions(+), 12 deletions(-) diff --git a/src/claim-request-management/entites/schema/claim-cases.schema.ts b/src/claim-request-management/entites/schema/claim-cases.schema.ts index 5037f40..8e1c841 100644 --- a/src/claim-request-management/entites/schema/claim-cases.schema.ts +++ b/src/claim-request-management/entites/schema/claim-cases.schema.ts @@ -265,6 +265,21 @@ export class ClaimCase { @Prop({ type: Types.ObjectId, index: true }) fileMakerApprovalActorId?: Types.ObjectId; + /** + * V5 split flow: number of times the FileMaker has rejected this claim. + * Capped at 2; on the 3rd attempt the reject endpoint returns 422 and + * the FileMaker must approve instead. + */ + @Prop({ type: Number, default: 0 }) + fileMakerRejectionCount?: number; + + /** + * V5 split flow: reason text from the most recent FileMaker rejection. + * Overwritten on each rejection. + */ + @Prop({ type: String }) + fileMakerRejectionReason?: string; + /** * Legacy fields kept optional to simplify progressive migration. * If you choose to migrate later, we can remove these. diff --git a/src/expert-claim/dto/claim-list-v2.dto.ts b/src/expert-claim/dto/claim-list-v2.dto.ts index 2c34c68..1d7ab5a 100644 --- a/src/expert-claim/dto/claim-list-v2.dto.ts +++ b/src/expert-claim/dto/claim-list-v2.dto.ts @@ -77,6 +77,19 @@ export class ClaimListItemV2Dto { example: true, }) requiresFileMakerApproval?: boolean; + + @ApiPropertyOptional({ + description: + "V5 only. Number of times the FileMaker has rejected this claim (0–2). When this reaches 2 the FileMaker can only approve.", + example: 1, + }) + fileMakerRejectionCount?: number; + + @ApiPropertyOptional({ + description: "V5 only. Reason text supplied with the most recent FileMaker rejection.", + example: "Damage assessment is incorrect — please re-evaluate parts X and Y", + }) + fileMakerRejectionReason?: string; } export class GetClaimListV2ResponseDto { diff --git a/src/expert-claim/exceptions/business-rule.exception.ts b/src/expert-claim/exceptions/business-rule.exception.ts index 97c7d7a..74e5e2a 100644 --- a/src/expert-claim/exceptions/business-rule.exception.ts +++ b/src/expert-claim/exceptions/business-rule.exception.ts @@ -22,4 +22,5 @@ export class BusinessRuleException extends HttpException { export const BusinessErrorCode = { DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED: "DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED", + FILE_MAKER_REJECTION_LIMIT_EXCEEDED: "FILE_MAKER_REJECTION_LIMIT_EXCEEDED", } as const; diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 2cb4898..bba0580 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -4257,6 +4257,9 @@ export class ExpertClaimService { (!!blame?.isMadeByFileMaker && String(blame?.status ?? "") === "WAITING_FOR_EXPERT"), assignedToMe: !!assignedToMe, + requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval, + fileMakerRejectionCount: (c as any).fileMakerRejectionCount ?? 0, + fileMakerRejectionReason: (c as any).fileMakerRejectionReason ?? undefined, }; }) as ClaimListItemV2Dto[]; @@ -4330,6 +4333,8 @@ export class ExpertClaimService { createdAt: c.createdAt, awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval, + fileMakerRejectionCount: (c as any).fileMakerRejectionCount ?? 0, + fileMakerRejectionReason: (c as any).fileMakerRejectionReason ?? undefined, needsFileReviewerCompletion: String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER" || (!!blame?.isMadeByFileMaker && diff --git a/src/request-management/file-maker-claim-approval-v5.controller.ts b/src/request-management/file-maker-claim-approval-v5.controller.ts index 9183847..c482124 100644 --- a/src/request-management/file-maker-claim-approval-v5.controller.ts +++ b/src/request-management/file-maker-claim-approval-v5.controller.ts @@ -10,6 +10,7 @@ import { ApiBody, ApiOperation, ApiParam, + ApiResponse, ApiTags, } from "@nestjs/swagger"; import { IsOptional, IsString } from "class-validator"; @@ -91,9 +92,28 @@ export class FileMakerClaimApprovalV5Controller { @ApiOperation({ summary: "Reject the completed claim back to FileReviewer (FileMaker V5)", description: - "Rejects a claim that is in WAITING_FOR_FILE_MAKER_APPROVAL status. " + - "Moves claim to FILE_MAKER_REJECTED so the FileReviewer can re-lock, " + - "adjust the damage assessment, and restart user interaction as needed.", + "Rejects a claim that is in `WAITING_FOR_FILE_MAKER_APPROVAL` status. " + + "Moves the claim back to `WAITING_FOR_DAMAGE_EXPERT` and the linked blame back to " + + "`WAITING_FOR_FILE_REVIEWER` so the FileReviewer can re-lock, adjust the damage " + + "assessment, and restart user interaction as needed.\n\n" + + "**Rejection limit:** the FileMaker may reject at most **2 times** per claim. " + + "On the 3rd attempt the endpoint returns **422** with " + + "`errorCode: FILE_MAKER_REJECTION_LIMIT_EXCEEDED` — the FileMaker must approve instead. " + + "The current count is returned as `fileMakerRejectionCount` in the response and in " + + "`GET v2/expert-claim/requests`.", + }) + @ApiResponse({ + status: 422, + description: + "Business rule violation: rejection limit exceeded. " + + "`errorCode: FILE_MAKER_REJECTION_LIMIT_EXCEEDED` — the claim has already been rejected twice.", + schema: { + example: { + errorCode: "FILE_MAKER_REJECTION_LIMIT_EXCEEDED", + message: + "This claim has already been rejected twice. You must approve it now — no further rejections are allowed.", + }, + }, }) async reject( @Param("claimRequestId") claimRequestId: string, diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 1cb1637..20b6175 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -84,6 +84,7 @@ import { WorkflowStepDbService } from "src/workflow-step-management/entities/db- import { WorkflowStepModel } from "src/workflow-step-management/entities/schema/workflow-step.schema"; import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum"; import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum"; +import { BusinessRuleException, BusinessErrorCode } from "src/expert-claim/exceptions/business-rule.exception"; import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service"; import { ExpertFileActivityType, @@ -10360,7 +10361,17 @@ export class RequestManagementService { this.assertFileMakerApprovalAccess(claim, fileMaker); - if (claim.status !== ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL) { + // After 2 rejections the claim is in WAITING_FOR_DAMAGE_EXPERT (forced-approve + // path); any other status is still an error. + const rejectionCount = (claim as any).fileMakerRejectionCount ?? 0; + const forcedApprove = + rejectionCount >= 2 && + claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT; + + if ( + claim.status !== ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL && + !forcedApprove + ) { throw new BadRequestException( `Claim must be in WAITING_FOR_FILE_MAKER_APPROVAL status to approve. Current: ${claim.status}`, ); @@ -10404,11 +10415,14 @@ export class RequestManagementService { } /** - * V5: FileMaker rejects the completed claim back to expert review. + * V5: FileMaker rejects the completed claim back to FileReviewer for correction. * - * Moves claim from WAITING_FOR_FILE_MAKER_APPROVAL → WAITING_FOR_DAMAGE_EXPERT - * so the FileReviewer can re-lock it and adjust the damage assessment or - * restart the back-and-forth with the user. + * Allowed at most 2 times per claim lifetime. On the 3rd attempt the endpoint + * returns 422 FILE_MAKER_REJECTION_LIMIT_EXCEEDED and the FileMaker must approve. + * + * Transition (both blame + claim are updated atomically): + * claim: WAITING_FOR_FILE_MAKER_APPROVAL → WAITING_FOR_DAMAGE_EXPERT + * blame: (any status) → WAITING_FOR_FILE_REVIEWER */ async fileMakerRejectV5( fileMaker: any, @@ -10418,6 +10432,7 @@ export class RequestManagementService { claimRequestId: string; publicId: string; status: string; + fileMakerRejectionCount: number; message: string; }> { const claim = await this.claimCaseDbService.findById(claimRequestId); @@ -10431,14 +10446,26 @@ export class RequestManagementService { ); } + const currentRejections = (claim as any).fileMakerRejectionCount ?? 0; + if (currentRejections >= 2) { + throw new BusinessRuleException( + BusinessErrorCode.FILE_MAKER_REJECTION_LIMIT_EXCEEDED, + "This claim has already been rejected twice. You must approve it now — no further rejections are allowed.", + ); + } + + const newRejectionCount = currentRejections + 1; const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim(); await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { $set: { - status: ClaimCaseStatus.FILE_MAKER_REJECTED, + status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, claimStatus: ClaimStatus.NEEDS_REVISION, + fileMakerRejectionCount: newRejectionCount, + fileMakerRejectionReason: reason ?? null, "workflow.currentStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT, "workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT, + "workflow.locked": false, }, $push: { history: { @@ -10449,17 +10476,43 @@ export class RequestManagementService { actorType: "file_maker", }, timestamp: new Date(), - metadata: { reason: reason ?? null }, + metadata: { reason: reason ?? null, rejectionCount: newRejectionCount }, }, }, }); + // Reset blame back to WAITING_FOR_FILE_REVIEWER so the reviewer's list + // query (which looks for that status + assignedFileReviewerId) surfaces it. + if (claim.blameRequestId) { + await this.blameRequestDbService.findByIdAndUpdate( + String(claim.blameRequestId), + { + $set: { status: CaseStatus.WAITING_FOR_FILE_REVIEWER }, + $push: { + history: { + type: "V5_FILE_MAKER_REJECTED_BLAME_RESET", + actor: { + actorId: new Types.ObjectId(fileMaker.sub), + actorName, + actorType: "file_maker", + }, + timestamp: new Date(), + metadata: { claimRequestId, reason: reason ?? null }, + }, + }, + }, + ); + } + return { claimRequestId, publicId: claim.publicId, - status: ClaimCaseStatus.FILE_MAKER_REJECTED, + status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, + fileMakerRejectionCount: newRejectionCount, message: - "Claim rejected by FileMaker. FileReviewer can re-lock the claim and adjust the damage assessment or restart the user interaction.", + newRejectionCount >= 2 + ? "Claim rejected by FileMaker (2nd rejection — next action must be approval). FileReviewer can re-lock and adjust the assessment." + : "Claim rejected by FileMaker. FileReviewer can re-lock the claim and adjust the damage assessment or restart the user interaction.", }; }