Merge pull request 'YARA-1135' (#227) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#227
This commit is contained in:
2026-07-28 16:44:09 +03:30
6 changed files with 119 additions and 12 deletions

View File

@@ -265,6 +265,21 @@ export class ClaimCase {
@Prop({ type: Types.ObjectId, index: true }) @Prop({ type: Types.ObjectId, index: true })
fileMakerApprovalActorId?: Types.ObjectId; 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. * Legacy fields kept optional to simplify progressive migration.
* If you choose to migrate later, we can remove these. * If you choose to migrate later, we can remove these.

View File

@@ -77,6 +77,19 @@ export class ClaimListItemV2Dto {
example: true, example: true,
}) })
requiresFileMakerApproval?: boolean; requiresFileMakerApproval?: boolean;
@ApiPropertyOptional({
description:
"V5 only. Number of times the FileMaker has rejected this claim (02). 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 { export class GetClaimListV2ResponseDto {

View File

@@ -22,4 +22,5 @@ export class BusinessRuleException extends HttpException {
export const BusinessErrorCode = { export const BusinessErrorCode = {
DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED: "DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED", DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED: "DAMAGE_EXPERT_RESEND_LIMIT_EXCEEDED",
FILE_MAKER_REJECTION_LIMIT_EXCEEDED: "FILE_MAKER_REJECTION_LIMIT_EXCEEDED",
} as const; } as const;

View File

@@ -4257,6 +4257,9 @@ export class ExpertClaimService {
(!!blame?.isMadeByFileMaker && (!!blame?.isMadeByFileMaker &&
String(blame?.status ?? "") === "WAITING_FOR_EXPERT"), String(blame?.status ?? "") === "WAITING_FOR_EXPERT"),
assignedToMe: !!assignedToMe, assignedToMe: !!assignedToMe,
requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval,
fileMakerRejectionCount: (c as any).fileMakerRejectionCount ?? 0,
fileMakerRejectionReason: (c as any).fileMakerRejectionReason ?? undefined,
}; };
}) as ClaimListItemV2Dto[]; }) as ClaimListItemV2Dto[];
@@ -4330,6 +4333,8 @@ export class ExpertClaimService {
createdAt: c.createdAt, createdAt: c.createdAt,
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval, requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval,
fileMakerRejectionCount: (c as any).fileMakerRejectionCount ?? 0,
fileMakerRejectionReason: (c as any).fileMakerRejectionReason ?? undefined,
needsFileReviewerCompletion: needsFileReviewerCompletion:
String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER" || String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER" ||
(!!blame?.isMadeByFileMaker && (!!blame?.isMadeByFileMaker &&

View File

@@ -10,6 +10,7 @@ import {
ApiBody, ApiBody,
ApiOperation, ApiOperation,
ApiParam, ApiParam,
ApiResponse,
ApiTags, ApiTags,
} from "@nestjs/swagger"; } from "@nestjs/swagger";
import { IsOptional, IsString } from "class-validator"; import { IsOptional, IsString } from "class-validator";
@@ -91,9 +92,28 @@ export class FileMakerClaimApprovalV5Controller {
@ApiOperation({ @ApiOperation({
summary: "Reject the completed claim back to FileReviewer (FileMaker V5)", summary: "Reject the completed claim back to FileReviewer (FileMaker V5)",
description: description:
"Rejects a claim that is in WAITING_FOR_FILE_MAKER_APPROVAL status. " + "Rejects a claim that is in `WAITING_FOR_FILE_MAKER_APPROVAL` status. " +
"Moves claim to FILE_MAKER_REJECTED so the FileReviewer can re-lock, " + "Moves the claim back to `WAITING_FOR_DAMAGE_EXPERT` and the linked blame back to " +
"adjust the damage assessment, and restart user interaction as needed.", "`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( async reject(
@Param("claimRequestId") claimRequestId: string, @Param("claimRequestId") claimRequestId: string,

View File

@@ -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 { WorkflowStepModel } from "src/workflow-step-management/entities/schema/workflow-step.schema";
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum"; import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.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 { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
import { import {
ExpertFileActivityType, ExpertFileActivityType,
@@ -10360,7 +10361,17 @@ export class RequestManagementService {
this.assertFileMakerApprovalAccess(claim, fileMaker); 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( throw new BadRequestException(
`Claim must be in WAITING_FOR_FILE_MAKER_APPROVAL status to approve. Current: ${claim.status}`, `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 * Allowed at most 2 times per claim lifetime. On the 3rd attempt the endpoint
* so the FileReviewer can re-lock it and adjust the damage assessment or * returns 422 FILE_MAKER_REJECTION_LIMIT_EXCEEDED and the FileMaker must approve.
* restart the back-and-forth with the user. *
* 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( async fileMakerRejectV5(
fileMaker: any, fileMaker: any,
@@ -10418,6 +10432,7 @@ export class RequestManagementService {
claimRequestId: string; claimRequestId: string;
publicId: string; publicId: string;
status: string; status: string;
fileMakerRejectionCount: number;
message: string; message: string;
}> { }> {
const claim = await this.claimCaseDbService.findById(claimRequestId); 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(); const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim();
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { $set: {
status: ClaimCaseStatus.FILE_MAKER_REJECTED, status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: ClaimStatus.NEEDS_REVISION, claimStatus: ClaimStatus.NEEDS_REVISION,
fileMakerRejectionCount: newRejectionCount,
fileMakerRejectionReason: reason ?? null,
"workflow.currentStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT, "workflow.currentStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT, "workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
"workflow.locked": false,
}, },
$push: { $push: {
history: { history: {
@@ -10449,17 +10476,43 @@ export class RequestManagementService {
actorType: "file_maker", actorType: "file_maker",
}, },
timestamp: new Date(), 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 { return {
claimRequestId, claimRequestId,
publicId: claim.publicId, publicId: claim.publicId,
status: ClaimCaseStatus.FILE_MAKER_REJECTED, status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
fileMakerRejectionCount: newRejectionCount,
message: 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.",
}; };
} }