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:
2026-09-18 16:04:33 +03:30
parent a84d83a135
commit 7d1a50db7b
38 changed files with 2345 additions and 370 deletions

View File

@@ -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(