From 637cbb20d5de15401c2a12526b58007c26f5c951 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 29 Jul 2026 10:16:18 +0330 Subject: [PATCH 1/2] Fix v5 lock and re-submit mechanism --- src/expert-claim/expert-claim.service.ts | 69 +++++++++++++++--------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 6bd425a..c5f80d5 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -2671,32 +2671,39 @@ export class ExpertClaimService { if (!reviewerBlame) { throw new NotFoundException("Linked blame file not found."); } - const blameStatus = (reviewerBlame as any).status as string; - if (blameStatus === "WAITING_FOR_FILE_REVIEWER") { - // Phase 1: blame-assign path - return this.assignFileReviewerToV4Blame(claimRequestId, claim, actor); - } - // Phase 2: blame is past the initial assignment step. - // Only the reviewer who was assigned during Phase 1 may proceed. const assignedReviewerId = (reviewerBlame as any).assignedFileReviewerId ? String((reviewerBlame as any).assignedFileReviewerId) : null; - if (!assignedReviewerId) { - throw new BadRequestException({ - success: false, - status: "unavailable" satisfies ExpertFileAssignStatus, - message: "No reviewer has been assigned to this file yet.", - }); + const blameStatus = (reviewerBlame as any).status as string; + + if (blameStatus === "WAITING_FOR_FILE_REVIEWER") { + if (assignedReviewerId && assignedReviewerId === actor.sub) { + // Reviewer already assigned (e.g. after a FileMaker rejection that reset + // blame back to WAITING_FOR_FILE_REVIEWER) — skip Phase 1 and fall + // through to the damage-expert workflow lock below. + } else { + // Phase 1: first-time blame assignment + return this.assignFileReviewerToV4Blame(claimRequestId, claim, actor); + } + } else { + // Phase 2: blame is past the initial assignment step. + // Only the reviewer who was assigned during Phase 1 may proceed. + if (!assignedReviewerId) { + throw new BadRequestException({ + success: false, + status: "unavailable" satisfies ExpertFileAssignStatus, + message: "No reviewer has been assigned to this file yet.", + }); + } + if (assignedReviewerId !== actor.sub) { + throw new ConflictException({ + success: false, + status: "locked" satisfies ExpertFileAssignStatus, + message: "This file is assigned to another reviewer.", + }); + } } - if (assignedReviewerId !== actor.sub) { - throw new ConflictException({ - success: false, - status: "locked" satisfies ExpertFileAssignStatus, - message: "This file is assigned to another reviewer.", - }); - } - // Assigned reviewer — fall through to the damage-expert workflow lock below. - // (actor.role stays FILE_REVIEWER; assertExpertActorOnClaim checks clientKey scope) + // Fall through to the damage-expert workflow lock below. } await this.assertExpertActorOnClaim(claim, actor); @@ -2777,10 +2784,16 @@ export class ExpertClaimService { const now = new Date(); const lockSnapshot = await this.snapshotDamageExpert(actor.sub); const expiredAt = new Date(now.getTime() + EXPERT_WORKFLOW_LOCK_TTL_MS); - const actorType = isFieldExpertOwner ? "field_expert" : "damage_expert"; + const actorType = isFieldExpertOwner + ? "field_expert" + : (actor as any).role === RoleEnum.FILE_REVIEWER + ? "file_reviewer" + : "damage_expert"; const actorRoleLabel = isFieldExpertOwner ? ("field_expert" as const) - : ("damage_expert" as const); + : (actor as any).role === RoleEnum.FILE_REVIEWER + ? ("file_reviewer" as const) + : ("damage_expert" as const); const lockedByPayload = { actorId: expertOid, @@ -2834,7 +2847,13 @@ export class ExpertClaimService { }, }; - if (!claim.workflow?.assignedForReviewBy) { + // Update assignedForReviewBy whenever a FILE_REVIEWER re-locks after a + // FileMaker rejection — the stale payload (from a prior damage-expert cycle) + // must be replaced with the actual reviewer so submit checks pass. + if ( + !claim.workflow?.assignedForReviewBy || + (actor as any).role === RoleEnum.FILE_REVIEWER + ) { baseLockUpdate["workflow.assignedForReviewBy"] = lockedByPayload; } From b0c7a0890cd011700e4122dad4cdf4bb8868505d Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Wed, 29 Jul 2026 10:51:42 +0330 Subject: [PATCH 2/2] Fixed flow steps of v5 --- src/expert-claim/expert-claim.service.ts | 21 ++++++++-- ...file-maker-claim-approval-v5.controller.ts | 17 +++----- .../request-management.service.ts | 39 +++++++++++++++---- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index c5f80d5..83810d6 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -971,7 +971,7 @@ export class ExpertClaimService { daghi: { option: part.daghi.option, ...(part.daghi.price && { price: part.daghi.price }), - ...(part.daghi.branchId && { + ...(part.daghi.branchId && Types.ObjectId.isValid(part.daghi.branchId) && { branchId: new Types.ObjectId(part.daghi.branchId), }), }, @@ -3382,6 +3382,12 @@ export class ExpertClaimService { const nextCaseStatus = claimCaseStatusAfterExpertReplyV2(processedParts); let nextClaimStatus = ClaimStatus.APPROVED; + // V5 flow: FileMaker must approve before the owner is asked to sign. + // When no factor upload is needed, skip the owner-sign step entirely here + // and park the claim at WAITING_FOR_FILE_MAKER_APPROVAL. The owner SMS + // and INSURER_REVIEW status are emitted by fileMakerApproveV5 instead. + const isV5Claim = !!(claim as any).requiresFileMakerApproval; + if (needsFactorUpload) { nextClaimStatus = ClaimStatus.NEEDS_REVISION; if (mixedFactorAndPrice) { @@ -3391,6 +3397,10 @@ export class ExpertClaimService { currentStep = ClaimWorkflowStep.OWNER_UPLOAD_FACTOR_DOCUMENTS; nextWorkflowStep = ClaimWorkflowStep.EXPERT_COST_EVALUATION; } + } else if (isV5Claim) { + // V5, no factors: hold at WAITING_FOR_FILE_MAKER_APPROVAL + currentStep = ClaimWorkflowStep.INSURER_REVIEW; + nextWorkflowStep = ClaimWorkflowStep.CLAIM_COMPLETED; } const mergedSelectedParts = [...ownerSelectedParts]; @@ -3408,7 +3418,9 @@ export class ExpertClaimService { } const updatePayload: Record = { - status: nextCaseStatus, + status: isV5Claim && !needsFactorUpload + ? ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL + : nextCaseStatus, claimStatus: nextClaimStatus, ...(expertAddedParts.length > 0 ? { "damage.selectedParts": mergedSelectedParts } @@ -3464,8 +3476,9 @@ export class ExpertClaimService { idempotencyKey: `claim:${claimRequestId}:handled:${actor.sub}`, }); + // V5: owner SMS is sent by fileMakerApproveV5 after FileMaker approval. const ownerPhoneNotify = await this.resolveClaimOwnerPhone(claim); - if (ownerPhoneNotify && !needsFactorUpload) { + if (ownerPhoneNotify && !needsFactorUpload && !isV5Claim) { const expertLastName = actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس"; await this.smsOrchestrationService.sendSignatureReviewNotice({ @@ -3473,7 +3486,7 @@ export class ExpertClaimService { fileKind: "claim", publicId: claim.publicId, expertLastName, - link: this.smsOrchestrationService.buildClaimLink(String(claim._id), "v1"), + link: this.smsOrchestrationService.buildClaimLink(String(claim._id), "v2"), }); } 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 c482124..0ef7882 100644 --- a/src/request-management/file-maker-claim-approval-v5.controller.ts +++ b/src/request-management/file-maker-claim-approval-v5.controller.ts @@ -64,26 +64,19 @@ export class FileMakerClaimApprovalV5Controller { @ApiOperation({ summary: "Approve the completed claim (FileMaker V5)", description: - "Approves a claim that is in WAITING_FOR_FILE_MAKER_APPROVAL status. " + - "Marks the claim COMPLETED and triggers fanavaran submission.", + "Approves a claim that is in `WAITING_FOR_FILE_MAKER_APPROVAL` status. " + + "Moves the claim to `INSURER_REVIEW_AWAITING_OWNER_SIGN` and sends the owner an SMS " + + "with a signature link. Fanavaran submission is triggered automatically after the " + + "owner signs.", }) async approve( @Param("claimRequestId") claimRequestId: string, @CurrentUser() fileMaker: any, ) { - const result = await this.requestManagementService.fileMakerApproveV5( + return this.requestManagementService.fileMakerApproveV5( fileMaker, claimRequestId, ); - - // Trigger fanavaran auto-submit now that the claim is COMPLETED and the gate - // (requiresFileMakerApproval) has been cleared. - const fanavaran = - await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted( - claimRequestId, - ); - - return { ...result, fanavaran }; } @Post("reject/:claimRequestId") diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 23005a7..1be3687 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -10389,28 +10389,53 @@ export class RequestManagementService { metadata: {}, }; - // Move claim to COMPLETED and clear the approval gate + // V5 approval: move claim to INSURER_REVIEW_AWAITING_OWNER_SIGN so the + // owner can now sign. Clear requiresFileMakerApproval so that when the + // owner later signs and autoSubmitToFanavaranV2OnClaimCompleted runs, it + // does not re-intercept the claim as a pending V5 gate. await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { $set: { - status: ClaimCaseStatus.COMPLETED, + status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN, claimStatus: ClaimStatus.APPROVED, requiresFileMakerApproval: false, - "workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED, + "workflow.currentStep": ClaimWorkflowStep.INSURER_REVIEW, "workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED, }, $push: { - "workflow.completedSteps": ClaimWorkflowStep.INSURER_REVIEW, history: historyEntry, }, }); + // Notify the owner that the claim is ready for their signature. + const notifyUserId = (claim as any).damagedPartyUserId ?? (claim as any).owner?.userId; + if (notifyUserId && claim.blameRequestId) { + const blame = await this.blameRequestDbService.findById( + String(claim.blameRequestId), + ); + const ownerParty = (blame?.parties || []).find( + (p: any) => + p?.person?.userId && String(p.person.userId) === String(notifyUserId), + ); + const ownerPhone = ownerParty?.person?.phoneNumber + ?? (await this.userDbService.findOne({ _id: new Types.ObjectId(String(notifyUserId)) }))?.mobile; + if (ownerPhone && typeof ownerPhone === "string") { + await this.smsOrchestrationService.sendSignatureReviewNotice({ + receptor: ownerPhone, + fileKind: "claim", + publicId: claim.publicId, + expertLastName: actorName.split(/\s+/).pop() || "کارشناس", + link: this.smsOrchestrationService.buildClaimLink(claimRequestId, "v2"), + }); + } + } + return { claimRequestId, publicId: claim.publicId, - status: ClaimCaseStatus.COMPLETED, + status: ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN, message: - "Claim approved by FileMaker. Claim is now marked COMPLETED. " + - "Proceed with fanavaran submission via the claim service.", + "Claim approved by FileMaker. Owner has been notified to sign. " + + "Once the owner signs, the claim will be completed and submitted to Fanavaran.", }; }