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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user