merge upstream

This commit is contained in:
2026-08-09 10:24:25 +03:30
3 changed files with 76 additions and 7 deletions

View File

@@ -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 ( if (
!isFileReviewerReselect &&
claimCase.damage?.selectedParts && claimCase.damage?.selectedParts &&
claimCase.damage.selectedParts.length > 0 claimCase.damage.selectedParts.length > 0
) { ) {
@@ -11161,10 +11167,21 @@ export class ClaimRequestManagementService {
// USER_SUBMISSION_COMPLETE: can occur when documents were uploaded via a // 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 // non-V3 path (e.g. generic v2 endpoint). Safe to advance as long as
// accident fields are set (checked just below). // 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 = const canAdvance =
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ||
step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE || step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE ||
claimCase.status === ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER; claimCase.status === ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER ||
isFileReviewerReentry;
if (!canAdvance) { if (!canAdvance) {
throw new BadRequestException( throw new BadRequestException(
@@ -11574,9 +11591,19 @@ export class ClaimRequestManagementService {
throw new BadRequestException("Video file is required."); throw new BadRequestException("Video file is required.");
} }
if (claimCase.media?.videoCaptureId) { if (claimCase.media?.videoCaptureId) {
throw new ConflictException( // FILE_REVIEWER re-reviewing after a FileMaker rejection: allow replacing
"A walk-around video has already been uploaded for this claim.", // 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 ( if (

View File

@@ -104,7 +104,7 @@ export class FileMakerClaimApprovalV5Controller {
example: { example: {
errorCode: "FILE_MAKER_REJECTION_LIMIT_EXCEEDED", errorCode: "FILE_MAKER_REJECTION_LIMIT_EXCEEDED",
message: message:
"This claim has already been rejected twice. You must approve it now — no further rejections are allowed.", "این پرونده قبلاً دو بار رد شده است. امکان رد مجدد وجود ندارد — باید پرونده را تأیید کنید.",
}, },
}, },
}) })

View File

@@ -10608,7 +10608,7 @@ export class RequestManagementService {
if (currentRejections >= 2) { if (currentRejections >= 2) {
throw new BusinessRuleException( throw new BusinessRuleException(
BusinessErrorCode.FILE_MAKER_REJECTION_LIMIT_EXCEEDED, 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]; const firstParty = req.parties[firstIdx];
await this.runPartyInquiriesV3Internal(req, dto, PartyRole.FIRST, firstParty); 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 // Do NOT call markPartyInquiriesCompleteOnBlame here: that helper is for V3/V4/V5
// FileMaker flows and would advance the workflow to FIRST_LOCATION prematurely. // 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. // 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); const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
if (firstIdx === -1) throw new BadRequestException("First party not found on request"); 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); const userId = await this.getOrCreateUserByPhoneNumber(phone);
if (!req.parties[firstIdx].person) req.parties[firstIdx].person = {} as any; if (!req.parties[firstIdx].person) req.parties[firstIdx].person = {} as any;
req.parties[firstIdx].person.phoneNumber = normalizeIranMobile(phone) ?? phone; req.parties[firstIdx].person.phoneNumber = normalizeIranMobile(phone) ?? phone;