diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 45885ea..db11302 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -8308,8 +8308,14 @@ export class ClaimRequestManagementService { ); } - // 4. Validate outer parts haven't been selected yet + // 4. Validate outer parts haven't been selected yet. + // For FILE_REVIEWER re-reviewing after a FileMaker rejection, the existing + // selection from the prior cycle is intentionally overwritten — skip this + // guard so the reviewer can update the damaged parts list. + const isFileReviewerReselect = + actor?.role === RoleEnum.FILE_REVIEWER; if ( + !isFileReviewerReselect && claimCase.damage?.selectedParts && claimCase.damage.selectedParts.length > 0 ) { @@ -11161,10 +11167,21 @@ export class ClaimRequestManagementService { // USER_SUBMISSION_COMPLETE: can occur when documents were uploaded via a // non-V3 path (e.g. generic v2 endpoint). Safe to advance as long as // accident fields are set (checked just below). + // EXPERT_REVIEWING + EXPERT_DAMAGE_ASSESSMENT: FileReviewer re-entering + // after a FileMaker rejection. The claim was locked back to + // EXPERT_REVIEWING by the reviewer's lock call; the document phase was + // already completed in the prior cycle so it's safe to advance directly + // to part selection. + const isFileReviewerReentry = + claimCase.status === ClaimCaseStatus.EXPERT_REVIEWING && + step === ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT && + (blame as any)?.isMadeByFileMaker === true; + const canAdvance = step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE || - claimCase.status === ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER; + claimCase.status === ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER || + isFileReviewerReentry; if (!canAdvance) { throw new BadRequestException( @@ -11574,9 +11591,19 @@ export class ClaimRequestManagementService { throw new BadRequestException("Video file is required."); } if (claimCase.media?.videoCaptureId) { - throw new ConflictException( - "A walk-around video has already been uploaded for this claim.", - ); + // FILE_REVIEWER re-reviewing after a FileMaker rejection: allow replacing + // the previous walk-around video by clearing the stale id first so that + // setVideoCaptureV2's own conflict check doesn't block the upload. + if (actor?.role === RoleEnum.FILE_REVIEWER) { + await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { + $unset: { "media.videoCaptureId": "" }, + }); + claimCase.media = { ...(claimCase.media ?? {}), videoCaptureId: undefined } as any; + } else { + throw new ConflictException( + "A walk-around video has already been uploaded for this claim.", + ); + } } if ( 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 0ef7882..b186981 100644 --- a/src/request-management/file-maker-claim-approval-v5.controller.ts +++ b/src/request-management/file-maker-claim-approval-v5.controller.ts @@ -104,7 +104,7 @@ export class FileMakerClaimApprovalV5Controller { 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.", + "این پرونده قبلاً دو بار رد شده است. امکان رد مجدد وجود ندارد — باید پرونده را تأیید کنید.", }, }, }) diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 5c8614e..1e8e2be 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -10608,7 +10608,7 @@ export class RequestManagementService { 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.", + "این پرونده قبلاً دو بار رد شده است. امکان رد مجدد وجود ندارد — باید پرونده را تأیید کنید.", ); } @@ -10811,6 +10811,25 @@ export class RequestManagementService { const firstParty = req.parties[firstIdx]; await this.runPartyInquiriesV3Internal(req, dto, PartyRole.FIRST, firstParty); + + // V6 call-center guard: the guilty party's insurance must belong to THIS + // deployment's insurer company. Reject the file early so the agent cannot + // initiate a blame flow for a third-party insurance holder. + const resolvedClientId = req.parties[firstIdx]?.person?.clientId; + if (resolvedClientId && process.env.CLIENT_ID) { + const resolvedClient = await this.clientService.findOne({ + _id: new Types.ObjectId(String(resolvedClientId)), + }); + if ( + resolvedClient && + String(resolvedClient.clientCode) !== String(process.env.CLIENT_ID) + ) { + throw new ForbiddenException( + "بیمه‌نامه طرف مقصر متعلق به شرکت بیمه این سامانه نیست. لینک تقصیر فقط برای بیمه‌گذاران همین شرکت قابل ارسال است.", + ); + } + } + // Do NOT call markPartyInquiriesCompleteOnBlame here: that helper is for V3/V4/V5 // FileMaker flows and would advance the workflow to FIRST_LOCATION prematurely. // In V6 the workflow must stay at CREATED until send-link advances it to FIRST_VIDEO. @@ -10891,6 +10910,29 @@ export class RequestManagementService { const firstIdx = this.getPartyIndex(req, PartyRole.FIRST); if (firstIdx === -1) throw new BadRequestException("First party not found on request"); + + // Safeguard: verify that run-inquiry was completed and that the resolved + // insurance company belongs to this deployment before proceeding. + const storedClientId = req.parties[firstIdx]?.person?.clientId; + if (!storedClientId) { + throw new BadRequestException( + "استعلام بیمه طرف مقصر انجام نشده است. ابتدا run-inquiry را فراخوانی کنید.", + ); + } + if (process.env.CLIENT_ID) { + const storedClient = await this.clientService.findOne({ + _id: new Types.ObjectId(String(storedClientId)), + }); + if ( + storedClient && + String(storedClient.clientCode) !== String(process.env.CLIENT_ID) + ) { + throw new ForbiddenException( + "بیمه‌نامه طرف مقصر متعلق به شرکت بیمه این سامانه نیست. لینک تقصیر فقط برای بیمه‌گذاران همین شرکت قابل ارسال است.", + ); + } + } + const userId = await this.getOrCreateUserByPhoneNumber(phone); if (!req.parties[firstIdx].person) req.parties[firstIdx].person = {} as any; req.parties[firstIdx].person.phoneNumber = normalizeIranMobile(phone) ?? phone;