forked from Yara724/api
fix: harden claim review and inquiry workflows
Preserve damage history and current vehicle price, restore depreciation mapping, normalize inquiry/report output, and support resumable expert review with paginated case retrieval.
This commit is contained in:
@@ -5,6 +5,9 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { CreationMethod } from "./entities/schema/request-management.schema";
|
||||
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
|
||||
describe("RequestManagementService V4 FileMaker workflow", () => {
|
||||
it("persists FIRST_INITIAL_FORM after the first party OTP is verified", async () => {
|
||||
@@ -216,4 +219,138 @@ describe("RequestManagementService V4 FileMaker workflow", () => {
|
||||
WorkflowStep.FIRST_VIDEO,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"resumes V4/V5 FileMaker work from the linked claim during partial document upload (approval=%s)",
|
||||
async (requiresFileMakerApproval) => {
|
||||
const fileMakerId = new Types.ObjectId();
|
||||
const blameId = new Types.ObjectId();
|
||||
const claimId = new Types.ObjectId();
|
||||
const request = {
|
||||
_id: blameId,
|
||||
publicId: "BL-FILE-MAKER-RESUME",
|
||||
requestNo: "BL-RESUME",
|
||||
type: BlameRequestType.THIRD_PARTY,
|
||||
status: CaseStatus.OPEN,
|
||||
isMadeByFileMaker: true,
|
||||
initiatedByFieldExpertId: fileMakerId,
|
||||
requiresFileMakerApproval,
|
||||
parties: [],
|
||||
workflow: {
|
||||
currentStep: WorkflowStep.SECOND_COMPLETED,
|
||||
nextStep: WorkflowStep.WAITING_FOR_GUILT_DECISION,
|
||||
completedSteps: [
|
||||
WorkflowStep.FIRST_COMPLETED,
|
||||
WorkflowStep.SECOND_COMPLETED,
|
||||
],
|
||||
},
|
||||
};
|
||||
const claim = {
|
||||
_id: claimId,
|
||||
blameRequestId: blameId,
|
||||
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
workflow: {
|
||||
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
nextStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||
},
|
||||
};
|
||||
const service =
|
||||
new (RequestManagementService as any)() as RequestManagementService;
|
||||
(service as any).blameRequestDbService = {
|
||||
findById: jest.fn().mockResolvedValue(request),
|
||||
find: jest.fn().mockResolvedValue([request]),
|
||||
};
|
||||
(service as any).claimCaseDbService = {
|
||||
findOne: jest.fn().mockResolvedValue(claim),
|
||||
find: jest.fn().mockResolvedValue([claim]),
|
||||
};
|
||||
|
||||
const reopened = await service.getMyFileMakerFileDetail(
|
||||
{ sub: String(fileMakerId), role: RoleEnum.FILE_MAKER },
|
||||
String(blameId),
|
||||
);
|
||||
|
||||
expect(reopened.status).toBe(CaseStatus.OPEN);
|
||||
expect(reopened.claimStatus).toBe(
|
||||
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
);
|
||||
expect(reopened.fileMakerResume).toEqual({
|
||||
action: "UPLOAD_REQUIRED_DOCUMENTS",
|
||||
entity: "CLAIM",
|
||||
entityId: String(claimId),
|
||||
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
nextStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
});
|
||||
|
||||
const list = await service.getMyFileMakerFiles({
|
||||
sub: String(fileMakerId),
|
||||
role: RoleEnum.FILE_MAKER,
|
||||
});
|
||||
expect(list.list[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
linkedClaimId: String(claimId),
|
||||
claimStatus: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
fileMakerResume: expect.objectContaining({
|
||||
action: "UPLOAD_REQUIRED_DOCUMENTS",
|
||||
entityId: String(claimId),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not leave the blame narrative before all required signatures", async () => {
|
||||
const fileMakerId = new Types.ObjectId();
|
||||
const blameId = new Types.ObjectId();
|
||||
const claimId = new Types.ObjectId();
|
||||
const request = {
|
||||
_id: blameId,
|
||||
publicId: "BL-FILE-MAKER-NARRATIVE",
|
||||
requestNo: "BL-NARRATIVE",
|
||||
type: BlameRequestType.THIRD_PARTY,
|
||||
status: CaseStatus.OPEN,
|
||||
isMadeByFileMaker: true,
|
||||
initiatedByFieldExpertId: fileMakerId,
|
||||
parties: [],
|
||||
workflow: {
|
||||
currentStep: WorkflowStep.FIRST_DESCRIPTION,
|
||||
nextStep: WorkflowStep.FIRST_SIGN,
|
||||
completedSteps: [WorkflowStep.FIRST_VOICE],
|
||||
},
|
||||
};
|
||||
const claim = {
|
||||
_id: claimId,
|
||||
blameRequestId: blameId,
|
||||
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
workflow: {
|
||||
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
nextStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||
},
|
||||
};
|
||||
const service =
|
||||
new (RequestManagementService as any)() as RequestManagementService;
|
||||
(service as any).blameRequestDbService = {
|
||||
findById: jest.fn().mockResolvedValue(request),
|
||||
};
|
||||
(service as any).claimCaseDbService = {
|
||||
findOne: jest.fn().mockResolvedValue(claim),
|
||||
};
|
||||
|
||||
const reopened = await service.getMyFileMakerFileDetail(
|
||||
{ sub: String(fileMakerId), role: RoleEnum.FILE_MAKER },
|
||||
String(blameId),
|
||||
);
|
||||
|
||||
expect(reopened.fileMakerResume).toEqual(
|
||||
expect.objectContaining({
|
||||
action: "CONTINUE_BLAME",
|
||||
entity: "BLAME",
|
||||
entityId: String(blameId),
|
||||
currentStep: WorkflowStep.FIRST_DESCRIPTION,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Put,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
UploadRequiredDocumentV2ResponseDto,
|
||||
} from "src/claim-request-management/dto/upload-document-v2.dto";
|
||||
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
|
||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
|
||||
/**
|
||||
* V4 FileMaker flow — first half of the split blame workflow.
|
||||
@@ -96,10 +98,13 @@ export class FileMakerBlameV4Controller {
|
||||
@Get("my-files")
|
||||
@ApiOperation({
|
||||
summary: "List all blame files created by this FileMaker",
|
||||
description: "Returns all V4 FileMaker blame files initiated by the authenticated FileMaker.",
|
||||
description: "Returns V4 FileMaker blame files using the shared search, sort, filter, and pagination query contract.",
|
||||
})
|
||||
async getMyFiles(@CurrentUser() fileMaker: any) {
|
||||
return this.requestManagementService.getMyFileMakerFiles(fileMaker);
|
||||
async getMyFiles(
|
||||
@CurrentUser() fileMaker: any,
|
||||
@Query() query: ListQueryV2Dto,
|
||||
) {
|
||||
return this.requestManagementService.getMyFileMakerFiles(fileMaker, query);
|
||||
}
|
||||
|
||||
@Get("my-files/:requestId")
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Put,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
UploadRequiredDocumentV2ResponseDto,
|
||||
} from "src/claim-request-management/dto/upload-document-v2.dto";
|
||||
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
|
||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
|
||||
/**
|
||||
* V5 FileMaker flow — identical to V4 but under the /v5/ prefix.
|
||||
@@ -95,10 +97,13 @@ export class FileMakerBlameV5Controller {
|
||||
@Get("my-files")
|
||||
@ApiOperation({
|
||||
summary: "List all blame files created by this FileMaker",
|
||||
description: "Returns all V5 FileMaker blame files initiated by the authenticated FileMaker.",
|
||||
description: "Returns V5 FileMaker blame files using the shared search, sort, filter, and pagination query contract.",
|
||||
})
|
||||
async getMyFiles(@CurrentUser() fileMaker: any) {
|
||||
return this.requestManagementService.getMyFileMakerFiles(fileMaker);
|
||||
async getMyFiles(
|
||||
@CurrentUser() fileMaker: any,
|
||||
@Query() query: ListQueryV2Dto,
|
||||
) {
|
||||
return this.requestManagementService.getMyFileMakerFiles(fileMaker, query);
|
||||
}
|
||||
|
||||
@Get("my-files/:requestId")
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
CapturePartV2Dto,
|
||||
CapturePartV2ResponseDto,
|
||||
} from "src/claim-request-management/dto/capture-part-v2.dto";
|
||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
|
||||
|
||||
/**
|
||||
@@ -88,10 +90,16 @@ export class FileReviewerBlameV4Controller {
|
||||
@ApiOperation({
|
||||
summary: "List available and assigned FileMaker blame files",
|
||||
description:
|
||||
"Returns V4 FileMaker blame files in this reviewer's insurer: sealed files that are still available to claim, plus files already assigned to the authenticated FileReviewer.",
|
||||
"Returns V4 FileMaker blame files in this reviewer's insurer using the shared search, sort, filter, and pagination query contract: sealed files that are still available to claim, plus files already assigned to the authenticated FileReviewer.",
|
||||
})
|
||||
async getMyFiles(@CurrentUser() fileReviewer: any) {
|
||||
return this.requestManagementService.getMyFileReviewerFiles(fileReviewer);
|
||||
async getMyFiles(
|
||||
@CurrentUser() fileReviewer: any,
|
||||
@Query() query: ListQueryV2Dto,
|
||||
) {
|
||||
return this.requestManagementService.getMyFileReviewerFiles(
|
||||
fileReviewer,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("my-files/:requestId")
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
CapturePartV2ResponseDto,
|
||||
} from "src/claim-request-management/dto/capture-part-v2.dto";
|
||||
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
|
||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
|
||||
/**
|
||||
* V5 FileReviewer flow — same as V4 except after the damage expert completes
|
||||
@@ -86,10 +88,16 @@ export class FileReviewerBlameV5Controller {
|
||||
@ApiOperation({
|
||||
summary: "List available and assigned FileMaker blame files",
|
||||
description:
|
||||
"Returns V5 FileMaker blame files in this reviewer's insurer: sealed files that are still available to claim, plus files already assigned to the authenticated FileReviewer.",
|
||||
"Returns V5 FileMaker blame files in this reviewer's insurer using the shared search, sort, filter, and pagination query contract: sealed files that are still available to claim, plus files already assigned to the authenticated FileReviewer.",
|
||||
})
|
||||
async getMyFiles(@CurrentUser() fileReviewer: any) {
|
||||
return this.requestManagementService.getMyFileReviewerFiles(fileReviewer);
|
||||
async getMyFiles(
|
||||
@CurrentUser() fileReviewer: any,
|
||||
@Query() query: ListQueryV2Dto,
|
||||
) {
|
||||
return this.requestManagementService.getMyFileReviewerFiles(
|
||||
fileReviewer,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("my-files/:requestId")
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("inquiry participant resolver", () => {
|
||||
},
|
||||
vin: "NAAM01E15HK123456",
|
||||
}),
|
||||
).toThrow("previousPolicyholderNationalCode");
|
||||
).toThrow("کد ملی بیمهگذار قبلی");
|
||||
});
|
||||
|
||||
it("defaults an omitted registration state to CURRENT", () => {
|
||||
@@ -102,7 +102,7 @@ describe("inquiry participant resolver", () => {
|
||||
},
|
||||
vin: "TOO-SHORT",
|
||||
}),
|
||||
).toThrow("vehicle.vin must contain exactly 17 characters.");
|
||||
).toThrow("شماره شاسی (VIN) باید دقیقاً ۱۷ کاراکتر باشد.");
|
||||
});
|
||||
|
||||
it("rejects an incomplete current plate", () => {
|
||||
@@ -115,7 +115,7 @@ describe("inquiry participant resolver", () => {
|
||||
ir: "22",
|
||||
},
|
||||
}),
|
||||
).toThrow("vehicle.currentPlate.centerDigits is required.");
|
||||
).toThrow("سه رقم میانی پلاک در پلاک فعلی الزامی است.");
|
||||
});
|
||||
|
||||
it("rejects invalid vehicle choice values", () => {
|
||||
@@ -129,7 +129,7 @@ describe("inquiry participant resolver", () => {
|
||||
ir: "22",
|
||||
},
|
||||
}),
|
||||
).toThrow("vehicle.registrationState must be CURRENT or RECENTLY_TRANSFERRED.");
|
||||
).toThrow("وضعیت پلاک خودرو باید «فعلی» یا «تازه تعویضشده» باشد.");
|
||||
|
||||
expect(() =>
|
||||
resolveInquiryVehicle({
|
||||
@@ -141,7 +141,7 @@ describe("inquiry participant resolver", () => {
|
||||
},
|
||||
isNewCar: "false" as any,
|
||||
}),
|
||||
).toThrow("vehicle.isNewCar must be a boolean.");
|
||||
).toThrow("وضعیت صفر بودن خودرو نامعتبر است.");
|
||||
});
|
||||
|
||||
it("rejects a previous policyholder national code for a current registration", () => {
|
||||
@@ -647,7 +647,7 @@ describe("inquiry participant resolver", () => {
|
||||
|
||||
expect(() =>
|
||||
resolveInquiryParticipants(BlameRequestType.THIRD_PARTY, input as any),
|
||||
).toThrow("THIRD_PARTY_POLICYHOLDER does not support the unknown option.");
|
||||
).toThrow("ثبت بیمهگذار شخص ثالث بهصورت نامشخص امکانپذیر نیست.");
|
||||
});
|
||||
|
||||
it("strips the removed unknown field from historical participant output", () => {
|
||||
|
||||
@@ -60,6 +60,20 @@ export interface InquirySubjects {
|
||||
driverNationalCode: string;
|
||||
}
|
||||
|
||||
const PARTICIPANT_ROLE_LABELS: Record<InquiryParticipantRole, string> = {
|
||||
[InquiryParticipantRole.DRIVER]: "راننده",
|
||||
[InquiryParticipantRole.VEHICLE_OWNER]: "مالک خودرو",
|
||||
[InquiryParticipantRole.THIRD_PARTY_POLICYHOLDER]: "بیمهگذار شخص ثالث",
|
||||
[InquiryParticipantRole.CAR_BODY_POLICYHOLDER]: "بیمهگذار بدنه",
|
||||
};
|
||||
|
||||
const PLATE_FIELD_LABELS = {
|
||||
leftDigits: "دو رقم سمت چپ پلاک",
|
||||
centerAlphabet: "حرف پلاک",
|
||||
centerDigits: "سه رقم میانی پلاک",
|
||||
ir: "کد ایران پلاک",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The single routing seam for external inquiries. Policy checks belong to
|
||||
* their policyholder, Sheba belongs to the vehicle owner, and licence data
|
||||
@@ -70,7 +84,7 @@ export function resolveInquirySubjects(
|
||||
): InquirySubjects {
|
||||
if (!submission.vehicleOwner) {
|
||||
throw new BadRequestException(
|
||||
"Vehicle owner identity is required for Sheba validation.",
|
||||
"اطلاعات هویتی مالک خودرو برای استعلام شبا الزامی است.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -102,7 +116,11 @@ function assertCompleteInquiryPlate(
|
||||
"ir",
|
||||
] as const) {
|
||||
if (plate?.[field] == null || String(plate[field]).trim() === "") {
|
||||
throw new BadRequestException(`${path}.${field} is required.`);
|
||||
const plateLabel =
|
||||
path === "vehicle.currentPlate" ? "پلاک فعلی" : "پلاک قبلی";
|
||||
throw new BadRequestException(
|
||||
`${PLATE_FIELD_LABELS[field]} در ${plateLabel} الزامی است.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,21 +189,21 @@ function requiredIdentity(
|
||||
): ResolvedInquiryParticipant {
|
||||
if (Object.prototype.hasOwnProperty.call(input, "phoneNumber")) {
|
||||
throw new BadRequestException(
|
||||
`${role} does not accept phoneNumber; phone numbers are collected separately from inquiry identity.`,
|
||||
`شماره همراه ${PARTICIPANT_ROLE_LABELS[role]} باید جدا از اطلاعات هویتی استعلام ارسال شود.`,
|
||||
);
|
||||
}
|
||||
const nationalCode = String(input.nationalCode ?? "").trim();
|
||||
const birthday = String(input.birthday ?? "").trim();
|
||||
if (!nationalCode || !birthday) {
|
||||
throw new BadRequestException(
|
||||
`${role} requires nationalCode and birthday.`,
|
||||
`کد ملی و تاریخ تولد ${PARTICIPANT_ROLE_LABELS[role]} الزامی است.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
role === InquiryParticipantRole.DRIVER &&
|
||||
typeof input.hasDrivingLicense !== "boolean"
|
||||
) {
|
||||
throw new BadRequestException("DRIVER requires hasDrivingLicense.");
|
||||
throw new BadRequestException("وضعیت داشتن گواهینامه راننده الزامی است.");
|
||||
}
|
||||
if (
|
||||
role === InquiryParticipantRole.DRIVER &&
|
||||
@@ -194,7 +212,7 @@ function requiredIdentity(
|
||||
!String(input.licenseType ?? "").trim())
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"DRIVER requires licenseNumber and licenseType when hasDrivingLicense is true.",
|
||||
"شماره و نوع گواهینامه برای راننده دارای گواهینامه الزامی است.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -220,7 +238,7 @@ export function resolveInquiryParticipants(
|
||||
);
|
||||
if (!hasRoleCompleteInput) {
|
||||
throw new BadRequestException(
|
||||
"driver, vehicleOwner, and thirdPartyPolicyholder are required in the structured inquiry format.",
|
||||
"اطلاعات راننده، مالک خودرو و بیمهگذار شخص ثالث برای استعلام الزامی است.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -228,7 +246,7 @@ export function resolveInquiryParticipants(
|
||||
input.carBodyPolicyholder != null
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"CAR_BODY_POLICYHOLDER is not allowed for a THIRD_PARTY case.",
|
||||
"بیمهگذار بدنه برای پرونده شخص ثالث قابل ثبت نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,19 +267,23 @@ export function resolveInquiryParticipants(
|
||||
if (existing) return existing;
|
||||
if (resolving.has(role)) {
|
||||
throw new BadRequestException(
|
||||
"Participant sameAs references cannot be circular.",
|
||||
"ارتباط اشخاص یکسان در اطلاعات استعلام نامعتبر است.",
|
||||
);
|
||||
}
|
||||
const value = input[ROLE_FIELDS[role]] as
|
||||
| InquiryParticipantInputDto
|
||||
| undefined;
|
||||
if (!value) throw new BadRequestException(`${role} is required.`);
|
||||
if (!value) {
|
||||
throw new BadRequestException(
|
||||
`اطلاعات ${PARTICIPANT_ROLE_LABELS[role]} الزامی است.`,
|
||||
);
|
||||
}
|
||||
|
||||
resolving.add(role);
|
||||
let participantId: string;
|
||||
if (Object.prototype.hasOwnProperty.call(value, "unknown")) {
|
||||
throw new BadRequestException(
|
||||
`${role} does not support the unknown option.`,
|
||||
`ثبت ${PARTICIPANT_ROLE_LABELS[role]} بهصورت نامشخص امکانپذیر نیست.`,
|
||||
);
|
||||
} else if (value.sameAs) {
|
||||
const hasPersonSpecificFields = Object.entries(value).some(
|
||||
@@ -269,7 +291,7 @@ export function resolveInquiryParticipants(
|
||||
);
|
||||
if (hasPersonSpecificFields) {
|
||||
throw new BadRequestException(
|
||||
`${role} must contain either sameAs or identity fields, not both.`,
|
||||
`برای ${PARTICIPANT_ROLE_LABELS[role]} باید فقط ارتباط با شخص دیگر یا اطلاعات هویتی مستقل ارسال شود.`,
|
||||
);
|
||||
}
|
||||
participantId = resolveRole(value.sameAs);
|
||||
@@ -280,7 +302,7 @@ export function resolveInquiryParticipants(
|
||||
);
|
||||
if (duplicate) {
|
||||
throw new BadRequestException(
|
||||
`${role} duplicates an existing nationalCode; use sameAs instead.`,
|
||||
`کد ملی ${PARTICIPANT_ROLE_LABELS[role]} تکراری است؛ ارتباط با شخص ثبتشده را انتخاب کنید.`,
|
||||
);
|
||||
}
|
||||
participants.set(participant.participantId, participant);
|
||||
@@ -317,29 +339,29 @@ export function resolveInquiryVehicle(
|
||||
input: InquiryVehicleInputDto,
|
||||
): ResolvedInquiryVehicle {
|
||||
if (!input) {
|
||||
throw new BadRequestException("vehicle is required.");
|
||||
throw new BadRequestException("اطلاعات خودرو برای استعلام الزامی است.");
|
||||
}
|
||||
const registrationState =
|
||||
input.registrationState ?? VehicleRegistrationState.CURRENT;
|
||||
if (!Object.values(VehicleRegistrationState).includes(registrationState)) {
|
||||
throw new BadRequestException(
|
||||
"vehicle.registrationState must be CURRENT or RECENTLY_TRANSFERRED.",
|
||||
"وضعیت پلاک خودرو باید «فعلی» یا «تازه تعویضشده» باشد.",
|
||||
);
|
||||
}
|
||||
if (input.isNewCar != null && typeof input.isNewCar !== "boolean") {
|
||||
throw new BadRequestException("vehicle.isNewCar must be a boolean.");
|
||||
throw new BadRequestException("وضعیت صفر بودن خودرو نامعتبر است.");
|
||||
}
|
||||
const previousPolicyholderNationalCode = String(
|
||||
input.previousPolicyholderNationalCode ?? "",
|
||||
).trim();
|
||||
if (!input.currentPlate) {
|
||||
throw new BadRequestException("vehicle.currentPlate is required.");
|
||||
throw new BadRequestException("پلاک فعلی خودرو برای استعلام الزامی است.");
|
||||
}
|
||||
assertCompleteInquiryPlate(input.currentPlate, "vehicle.currentPlate");
|
||||
const vin = String(input.vin ?? "").trim();
|
||||
if (vin && vin.length !== 17) {
|
||||
throw new BadRequestException(
|
||||
"vehicle.vin must contain exactly 17 characters.",
|
||||
"شماره شاسی (VIN) باید دقیقاً ۱۷ کاراکتر باشد.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -347,7 +369,7 @@ export function resolveInquiryVehicle(
|
||||
(!input.previousPlate || !vin || !previousPolicyholderNationalCode)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"RECENTLY_TRANSFERRED requires previousPlate, vin, and previousPolicyholderNationalCode.",
|
||||
"برای خودروی تازه تعویضپلاکشده، پلاک قبلی، شماره شاسی (VIN) و کد ملی بیمهگذار قبلی الزامی است.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -355,7 +377,7 @@ export function resolveInquiryVehicle(
|
||||
(input.previousPlate || input.previousPolicyholderNationalCode != null)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"previousPlate and previousPolicyholderNationalCode are only allowed for RECENTLY_TRANSFERRED vehicles.",
|
||||
"پلاک و کد ملی بیمهگذار قبلی فقط برای خودروی تازه تعویضپلاکشده قابل ثبت است.",
|
||||
);
|
||||
}
|
||||
if (input.previousPlate) {
|
||||
@@ -442,7 +464,7 @@ function assertStructuredInquiryInput(input: Record<string, any>): void {
|
||||
);
|
||||
if (legacyFields.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`Legacy inquiry fields are not accepted: ${legacyFields.join(", ")}. Use driver, vehicleOwner, thirdPartyPolicyholder, carBodyPolicyholder, and vehicle.`,
|
||||
"ساختار قدیمی اطلاعات استعلام پذیرفته نمیشود؛ اطلاعات اشخاص و خودرو را در بخشهای جدید ارسال کنید.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -466,7 +488,7 @@ export function assertPreviousPlateInquiryMatchesVin(
|
||||
.filter(Boolean);
|
||||
if (!expected || !candidates.includes(expected)) {
|
||||
throw new BadRequestException(
|
||||
"Previous-plate inquiry does not match the submitted VIN/chassis; manual review is required.",
|
||||
"نتیجه استعلام پلاک قبلی با شماره شاسی (VIN) واردشده مطابقت ندارد و پرونده نیازمند بررسی دستی است.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -540,7 +562,7 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
||||
});
|
||||
if (!isLast) continue;
|
||||
const error = new BadRequestException(
|
||||
"No current usable policy was found for the submitted vehicle identifiers.",
|
||||
"بیمهنامه معتبر و فعالی مطابق مشخصات خودرو و کد ملی واردشده یافت نشد.",
|
||||
) as BadRequestException & { attempts?: typeof attempts };
|
||||
error.attempts = attempts;
|
||||
throw error;
|
||||
@@ -592,7 +614,12 @@ export async function runPlateInquiryWithFallback<T>(options: {
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new BadRequestException("Inquiry failed for all plates.");
|
||||
throw (
|
||||
lastError ??
|
||||
new BadRequestException(
|
||||
"برای هیچیک از پلاکهای ثبتشده نتیجه معتبری یافت نشد.",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||
@@ -610,7 +637,7 @@ export function normalizeInquirySubmission<T extends Record<string, any>>(
|
||||
);
|
||||
if (!driver || !thirdPartyPolicyholder) {
|
||||
throw new BadRequestException(
|
||||
"Driver and third-party policyholder identities are required.",
|
||||
"اطلاعات هویتی راننده و بیمهگذار شخص ثالث الزامی است.",
|
||||
);
|
||||
}
|
||||
const vehicleOwner = participantForRole(
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ReinquiryInquiriesResponseDto,
|
||||
ReinquiryPartyResultDto,
|
||||
} from "./dto/reinquiry-inquiries.dto";
|
||||
import { getInquiryErrorMessage } from "src/common/utils/inquiry-error";
|
||||
|
||||
type PlateParts = {
|
||||
leftDigits: number;
|
||||
@@ -48,7 +49,7 @@ export class InquiryRefreshService {
|
||||
|
||||
if (!body.publicId && !body.blameRequestId && limit === 0) {
|
||||
throw new BadRequestException(
|
||||
"Provide publicId, blameRequestId, or limit for bulk refresh.",
|
||||
"برای اجرای مجدد استعلام، شناسه عمومی پرونده، شناسه پرونده یا تعداد پروندهها را وارد کنید.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,14 +59,16 @@ export class InquiryRefreshService {
|
||||
if (body.publicId) filter.publicId = body.publicId;
|
||||
if (body.blameRequestId) {
|
||||
if (!Types.ObjectId.isValid(body.blameRequestId)) {
|
||||
throw new BadRequestException("Invalid blameRequestId");
|
||||
throw new BadRequestException("شناسه پرونده معتبر نیست.");
|
||||
}
|
||||
filter._id = new Types.ObjectId(body.blameRequestId);
|
||||
}
|
||||
|
||||
let docs = await this.blameRequestDbService.find(filter, { lean: true });
|
||||
if (!docs.length) {
|
||||
throw new NotFoundException("No matching blame cases found");
|
||||
throw new NotFoundException(
|
||||
"پرونده تقصیر مطابق اطلاعات واردشده یافت نشد.",
|
||||
);
|
||||
}
|
||||
docs = limit > 0 ? docs.slice(0, limit) : docs;
|
||||
|
||||
@@ -111,8 +114,8 @@ export class InquiryRefreshService {
|
||||
if (index === -1) {
|
||||
partyResults.push({
|
||||
role,
|
||||
thirdParty: { ok: false, message: "party not found" },
|
||||
person: { ok: false, message: "party not found" },
|
||||
thirdParty: { ok: false, message: "طرف پرونده یافت نشد." },
|
||||
person: { ok: false, message: "طرف پرونده یافت نشد." },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -150,7 +153,9 @@ export class InquiryRefreshService {
|
||||
blameRequestId: new Types.ObjectId(String(doc._id)),
|
||||
});
|
||||
claimsUpdated = linkedClaims.length;
|
||||
this.logger.log(`[dry-run] ${label} would update blame + ${claimsUpdated} claim(s)`);
|
||||
this.logger.log(
|
||||
`[dry-run] ${label} would update blame + ${claimsUpdated} claim(s)`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -208,9 +213,7 @@ export class InquiryRefreshService {
|
||||
plateId: party?.vehicle?.plateId,
|
||||
...(plate ? { plate } : {}),
|
||||
...(nationalCode ? { nationalCode } : {}),
|
||||
...(birthDate !== null && birthDate !== undefined
|
||||
? { birthDate }
|
||||
: {}),
|
||||
...(birthDate !== null && birthDate !== undefined ? { birthDate } : {}),
|
||||
};
|
||||
|
||||
if (dryRun) {
|
||||
@@ -223,8 +226,8 @@ export class InquiryRefreshService {
|
||||
result.thirdParty = {
|
||||
ok: false,
|
||||
message: !plate
|
||||
? "plate not found on party"
|
||||
: "nationalCodeOfInsurer/nationalCodeOfDriver missing",
|
||||
? "پلاک برای این طرف پرونده ثبت نشده است."
|
||||
: "کد ملی برای این طرف پرونده ثبت نشده است.",
|
||||
};
|
||||
} else {
|
||||
await this.waitForRateLimit();
|
||||
@@ -249,7 +252,9 @@ export class InquiryRefreshService {
|
||||
inquiriesChanged = true;
|
||||
result.thirdParty = {
|
||||
ok: false,
|
||||
message: inquiry.mapped.Error.Message || "third-party inquiry error",
|
||||
message:
|
||||
inquiry.mapped.Error.Message ||
|
||||
getInquiryErrorMessage(inquiry.mapped, "thirdPartyPlate"),
|
||||
};
|
||||
} else {
|
||||
nextParty = this.applyThirdPartyToParty(
|
||||
@@ -292,11 +297,18 @@ export class InquiryRefreshService {
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.recordPartyInquiry(inquiries, "thirdParty", role, false, {}, error);
|
||||
this.recordPartyInquiry(
|
||||
inquiries,
|
||||
"thirdParty",
|
||||
role,
|
||||
false,
|
||||
{},
|
||||
error,
|
||||
);
|
||||
inquiriesChanged = true;
|
||||
result.thirdParty = {
|
||||
ok: false,
|
||||
message: error?.message || String(error),
|
||||
message: getInquiryErrorMessage(error, "thirdPartyPlate"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -305,8 +317,8 @@ export class InquiryRefreshService {
|
||||
result.person = {
|
||||
ok: false,
|
||||
message: !nationalCode
|
||||
? "nationalCodeOfInsurer/nationalCodeOfDriver missing"
|
||||
: "insurerBirthday/driverBirthday missing",
|
||||
? "کد ملی برای این طرف پرونده ثبت نشده است."
|
||||
: "تاریخ تولد برای این طرف پرونده ثبت نشده است.",
|
||||
};
|
||||
} else {
|
||||
await this.waitForRateLimit();
|
||||
@@ -333,7 +345,7 @@ export class InquiryRefreshService {
|
||||
inquiriesChanged = true;
|
||||
result.person = {
|
||||
ok: false,
|
||||
message: error?.message || String(error),
|
||||
message: getInquiryErrorMessage(error, "personalIdentity"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -477,12 +489,14 @@ export class InquiryRefreshService {
|
||||
): Promise<void> {
|
||||
const blameDoc = await this.blameRequestDbService.findById(blameId);
|
||||
if (!blameDoc) {
|
||||
throw new NotFoundException(`Blame case ${blameId} not found`);
|
||||
throw new NotFoundException("پرونده تقصیر یافت نشد.");
|
||||
}
|
||||
|
||||
for (const role of roles) {
|
||||
const memParty = updatedParties.find((party) => party?.role === role);
|
||||
const docIdx = blameDoc.parties.findIndex((party) => party?.role === role);
|
||||
const docIdx = blameDoc.parties.findIndex(
|
||||
(party) => party?.role === role,
|
||||
);
|
||||
if (!memParty || docIdx === -1) continue;
|
||||
|
||||
const party = blameDoc.parties[docIdx];
|
||||
@@ -514,7 +528,9 @@ export class InquiryRefreshService {
|
||||
party.insurance.company = memParty.insurance.company;
|
||||
}
|
||||
if (memParty.insurance.financialCeiling !== undefined) {
|
||||
party.insurance.financialCeiling = String(memParty.insurance.financialCeiling);
|
||||
party.insurance.financialCeiling = String(
|
||||
memParty.insurance.financialCeiling,
|
||||
);
|
||||
}
|
||||
if (memParty.insurance.startDate !== undefined) {
|
||||
party.insurance.startDate = memParty.insurance.startDate;
|
||||
@@ -541,7 +557,8 @@ export class InquiryRefreshService {
|
||||
});
|
||||
|
||||
const inquiryPatch: Record<string, unknown> = {};
|
||||
if (inquiries.thirdParty) inquiryPatch["inquiries.thirdParty"] = inquiries.thirdParty;
|
||||
if (inquiries.thirdParty)
|
||||
inquiryPatch["inquiries.thirdParty"] = inquiries.thirdParty;
|
||||
if (inquiries.person) inquiryPatch["inquiries.person"] = inquiries.person;
|
||||
if (!Object.keys(inquiryPatch).length) return 0;
|
||||
|
||||
@@ -590,14 +607,16 @@ export class InquiryRefreshService {
|
||||
|
||||
private normalizeInquiryError(error: any): Record<string, unknown> {
|
||||
return {
|
||||
message: error?.message || String(error),
|
||||
message: getInquiryErrorMessage(error, "generic"),
|
||||
status: error?.status ?? error?.response?.status,
|
||||
data: error?.data ?? error?.response?.data,
|
||||
};
|
||||
}
|
||||
|
||||
private resolvePartyPlate(party: Record<string, any>): PlateParts | null {
|
||||
const fromPlateId = this.parsePlateFromCompactString(party?.vehicle?.plateId);
|
||||
const fromPlateId = this.parsePlateFromCompactString(
|
||||
party?.vehicle?.plateId,
|
||||
);
|
||||
if (fromPlateId) return fromPlateId;
|
||||
|
||||
const candidates = [
|
||||
@@ -608,14 +627,24 @@ export class InquiryRefreshService {
|
||||
].filter(Boolean);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const leftDigits = this.firstPresent(candidate.Plk1, candidate.platePartOne);
|
||||
const leftDigits = this.firstPresent(
|
||||
candidate.Plk1,
|
||||
candidate.platePartOne,
|
||||
);
|
||||
const centerAlphabet = this.firstPresent(
|
||||
candidate.plateLetterid,
|
||||
candidate.plateLetterId,
|
||||
candidate.plateLetterTitle,
|
||||
);
|
||||
const centerDigits = this.firstPresent(candidate.Plk3, candidate.platePartThree);
|
||||
const ir = this.firstPresent(candidate.PlkSrl, candidate.plkSrl, candidate.plateSerialNumber);
|
||||
const centerDigits = this.firstPresent(
|
||||
candidate.Plk3,
|
||||
candidate.platePartThree,
|
||||
);
|
||||
const ir = this.firstPresent(
|
||||
candidate.PlkSrl,
|
||||
candidate.plkSrl,
|
||||
candidate.plateSerialNumber,
|
||||
);
|
||||
|
||||
if (
|
||||
leftDigits !== undefined &&
|
||||
@@ -623,7 +652,9 @@ export class InquiryRefreshService {
|
||||
centerDigits !== undefined &&
|
||||
ir !== undefined
|
||||
) {
|
||||
const plateLetter = this.plateNormalizer.normalizePlateText(String(centerAlphabet));
|
||||
const plateLetter = this.plateNormalizer.normalizePlateText(
|
||||
String(centerAlphabet),
|
||||
);
|
||||
const parsed: PlateParts = {
|
||||
leftDigits: Number(leftDigits),
|
||||
centerAlphabet: plateLetter,
|
||||
@@ -653,7 +684,9 @@ export class InquiryRefreshService {
|
||||
const ir = Number(irRaw);
|
||||
const leftDigits = Number(leftRaw);
|
||||
const centerDigits = Number(centerRaw);
|
||||
const centerAlphabet = this.plateNormalizer.normalizePlateText(String(alphaRaw || ""));
|
||||
const centerAlphabet = this.plateNormalizer.normalizePlateText(
|
||||
String(alphaRaw || ""),
|
||||
);
|
||||
if (
|
||||
!Number.isFinite(ir) ||
|
||||
!Number.isFinite(leftDigits) ||
|
||||
@@ -685,6 +718,8 @@ export class InquiryRefreshService {
|
||||
}
|
||||
|
||||
private firstPresent(...values: unknown[]): unknown {
|
||||
return values.find((value) => value !== undefined && value !== null && value !== "");
|
||||
return values.find(
|
||||
(value) => value !== undefined && value !== null && value !== "",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,8 @@ describe("damaged-party inquiry requirements", () => {
|
||||
const service = getService();
|
||||
|
||||
await expect(
|
||||
(service as any).validateShebaV3(
|
||||
undefined,
|
||||
"0012345678",
|
||||
"client-id",
|
||||
),
|
||||
).rejects.toThrow("sheba is required for the damaged party.");
|
||||
(service as any).validateShebaV3(undefined, "0012345678", "client-id"),
|
||||
).rejects.toThrow("شماره شبا برای طرف زیاندیده الزامی است.");
|
||||
expect(
|
||||
(service as any).sandHubService.getShebaValidation,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
@@ -14,6 +14,7 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
||||
publicId: "BLM-OPEN",
|
||||
type: "THIRD_PARTY",
|
||||
status: "WAITING_FOR_FILE_REVIEWER",
|
||||
createdAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
isMadeByFileMaker: true,
|
||||
expertInitiated: true,
|
||||
creationMethod: "IN_PERSON",
|
||||
@@ -34,6 +35,9 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
||||
undefined,
|
||||
blameRequestDbService,
|
||||
) as RequestManagementService;
|
||||
(service as any).claimCaseDbService = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
return { service, blameRequestDbService };
|
||||
}
|
||||
|
||||
@@ -46,7 +50,7 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
||||
clientKey: String(clientId),
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
expect(result.list).toEqual([
|
||||
expect.objectContaining({ _id: sealedFile._id, publicId: "BLM-OPEN" }),
|
||||
]);
|
||||
expect(blameRequestDbService.find).toHaveBeenCalledWith(
|
||||
@@ -82,7 +86,34 @@ describe("RequestManagementService FileReviewer inbox", () => {
|
||||
clientKey: String(clientId),
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(result.list).toEqual([]);
|
||||
});
|
||||
|
||||
it("sorts and paginates the reviewer inbox with the shared list contract", async () => {
|
||||
const olderFile = {
|
||||
...sealedFile,
|
||||
_id: new Types.ObjectId(),
|
||||
publicId: "BLM-OLDER",
|
||||
createdAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
};
|
||||
const { service } = createService([olderFile, sealedFile]);
|
||||
|
||||
const result = await service.getMyFileReviewerFiles(
|
||||
{
|
||||
sub: String(reviewerId),
|
||||
role: RoleEnum.FILE_REVIEWER,
|
||||
clientKey: String(clientId),
|
||||
},
|
||||
{ page: 1, limit: 1, sortBy: "createdAt", sortOrder: "desc" },
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({ total: 2, page: 1, limit: 1, totalPages: 2 }),
|
||||
);
|
||||
expect(result.list).toHaveLength(1);
|
||||
expect(result.list[0]).toEqual(
|
||||
expect.objectContaining({ publicId: "BLM-OPEN" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not expose an open file's details to a reviewer from another tenant", async () => {
|
||||
|
||||
@@ -60,9 +60,14 @@ import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.
|
||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
|
||||
import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
||||
import {
|
||||
applyListQueryV2,
|
||||
isInListDateRange,
|
||||
parseListDateRange,
|
||||
} from "src/helpers/list-query-v2";
|
||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
import { GetUserBlameListV2ResponseDto } from "src/request-management/dto/blame-list-user-v2.dto";
|
||||
import { resolveUnifiedFileStatus } from "src/helpers/unified-file-status";
|
||||
import { AutoCloseRequestService } from "src/utils/cron/cron.service";
|
||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||
import {
|
||||
@@ -138,6 +143,9 @@ import {
|
||||
runPlateInquiryWithFallback,
|
||||
sanitizeStoredInquiryParticipants,
|
||||
} from "./inquiry-participant-resolver";
|
||||
import {
|
||||
getInquiryErrorMessage,
|
||||
} from "src/common/utils/inquiry-error";
|
||||
|
||||
/**
|
||||
* Formats a compact Jalali date (number or string like 13780624) as YYYY/MM/DD.
|
||||
@@ -154,6 +162,18 @@ function formatJalaliCompact(
|
||||
return String(raw);
|
||||
}
|
||||
|
||||
type FileMakerResumeProjection = {
|
||||
action:
|
||||
| "CONTINUE_BLAME"
|
||||
| "UPLOAD_REQUIRED_DOCUMENTS"
|
||||
| "WAIT_FOR_FILE_REVIEWER";
|
||||
entity: "BLAME" | "CLAIM";
|
||||
entityId: string;
|
||||
status: string;
|
||||
currentStep: string;
|
||||
nextStep?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RequestManagementService {
|
||||
private readonly logger = new Logger(RequestManagementService.name);
|
||||
@@ -183,10 +203,16 @@ export class RequestManagementService {
|
||||
}
|
||||
|
||||
/** Keep eligibility rejections distinct from an unavailable CAR_BODY provider. */
|
||||
private throwCarBodyInquiryFailure(err: unknown): never {
|
||||
if (err instanceof ForbiddenException) throw err;
|
||||
private throwCarBodyInquiryFailure(
|
||||
err: unknown,
|
||||
context: "carBodyPlate" | "carBodyVin" = "carBodyPlate",
|
||||
): never {
|
||||
const message = getInquiryErrorMessage(err, context);
|
||||
if (err instanceof ForbiddenException) {
|
||||
throw new ForbiddenException(message);
|
||||
}
|
||||
|
||||
throw new HttpException("Car body inquiry failed", HttpStatus.BAD_REQUEST);
|
||||
throw new HttpException(message, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +226,7 @@ export class RequestManagementService {
|
||||
const configuredCode = Number(process.env.CLIENT_ID);
|
||||
if (!Number.isFinite(configuredCode)) {
|
||||
throw new InternalServerErrorException(
|
||||
"CLIENT_ID must be configured to save CAR_BODY policy ownership.",
|
||||
"تنظیمات شرکت بیمه برای ذخیره بیمهنامه بدنه کامل نیست.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,7 +246,7 @@ export class RequestManagementService {
|
||||
const clientId = (client as any)?._id ?? (client as any)?._doc?._id;
|
||||
if (!clientId) {
|
||||
throw new InternalServerErrorException(
|
||||
"Configured CAR_BODY insurer client could not be resolved.",
|
||||
"شرکت بیمه تنظیمشده برای بیمهنامه بدنه قابل شناسایی نیست.",
|
||||
);
|
||||
}
|
||||
return clientId;
|
||||
@@ -394,7 +420,7 @@ export class RequestManagementService {
|
||||
const policyholderNationalCode = subjects.carBodyPolicyNationalCode;
|
||||
if (!policyholderNationalCode) {
|
||||
throw new BadRequestException(
|
||||
"Car-body policyholder identity is required for a CAR_BODY inquiry.",
|
||||
"اطلاعات بیمهگذار برای استعلام بیمه بدنه الزامی است.",
|
||||
);
|
||||
}
|
||||
const result = await runPlateInquiryWithFallback({
|
||||
@@ -434,7 +460,7 @@ export class RequestManagementService {
|
||||
).trim();
|
||||
if (!chassis) {
|
||||
throw new BadRequestException(
|
||||
"vehicle.vin is required for a VIN/chassis inquiry.",
|
||||
"شماره شاسی (VIN) برای استعلام خودرو الزامی است.",
|
||||
);
|
||||
}
|
||||
const subjects = resolveInquirySubjects(submission);
|
||||
@@ -462,7 +488,7 @@ export class RequestManagementService {
|
||||
for (const participant of participants) {
|
||||
if (!participant.nationalCode || !participant.birthday) {
|
||||
throw new BadRequestException(
|
||||
`${participant.participantId} requires nationalCode and birthday for personal inquiry.`,
|
||||
"کد ملی و تاریخ تولد برای استعلام هویت الزامی است.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -486,7 +512,7 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`${participant.participantId} personal identity inquiry failed: ${error?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(error, "personalIdentity"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -512,7 +538,7 @@ export class RequestManagementService {
|
||||
const plate = submission.vehicle?.currentPlate ?? submission.dto.plate;
|
||||
if (!plate) {
|
||||
throw new BadRequestException(
|
||||
"Current plate is required for vehicle ownership inquiry.",
|
||||
"پلاک فعلی برای استعلام مالکیت خودرو الزامی است.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -536,7 +562,7 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`Vehicle ownership inquiry failed: ${error?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(error, "carOwnership"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -558,7 +584,7 @@ export class RequestManagementService {
|
||||
}
|
||||
if (!submission.driver.licenseNumber) {
|
||||
throw new BadRequestException(
|
||||
"Driver licence number is required when the driver has a licence.",
|
||||
"شماره گواهینامه راننده برای استعلام گواهینامه الزامی است.",
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -582,7 +608,7 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`Driver licence inquiry failed: ${error?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(error, "drivingLicense"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -683,7 +709,7 @@ export class RequestManagementService {
|
||||
private normalizeInquiryError(err: any): any {
|
||||
if (!err) return undefined;
|
||||
return {
|
||||
message: err?.message || String(err),
|
||||
message: getInquiryErrorMessage(err, "generic"),
|
||||
status: err?.response?.status,
|
||||
data: err?.response?.data,
|
||||
...(Array.isArray(err?.attempts) ? { attempts: err.attempts } : {}),
|
||||
@@ -1745,7 +1771,7 @@ export class RequestManagementService {
|
||||
body.insurerLicense === body.driverLicense)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Insurer and Driver should be two different persons in this mode.",
|
||||
"در این حالت، بیمهگذار و راننده باید دو شخص متفاوت باشند.",
|
||||
);
|
||||
}
|
||||
} else if (body.driverIsInsurer === true) {
|
||||
@@ -1757,7 +1783,7 @@ export class RequestManagementService {
|
||||
String(body.driverBirthday) === String(body.insurerBirthday);
|
||||
if (!sameNat || !sameLic || !sameBirthday) {
|
||||
throw new BadRequestException(
|
||||
"When driverIsInsurer is true, insurer and driver data must be the same.",
|
||||
"وقتی راننده همان بیمهگذار است، اطلاعات راننده و بیمهگذار باید یکسان باشد.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1831,7 +1857,10 @@ export class RequestManagementService {
|
||||
err,
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new HttpException("Inquiry failed", HttpStatus.BAD_REQUEST);
|
||||
throw new HttpException(
|
||||
getInquiryErrorMessage(err, "thirdPartyPlate"),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (inquiryMapped?.Error) {
|
||||
@@ -1847,7 +1876,8 @@ export class RequestManagementService {
|
||||
});
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new HttpException(
|
||||
inquiryMapped.Error.Message || "Inquiry returned error",
|
||||
inquiryMapped.Error.Message ||
|
||||
getInquiryErrorMessage(inquiryMapped, "thirdPartyPlate"),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -1875,7 +1905,7 @@ export class RequestManagementService {
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
if (!clientName) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyName missing from inquiry response`,
|
||||
"پاسخ استعلام بیمهنامه ناقص است و نام شرکت بیمه در آن وجود ندارد.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -1903,7 +1933,7 @@ export class RequestManagementService {
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyCode missing or invalid in inquiry response`,
|
||||
"پاسخ استعلام بیمهنامه ناقص است و شرکت بیمه قابل شناسایی نیست.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -2188,7 +2218,7 @@ export class RequestManagementService {
|
||||
body.insurerLicense === body.driverLicense)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Insurer and Driver should be two different persons in this mode.",
|
||||
"در این حالت، بیمهگذار و راننده باید دو شخص متفاوت باشند.",
|
||||
);
|
||||
}
|
||||
} else if (body.driverIsInsurer === true) {
|
||||
@@ -2200,7 +2230,7 @@ export class RequestManagementService {
|
||||
String(body.driverBirthday) === String(body.insurerBirthday);
|
||||
if (!sameNat || !sameLic || !sameBirthday) {
|
||||
throw new BadRequestException(
|
||||
"When driverIsInsurer is true, insurer and driver data must be the same.",
|
||||
"وقتی راننده همان بیمهگذار است، اطلاعات راننده و بیمهگذار باید یکسان باشد.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2261,7 +2291,10 @@ export class RequestManagementService {
|
||||
err,
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new HttpException("VIN inquiry failed", HttpStatus.BAD_REQUEST);
|
||||
throw new HttpException(
|
||||
getInquiryErrorMessage(err, "thirdPartyVin"),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (inquiryMapped?.Error) {
|
||||
@@ -2275,7 +2308,8 @@ export class RequestManagementService {
|
||||
});
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new HttpException(
|
||||
inquiryMapped.Error.Message || "VIN inquiry returned error",
|
||||
inquiryMapped.Error.Message ||
|
||||
getInquiryErrorMessage(inquiryMapped, "thirdPartyVin"),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -2303,7 +2337,7 @@ export class RequestManagementService {
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
if (!clientName) {
|
||||
const error = new BadRequestException(
|
||||
"CompanyName missing from VIN inquiry response",
|
||||
"پاسخ استعلام VIN ناقص است و نام شرکت بیمه در آن وجود ندارد.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -2329,7 +2363,7 @@ export class RequestManagementService {
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
const error = new BadRequestException(
|
||||
"CompanyCode missing or invalid in VIN inquiry response",
|
||||
"پاسخ استعلام VIN ناقص است و شرکت بیمه قابل شناسایی نیست.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -2459,7 +2493,7 @@ export class RequestManagementService {
|
||||
err,
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
this.throwCarBodyInquiryFailure(err);
|
||||
this.throwCarBodyInquiryFailure(err, "carBodyVin");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3472,7 +3506,10 @@ export class RequestManagementService {
|
||||
: null;
|
||||
|
||||
if (!client) {
|
||||
throw new HttpException("Client not found", HttpStatus.CONFLICT);
|
||||
throw new HttpException(
|
||||
"شرکت بیمه موجود در پاسخ استعلام قابل شناسایی نیست.",
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
|
||||
const partyDetails =
|
||||
@@ -3642,7 +3679,7 @@ export class RequestManagementService {
|
||||
this.logger.error(er);
|
||||
if (er instanceof HttpException) throw er;
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to update request with plate details.",
|
||||
"ذخیره اطلاعات پلاک و بیمهنامه انجام نشد.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3670,7 +3707,7 @@ export class RequestManagementService {
|
||||
body.driverIsInsurer === false
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Insurer and Driver should be two different persons in this mode.",
|
||||
"در این حالت، بیمهگذار و راننده باید دو شخص متفاوت باشند.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3703,7 +3740,7 @@ export class RequestManagementService {
|
||||
|
||||
if (isSameNationalCode || isSamePlate) {
|
||||
throw new ConflictException(
|
||||
"The plate and national code for the second party cannot be the same as the first party.",
|
||||
"پلاک و کد ملی طرف دوم نمیتواند با طرف اول یکسان باشد.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6466,6 +6503,20 @@ export class RequestManagementService {
|
||||
},
|
||||
);
|
||||
|
||||
const requestIds = requests.map((request: any) => request._id);
|
||||
const claimsForStatus =
|
||||
requestIds.length > 0
|
||||
? ((await this.claimCaseDbService.find(
|
||||
{ blameRequestId: { $in: requestIds } },
|
||||
{ lean: true, select: "blameRequestId status" },
|
||||
)) as any[])
|
||||
: [];
|
||||
const claimStatusByBlameId = new Map<string, string>(
|
||||
claimsForStatus
|
||||
.filter((claim) => claim?.blameRequestId && claim?.status)
|
||||
.map((claim) => [String(claim.blameRequestId), claim.status]),
|
||||
);
|
||||
|
||||
const enriched = requests.map((req: any) => {
|
||||
const isInitiator =
|
||||
(user?.role === RoleEnum.FIELD_EXPERT &&
|
||||
@@ -6487,17 +6538,44 @@ export class RequestManagementService {
|
||||
...obj,
|
||||
userSide: party?.role ?? null,
|
||||
initiatedByMe: isInitiator,
|
||||
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||
blameStatus: req.status,
|
||||
claimStatus: claimStatusByBlameId.get(String(req._id)),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
let filtered = enriched;
|
||||
if (query.unifiedStatus) {
|
||||
filtered = filtered.filter(
|
||||
(row) => row.unifiedFileStatus === query.unifiedStatus,
|
||||
);
|
||||
}
|
||||
const { fromDate, toDate } = parseListDateRange(
|
||||
query.startDate,
|
||||
query.endDate,
|
||||
);
|
||||
if (fromDate || toDate) {
|
||||
filtered = filtered.filter((row) =>
|
||||
isInListDateRange(row.createdAt, fromDate, toDate),
|
||||
);
|
||||
}
|
||||
|
||||
const paged = applyListQueryV2(
|
||||
enriched,
|
||||
filtered,
|
||||
{
|
||||
publicId: (r) => String((r as { publicId?: string }).publicId ?? ""),
|
||||
createdAt: (r) => (r as { createdAt?: Date }).createdAt,
|
||||
requestNo: (r) =>
|
||||
String((r as { requestNo?: string }).requestNo ?? ""),
|
||||
status: (r) => String((r as { status?: string }).status ?? ""),
|
||||
status: (r) =>
|
||||
String(
|
||||
(r as { unifiedFileStatus?: string; status?: string })
|
||||
.unifiedFileStatus ??
|
||||
(r as { status?: string }).status ??
|
||||
"",
|
||||
),
|
||||
fileType: (r) => (r as { type?: string }).type,
|
||||
searchExtras: (r) => {
|
||||
const row = r as {
|
||||
blameStatus?: string;
|
||||
@@ -7004,8 +7082,9 @@ export class RequestManagementService {
|
||||
e,
|
||||
);
|
||||
await (req as any).save();
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to process plate information.",
|
||||
if (e instanceof HttpException) throw e;
|
||||
throw new BadRequestException(
|
||||
getInquiryErrorMessage(e, "thirdPartyPlate"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7020,7 +7099,7 @@ export class RequestManagementService {
|
||||
: await this.clientService.findOne({ clientName });
|
||||
if (!client) {
|
||||
const error = new NotFoundException(
|
||||
`Client not found for company: ${clientName}`,
|
||||
"شرکت بیمه موجود در پاسخ استعلام قابل شناسایی نیست.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -7345,7 +7424,7 @@ export class RequestManagementService {
|
||||
: null;
|
||||
if (!client) {
|
||||
const error = new NotFoundException(
|
||||
`Client not found for company: ${clientName}`,
|
||||
"شرکت بیمه موجود در پاسخ استعلام قابل شناسایی نیست.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -8104,7 +8183,7 @@ export class RequestManagementService {
|
||||
|
||||
if (!client) {
|
||||
const error = new NotFoundException(
|
||||
`Client not found for company: ${clientName}`,
|
||||
"شرکت بیمه موجود در پاسخ استعلام قابل شناسایی نیست.",
|
||||
);
|
||||
await this.persistLegacyInquiryAudit(
|
||||
requestId,
|
||||
@@ -8188,8 +8267,8 @@ export class RequestManagementService {
|
||||
);
|
||||
this.logger.error("Error processing first party plate:", plateError);
|
||||
if (plateError instanceof HttpException) throw plateError;
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to process first party plate information",
|
||||
throw new BadRequestException(
|
||||
getInquiryErrorMessage(plateError, "thirdPartyPlate"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8241,7 +8320,7 @@ export class RequestManagementService {
|
||||
|
||||
if (!client) {
|
||||
const error = new NotFoundException(
|
||||
`Client not found for company: ${clientName}${companyCode ? ` (code: ${companyCode})` : ""}`,
|
||||
"شرکت بیمه موجود در پاسخ استعلام قابل شناسایی نیست.",
|
||||
);
|
||||
await this.persistLegacyInquiryAudit(
|
||||
requestId,
|
||||
@@ -8326,8 +8405,9 @@ export class RequestManagementService {
|
||||
},
|
||||
);
|
||||
this.logger.error("Error processing second party plate:", plateError);
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to process second party plate information",
|
||||
if (plateError instanceof HttpException) throw plateError;
|
||||
throw new BadRequestException(
|
||||
getInquiryErrorMessage(plateError, "thirdPartyPlate"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8584,7 +8664,7 @@ export class RequestManagementService {
|
||||
|
||||
if (!client) {
|
||||
const error = new NotFoundException(
|
||||
`Client not found for company: ${clientName}`,
|
||||
"شرکت بیمه موجود در پاسخ استعلام قابل شناسایی نیست.",
|
||||
);
|
||||
await this.persistLegacyInquiryAudit(
|
||||
requestId,
|
||||
@@ -8710,9 +8790,8 @@ export class RequestManagementService {
|
||||
},
|
||||
);
|
||||
this.logger.error("Error processing first party plate:", plateError);
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to process first party plate information",
|
||||
);
|
||||
if (plateError instanceof HttpException) throw plateError;
|
||||
throw new BadRequestException(getInquiryErrorMessage(plateError));
|
||||
}
|
||||
|
||||
// For CAR_BODY: Create expertSubmitReply
|
||||
@@ -9850,14 +9929,14 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`${roleLabel} party plate inquiry failed: ${err?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(err, "thirdPartyPlate"),
|
||||
);
|
||||
}
|
||||
|
||||
if (inquiryMapped?.Error) {
|
||||
const error = new BadRequestException(
|
||||
inquiryMapped.Error.Message ||
|
||||
`${roleLabel} party plate inquiry returned an error`,
|
||||
getInquiryErrorMessage(inquiryMapped, "thirdPartyPlate"),
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -9881,7 +9960,7 @@ export class RequestManagementService {
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
if (!clientName) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyName missing from ${roleLabel} party inquiry response`,
|
||||
"پاسخ استعلام بیمهنامه ناقص است و نام شرکت بیمه در آن وجود ندارد.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -9908,7 +9987,7 @@ export class RequestManagementService {
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyCode missing or invalid in ${roleLabel} party inquiry response`,
|
||||
"پاسخ استعلام بیمهنامه ناقص است و شرکت بیمه قابل شناسایی نیست.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -10128,7 +10207,7 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`${roleLabel} party driving license inquiry failed: ${err?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(err, "drivingLicense"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10143,7 +10222,7 @@ export class RequestManagementService {
|
||||
): Promise<void> {
|
||||
if (!String(sheba ?? "").trim()) {
|
||||
throw new BadRequestException(
|
||||
"sheba is required for the damaged party.",
|
||||
"شماره شبا برای طرف زیاندیده الزامی است.",
|
||||
);
|
||||
}
|
||||
await this.sandHubService.getShebaValidation(
|
||||
@@ -10764,14 +10843,14 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`${roleLabel} party VIN inquiry failed: ${err?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(err, "thirdPartyVin"),
|
||||
);
|
||||
}
|
||||
|
||||
if (inquiryMapped?.Error) {
|
||||
const error = new BadRequestException(
|
||||
inquiryMapped.Error.Message ||
|
||||
`${roleLabel} party VIN inquiry returned an error`,
|
||||
getInquiryErrorMessage(inquiryMapped, "thirdPartyVin"),
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -10793,7 +10872,7 @@ export class RequestManagementService {
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
if (!clientName) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyName missing from ${roleLabel} party VIN inquiry response`,
|
||||
"پاسخ استعلام VIN ناقص است و نام شرکت بیمه در آن وجود ندارد.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -10818,7 +10897,7 @@ export class RequestManagementService {
|
||||
: null;
|
||||
if (clientName && !client) {
|
||||
const error = new BadRequestException(
|
||||
`CompanyCode missing or invalid in ${roleLabel} party VIN inquiry response`,
|
||||
"پاسخ استعلام VIN ناقص است و شرکت بیمه قابل شناسایی نیست.",
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
@@ -10970,7 +11049,7 @@ export class RequestManagementService {
|
||||
err,
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
this.throwCarBodyInquiryFailure(err);
|
||||
this.throwCarBodyInquiryFailure(err, "carBodyVin");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11045,7 +11124,7 @@ export class RequestManagementService {
|
||||
);
|
||||
await this.persistBlameInquiryAudit(req);
|
||||
throw new BadRequestException(
|
||||
`${roleLabel} party driving license inquiry failed: ${err?.message || "استعلام در دسترس نیست"}`,
|
||||
getInquiryErrorMessage(err, "drivingLicense"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12756,27 +12835,124 @@ export class RequestManagementService {
|
||||
return { ...workflow, completedSteps };
|
||||
}
|
||||
|
||||
// /**
|
||||
// * V4/V5 dirty bridge: FileMaker FE resumes from blame `status`, but pre-capture
|
||||
// * document upload lives on the claim (`UPLOADING_REQUIRED_DOCUMENTS`) while blame
|
||||
// * is still at FIRST/SECOND_COMPLETED. Mirror claim status into `status` only for
|
||||
// * that phase so leave/re-enter can continue; keep real blame status as
|
||||
// * `blameCaseStatus`. Remove once FE keys off `claimStatus` / a unified resume pointer.
|
||||
// */
|
||||
// private fileMakerStatusForResume(
|
||||
// blameStatus: unknown,
|
||||
// claimStatus: unknown,
|
||||
// ): { status: unknown; blameCaseStatus?: unknown } {
|
||||
// if (claimStatus === ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS) {
|
||||
// return {
|
||||
// status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
// blameCaseStatus: blameStatus,
|
||||
// };
|
||||
// }
|
||||
// return { status: blameStatus };
|
||||
// }
|
||||
/**
|
||||
* FileMaker owns a cross-aggregate workflow: the party narrative is stored on
|
||||
* the blame case, while required-document progress is stored on its linked
|
||||
* claim. Expose the authoritative aggregate to resume instead of overloading
|
||||
* either record's status with the other record's state.
|
||||
*/
|
||||
private fileMakerResumeProjection(
|
||||
file: any,
|
||||
claim?: any,
|
||||
): FileMakerResumeProjection {
|
||||
const blameWorkflow = this.fileMakerWorkflowProjection(file);
|
||||
const narrativeTerminalStep =
|
||||
file?.type === BlameRequestType.CAR_BODY
|
||||
? WorkflowStep.FIRST_COMPLETED
|
||||
: WorkflowStep.SECOND_COMPLETED;
|
||||
const narrativeComplete =
|
||||
blameWorkflow.currentStep === narrativeTerminalStep ||
|
||||
(blameWorkflow.completedSteps ?? []).includes(narrativeTerminalStep);
|
||||
const claimWorkflow = claim?.workflow ?? {};
|
||||
|
||||
async getMyFileMakerFiles(fileMaker: any): Promise<any[]> {
|
||||
if (
|
||||
narrativeComplete &&
|
||||
claim &&
|
||||
claim.status === ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS &&
|
||||
claimWorkflow.currentStep === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||
) {
|
||||
return {
|
||||
action: "UPLOAD_REQUIRED_DOCUMENTS",
|
||||
entity: "CLAIM",
|
||||
entityId: String(claim._id),
|
||||
status: claim.status,
|
||||
currentStep: claimWorkflow.currentStep,
|
||||
nextStep: claimWorkflow.nextStep,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
narrativeComplete &&
|
||||
claim &&
|
||||
(claim.status === ClaimCaseStatus.WAITING_FOR_FILE_REVIEWER ||
|
||||
file?.status === CaseStatus.WAITING_FOR_FILE_REVIEWER)
|
||||
) {
|
||||
return {
|
||||
action: "WAIT_FOR_FILE_REVIEWER",
|
||||
entity: "CLAIM",
|
||||
entityId: String(claim._id),
|
||||
status: claim.status,
|
||||
currentStep: claimWorkflow.currentStep,
|
||||
nextStep: claimWorkflow.nextStep,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
action: "CONTINUE_BLAME",
|
||||
entity: "BLAME",
|
||||
entityId: String(file._id),
|
||||
status: file.status,
|
||||
currentStep: blameWorkflow.currentStep,
|
||||
nextStep: blameWorkflow.nextStep,
|
||||
};
|
||||
}
|
||||
|
||||
private paginateUserFacingFiles(
|
||||
rows: any[],
|
||||
query: ListQueryV2Dto,
|
||||
): GetUserBlameListV2ResponseDto {
|
||||
let filtered = rows;
|
||||
if (query.unifiedStatus) {
|
||||
filtered = filtered.filter(
|
||||
(row) => row.unifiedFileStatus === query.unifiedStatus,
|
||||
);
|
||||
}
|
||||
|
||||
const { fromDate, toDate } = parseListDateRange(
|
||||
query.startDate,
|
||||
query.endDate,
|
||||
);
|
||||
if (fromDate || toDate) {
|
||||
filtered = filtered.filter((row) =>
|
||||
isInListDateRange(row.createdAt, fromDate, toDate),
|
||||
);
|
||||
}
|
||||
|
||||
const paged = applyListQueryV2(
|
||||
filtered,
|
||||
{
|
||||
publicId: (row) => String(row.publicId ?? ""),
|
||||
createdAt: (row) => row.createdAt,
|
||||
requestNo: (row) => String(row.requestNo ?? ""),
|
||||
status: (row) => String(row.unifiedFileStatus ?? row.status ?? ""),
|
||||
fileType: (row) => row.type,
|
||||
searchExtras: (row) =>
|
||||
[
|
||||
row._id,
|
||||
row.blameStatus,
|
||||
row.claimStatus,
|
||||
row.workflow?.currentStep,
|
||||
row.claimWorkflow?.currentStep,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map(String),
|
||||
},
|
||||
query,
|
||||
);
|
||||
|
||||
return {
|
||||
list: paged.list,
|
||||
total: paged.total,
|
||||
page: paged.page,
|
||||
limit: paged.limit,
|
||||
totalPages: paged.totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
async getMyFileMakerFiles(
|
||||
fileMaker: any,
|
||||
query: ListQueryV2Dto = {},
|
||||
): Promise<GetUserBlameListV2ResponseDto> {
|
||||
if (fileMaker?.role !== RoleEnum.FILE_MAKER) {
|
||||
throw new ForbiddenException("Only FileMakers can use this endpoint.");
|
||||
}
|
||||
@@ -12785,26 +12961,27 @@ export class RequestManagementService {
|
||||
isMadeByFileMaker: true,
|
||||
initiatedByFieldExpertId: makerId,
|
||||
});
|
||||
// const blameIds = (files || []).map((f: any) => f._id).filter(Boolean);
|
||||
// const claims =
|
||||
// blameIds.length > 0
|
||||
// ? await this.claimCaseDbService.find(
|
||||
// { blameRequestId: { $in: blameIds } },
|
||||
// { lean: true, select: "blameRequestId status" },
|
||||
// )
|
||||
// : [];
|
||||
// const claimStatusByBlameId = new Map<string, unknown>();
|
||||
// for (const c of claims as any[]) {
|
||||
// const blameId = c?.blameRequestId != null ? String(c.blameRequestId) : "";
|
||||
// if (blameId) claimStatusByBlameId.set(blameId, c.status);
|
||||
// }
|
||||
const blameIds = (files || []).map((f: any) => f._id).filter(Boolean);
|
||||
const claims =
|
||||
blameIds.length > 0
|
||||
? await this.claimCaseDbService.find(
|
||||
{ blameRequestId: { $in: blameIds } },
|
||||
{
|
||||
lean: true,
|
||||
select: "blameRequestId status workflow",
|
||||
},
|
||||
)
|
||||
: [];
|
||||
const claimByBlameId = new Map<string, any>();
|
||||
for (const claim of claims as any[]) {
|
||||
const blameId =
|
||||
claim?.blameRequestId != null ? String(claim.blameRequestId) : "";
|
||||
if (blameId) claimByBlameId.set(blameId, claim);
|
||||
}
|
||||
|
||||
return (files || []).map((f: any) => {
|
||||
const list = (files || []).map((f: any) => {
|
||||
const workflow = this.fileMakerWorkflowProjection(f);
|
||||
// const resume = this.fileMakerStatusForResume(
|
||||
// f.status,
|
||||
// claimStatusByBlameId.get(String(f._id)),
|
||||
// );
|
||||
const claim = claimByBlameId.get(String(f._id));
|
||||
return {
|
||||
_id: f._id,
|
||||
publicId: f.publicId,
|
||||
@@ -12817,11 +12994,21 @@ export class RequestManagementService {
|
||||
nextStep: workflow.nextStep,
|
||||
completedSteps: workflow.completedSteps,
|
||||
},
|
||||
linkedClaimId: claim?._id ? String(claim._id) : null,
|
||||
claimStatus: claim?.status,
|
||||
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||
blameStatus: f.status,
|
||||
claimStatus: claim?.status,
|
||||
}),
|
||||
claimWorkflow: claim?.workflow,
|
||||
fileMakerResume: this.fileMakerResumeProjection(f, claim),
|
||||
requiresFileMakerApproval: f.requiresFileMakerApproval,
|
||||
createdAt: f.createdAt,
|
||||
updatedAt: f.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
return this.paginateUserFacingFiles(list, query);
|
||||
}
|
||||
|
||||
async getMyFileMakerFileDetail(
|
||||
@@ -12855,6 +13042,7 @@ export class RequestManagementService {
|
||||
: claim
|
||||
? { ...(claim as any) }
|
||||
: null;
|
||||
const fileMakerResume = this.fileMakerResumeProjection(plain, claimPlain);
|
||||
return {
|
||||
_id: plain._id,
|
||||
publicId: plain.publicId,
|
||||
@@ -12907,6 +13095,7 @@ export class RequestManagementService {
|
||||
hasSigned: p.confirmation != null,
|
||||
})),
|
||||
linkedClaimId: claimPlain ? String(claimPlain._id) : null,
|
||||
fileMakerResume,
|
||||
...(claimPlain
|
||||
? {
|
||||
claimStatus: claimPlain.status,
|
||||
@@ -12933,7 +13122,10 @@ export class RequestManagementService {
|
||||
|
||||
// ─── FileReviewer file list / detail (V4 + V5) ─────────────────────────────
|
||||
|
||||
async getMyFileReviewerFiles(fileReviewer: any): Promise<any[]> {
|
||||
async getMyFileReviewerFiles(
|
||||
fileReviewer: any,
|
||||
query: ListQueryV2Dto = {},
|
||||
): Promise<GetUserBlameListV2ResponseDto> {
|
||||
if (fileReviewer?.role !== RoleEnum.FILE_REVIEWER) {
|
||||
throw new ForbiddenException("Only FileReviewers can use this endpoint.");
|
||||
}
|
||||
@@ -12971,13 +13163,35 @@ export class RequestManagementService {
|
||||
);
|
||||
});
|
||||
|
||||
return visibleFiles.map((f: any) => ({
|
||||
const visibleBlameIds = visibleFiles
|
||||
.map((f: any) => f._id)
|
||||
.filter(Boolean);
|
||||
const claims =
|
||||
visibleBlameIds.length > 0
|
||||
? await this.claimCaseDbService.find(
|
||||
{ blameRequestId: { $in: visibleBlameIds } },
|
||||
{ lean: true, select: "blameRequestId status" },
|
||||
)
|
||||
: [];
|
||||
const claimByBlameId = new Map<string, any>();
|
||||
for (const claim of claims as any[]) {
|
||||
if (claim?.blameRequestId) {
|
||||
claimByBlameId.set(String(claim.blameRequestId), claim);
|
||||
}
|
||||
}
|
||||
|
||||
const list = visibleFiles.map((f: any) => ({
|
||||
_id: f._id,
|
||||
publicId: f.publicId,
|
||||
requestNo: f.requestNo,
|
||||
type: f.type,
|
||||
status: f.status,
|
||||
blameStatus: f.blameStatus,
|
||||
claimStatus: claimByBlameId.get(String(f._id))?.status,
|
||||
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||
blameStatus: f.status,
|
||||
claimStatus: claimByBlameId.get(String(f._id))?.status,
|
||||
}),
|
||||
workflow: {
|
||||
currentStep: f.workflow?.currentStep,
|
||||
nextStep: f.workflow?.nextStep,
|
||||
@@ -12987,6 +13201,8 @@ export class RequestManagementService {
|
||||
createdAt: f.createdAt,
|
||||
updatedAt: f.updatedAt,
|
||||
}));
|
||||
|
||||
return this.paginateUserFacingFiles(list, query);
|
||||
}
|
||||
|
||||
async getMyFileReviewerFileDetail(
|
||||
|
||||
@@ -82,7 +82,7 @@ export class RequestManagementV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "List my blame requests (V2)",
|
||||
description:
|
||||
"Party-owned blame files, or files initiated by the current FIELD_EXPERT / REGISTRAR. Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`.",
|
||||
"Party-owned blame files, or files initiated by the current FIELD_EXPERT / REGISTRAR. Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType`, `startDate`, `endDate`.",
|
||||
})
|
||||
async getAllBlameRequestsV2(
|
||||
@CurrentUser() user: any,
|
||||
|
||||
Reference in New Issue
Block a user