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

@@ -462,4 +462,128 @@ describe("buildInsurerFileReport", () => {
"پایه یک",
);
});
it("localizes participant and recent-transfer inquiry data", () => {
const report = buildInsurerFileReport({
overview: { publicId: "A00017" },
blame: {
type: "THIRD_PARTY",
parties: [
{
role: "FIRST",
person: { userId: "guilty", fullName: "مقصر" },
},
{
role: "SECOND",
person: { userId: "damaged", fullName: "زیان‌دیده" },
participants: [
{
participantId: "DRIVER",
fullName: "راننده",
nationalCode: "0012345678",
birthday: "1370/01/01",
hasDrivingLicense: true,
licenseNumber: "123456789",
licenseType: "BASE_2",
},
],
participantRoles: {
driver: "DRIVER",
vehicleOwner: "DRIVER",
thirdPartyPolicyholder: "DRIVER",
},
vehicle: {
registrationState: "RECENTLY_TRANSFERRED",
previousPlateId: "55ج222ایران33",
previousPolicyholderNationalCode: "0098765432",
currentPlate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
inquiry: {
plateKind: "PREVIOUS",
attempts: [
{
plateKind: "CURRENT",
plate: {
leftDigits: "44",
centerAlphabet: "ب",
centerDigits: "111",
ir: "22",
},
succeeded: false,
error: "not found",
},
{
plateKind: "PREVIOUS",
succeeded: true,
usable: true,
},
],
},
},
},
],
expert: { decision: { guiltyPartyId: "guilty" } },
},
claim: {
vehicle: { carType: "SEDAN" },
},
});
expect(
getFieldValue(
report,
PR.damagedVehicleSection,
"خودرو / وضعیت پلاک و مالکیت",
),
).toBe("انتقال مالکیت اخیر");
expect(
getFieldValue(report, PR.damagedVehicleSection, "خودرو / نوع خودرو"),
).toBe("سواری");
expect(
getFieldValue(report, PR.damagedVehicleSection, "خودرو / پلاک قبلی"),
).toBe("55ج222ایران33");
expect(
getFieldValue(
report,
PR.damagedVehicleSection,
"خودرو / پلاک فعلی / دو رقم چپ پلاک",
),
).toBe("44");
const driver = getFieldValue(
report,
PR.damagedParticipantsSection,
PR.driverRole,
);
expect(driver).toContain("گواهینامه دارد: بله");
expect(driver).toContain("نوع گواهینامه: پایه دو");
expect(
getFieldValue(
report,
PR.damagedVehicleSection,
"خودرو / سوابق تلاش استعلام / تلاش ۱ / نوع پلاک استعلام‌شده",
),
).toBe("پلاک فعلی");
expect(
getFieldValue(
report,
PR.damagedVehicleSection,
"خودرو / سوابق تلاش استعلام / تلاش ۱ / خطا",
),
).toBe("موردی یافت نشد");
const renderedText = report.sections
.flatMap((section) => [
section.title,
...section.fields.flatMap((field) => [field.label, field.value]),
])
.join(" ");
expect(renderedText).not.toMatch(
/RECENTLY_TRANSFERRED|BASE_2|registrationState|previousPlateId|plateKind|not found|CURRENT|PREVIOUS/,
);
});
});

View File

@@ -14,6 +14,7 @@ import {
PR,
persianAccidentCondition,
persianFieldPath,
persianReportValue,
persianStatus,
} from "./persian-report-labels";
@@ -110,10 +111,13 @@ function firstDefined(...values: unknown[]): string | undefined {
return undefined;
}
function normalizeListValue(value: unknown): string | undefined {
function normalizeListValue(value: unknown, path = ""): string | undefined {
if (!Array.isArray(value) || !value.length) return undefined;
const items = value
.map((item) => asString(item) ?? JSON.stringify(item))
.map(
(item) =>
asString(persianReportValue(path, item)) ?? JSON.stringify(item),
)
.filter(Boolean);
return items.length ? items.join("، ") : undefined;
}
@@ -124,18 +128,31 @@ function flattenObject(
depth = 0,
): InsurerFileReportField[] {
if (obj == null) return [];
if (depth > 4) {
return [{ label: persianFieldPath(prefix), value: asString(obj) }];
if (depth > 6) {
return [
{
label: persianFieldPath(prefix),
value: asString(persianReportValue(prefix, obj)),
},
];
}
if (Array.isArray(obj)) {
const value = normalizeListValue(obj);
if (obj.some((item) => item && typeof item === "object")) {
return obj.flatMap((item, index) =>
flattenObject(item, `${prefix}.${index + 1}`, depth + 1),
);
}
const value = normalizeListValue(obj, prefix);
return value ? [{ label: persianFieldPath(prefix || "items"), value }] : [];
}
if (typeof obj !== "object") {
return [
{ label: persianFieldPath(prefix || "value"), value: asString(obj) },
{
label: persianFieldPath(prefix || "value"),
value: asString(persianReportValue(prefix || "value", obj)),
},
];
}
@@ -150,14 +167,13 @@ function flattenObject(
if (value === undefined || value === null || value === "") continue;
const path = prefix ? `${prefix}.${key}` : key;
if (
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Date)
) {
if (typeof value === "object" && !(value instanceof Date)) {
rows.push(...flattenObject(value, path, depth + 1));
} else {
rows.push({ label: persianFieldPath(path), value: asString(value) });
rows.push({
label: persianFieldPath(path),
value: asString(persianReportValue(path, value)),
});
}
}
@@ -638,6 +654,17 @@ function buildParticipantRolesSection(
participant?.licenseNumber
? `${PR.licenseNumber}: ${asString(participant.licenseNumber)}`
: undefined,
participant?.hasDrivingLicense != null
? `${PR.hasDrivingLicense}: ${persianStatus(participant.hasDrivingLicense)}`
: undefined,
participant?.licenseType
? `${PR.licenseType}: ${asString(
persianReportValue(
"participant.licenseType",
participant.licenseType,
),
)}`
: undefined,
]
.filter(Boolean)
.join("، ");
@@ -695,8 +722,12 @@ function buildDriverSection(
{
label: PR.licenseType,
value:
licenseType ??
asString(person.licenseType) ??
asString(
persianReportValue(
"participant.licenseType",
licenseType ?? person.licenseType,
),
) ??
(person.driverLicense ? PR.driverLicense : undefined),
},
{
@@ -895,7 +926,7 @@ function evaluationDaghiValue(value: unknown): string | undefined {
return asString(value);
}
const daghi = value as ReportRecord;
const option = asString(daghi.option);
const option = asString(persianReportValue("daghi.option", daghi.option));
const price = formatToman(daghi.price);
return [option, price].filter(Boolean).join(" - ") || undefined;
}
@@ -915,7 +946,9 @@ function buildEvaluationPartsSection(
{ label: `${prefix} / ${PR.partName}`, value: evaluationPartName(part) },
{
label: `${prefix} / ${PR.damageType}`,
value: asString(part.typeOfDamage),
value: asString(
persianReportValue("evaluation.part.typeOfDamage", part.typeOfDamage),
),
},
{ label: `${prefix} / ${PR.partPrice}`, value: formatToman(part.price) },
{

View File

@@ -29,6 +29,7 @@ export const PR = {
licenseType: "نوع گواهینامه",
licenseDate: "تاریخ گواهینامه",
licenseNumber: "شماره گواهینامه",
hasDrivingLicense: "گواهینامه دارد",
driverLicense: "گواهینامه راننده",
insuranceCompany: "شرکت بیمه",
policyNumber: "شماره بیمه‌نامه",
@@ -194,6 +195,31 @@ const KEY_LABELS: Record<string, string> = {
StatusTypeCode: "کد وضعیت",
label_fa: "برچسب فارسی",
catalogKey: "کلید کاتالوگ",
participantId: "شناسه شخص",
participants: "اشخاص استعلام",
participantRoles: "نقش‌های اشخاص",
nationalCode: "کد ملی",
birthday: "تاریخ تولد",
fullName: "نام و نام خانوادگی",
phoneNumber: "شماره تلفن",
hasDrivingLicense: "گواهینامه دارد",
licenseNumber: "شماره گواهینامه",
registrationState: "وضعیت پلاک و مالکیت",
currentPlate: "پلاک فعلی",
previousPlate: "پلاک قبلی",
previousPlateId: "پلاک قبلی",
previousPolicyholderNationalCode: "کد ملی بیمه‌گذار پلاک قبلی",
vehicleVin: "شماره شاسی (VIN)",
driver: "راننده",
vehicleOwner: "مالک وسیله نقلیه",
thirdPartyPolicyholder: "بیمه‌گذار شخص ثالث",
carBodyPolicyholder: "بیمه‌گذار بدنه",
sameAs: "همان شخص",
plateKind: "نوع پلاک استعلام‌شده",
succeeded: "موفق",
usable: "قابل استفاده",
attempts: "سوابق تلاش استعلام",
error: "خطا",
};
const STATUS_LABELS: Record<string, string> = {
@@ -209,6 +235,76 @@ const STATUS_LABELS: Record<string, string> = {
false: "خیر",
};
const REGISTRATION_STATE_LABELS: Record<string, string> = {
CURRENT: "عادی (پلاک فعلی)",
RECENTLY_TRANSFERRED: "انتقال مالکیت اخیر",
};
const PLATE_KIND_LABELS: Record<string, string> = {
CURRENT: "پلاک فعلی",
PREVIOUS: "پلاک قبلی",
};
const PARTICIPANT_ROLE_VALUE_LABELS: Record<string, string> = {
DRIVER: "راننده",
VEHICLE_OWNER: "مالک وسیله نقلیه",
THIRD_PARTY_POLICYHOLDER: "بیمه‌گذار شخص ثالث",
CAR_BODY_POLICYHOLDER: "بیمه‌گذار بدنه",
FIRST: "طرف اول",
SECOND: "طرف دوم",
};
const CASE_TYPE_LABELS: Record<string, string> = {
THIRD_PARTY: "شخص ثالث",
CAR_BODY: "بدنه",
};
const VEHICLE_TYPE_LABELS: Record<string, string> = {
SEDAN: "سواری",
SUV: "شاسی‌بلند",
HATCHBACK: "هاچ‌بک",
PICKUP: "وانت",
VAN: "ون",
};
const VALIDITY_LABELS: Record<string, string> = {
ACTIVE: "فعال",
INACTIVE: "غیرفعال",
VALID: "معتبر",
INVALID: "نامعتبر",
EXPIRED: "منقضی‌شده",
};
const DAMAGE_TYPE_LABELS: Record<string, string> = {
REPAIR: "تعمیر",
CHANGE: "تعویض",
REPLACE: "تعویض",
};
const INQUIRY_ERROR_LABELS: Record<string, string> = {
NOT_FOUND: "موردی یافت نشد",
NO_RECORD_FOUND: "موردی یافت نشد",
TIMEOUT: "مهلت پاسخ استعلام به پایان رسید",
REQUEST_FAILED: "استعلام ناموفق بود",
FAILED: "استعلام ناموفق بود",
UNAVAILABLE: "سرویس استعلام در دسترس نیست",
};
const LICENSE_TYPE_LABELS: Record<string, string> = {
"1": "پایه یک",
"2": "پایه دو",
"3": "پایه سه",
BASE_1: "پایه یک",
BASE1: "پایه یک",
GRADE_1: "پایه یک",
BASE_2: "پایه دو",
BASE2: "پایه دو",
GRADE_2: "پایه دو",
BASE_3: "پایه سه",
BASE3: "پایه سه",
GRADE_3: "پایه سه",
};
const WEATHER_LABELS: Record<string, string> = {
clear: "صاف",
sunny: "صاف",
@@ -255,9 +351,39 @@ export function persianFieldPath(path: string): string {
return `بیمه / ${translatedLast}`;
}
if (parts[0] === "claim" && parts[1] === "vehicle") {
const attemptIndex = parts.indexOf("attempts");
if (attemptIndex >= 0) {
const attemptNumber = Number(parts[attemptIndex + 1]);
const attemptLabel = Number.isFinite(attemptNumber)
? `تلاش ${attemptNumber.toLocaleString("fa-IR")}`
: "تلاش استعلام";
const plateLabel = parts.includes("plate") ? " / پلاک" : "";
return `خودرو / سوابق تلاش استعلام / ${attemptLabel}${plateLabel} / ${translatedLast}`;
}
if (parts.includes("currentPlate")) {
return `خودرو / پلاک فعلی / ${translatedLast}`;
}
if (parts.includes("previousPlate")) {
return `خودرو / پلاک قبلی / ${translatedLast}`;
}
return `خودرو / ${translatedLast}`;
}
if (parts[0] === "party" && parts[1] === "vehicle") {
const attemptIndex = parts.indexOf("attempts");
if (attemptIndex >= 0) {
const attemptNumber = Number(parts[attemptIndex + 1]);
const attemptLabel = Number.isFinite(attemptNumber)
? `تلاش ${attemptNumber.toLocaleString("fa-IR")}`
: "تلاش استعلام";
const plateLabel = parts.includes("plate") ? " / پلاک" : "";
return `خودرو / سوابق تلاش استعلام / ${attemptLabel}${plateLabel} / ${translatedLast}`;
}
if (parts.includes("currentPlate")) {
return `خودرو / پلاک فعلی / ${translatedLast}`;
}
if (parts.includes("previousPlate")) {
return `خودرو / پلاک قبلی / ${translatedLast}`;
}
return `خودرو / ${translatedLast}`;
}
@@ -267,6 +393,51 @@ export function persianFieldPath(path: string): string {
return translated.join(" / ");
}
/**
* Localize only known domain tokens. Free-form provider values, names,
* identifiers, plates and VINs are deliberately preserved verbatim.
*/
export function persianReportValue(
path: string,
value: unknown,
): unknown {
if (value === undefined || value === null || value === "") return value;
if (typeof value === "boolean") return value ? "بله" : "خیر";
if (typeof value !== "string" && typeof value !== "number") return value;
const raw = String(value).trim();
const key = raw.toUpperCase().replace(/[\s-]+/g, "_");
const field = path.split(".").filter(Boolean).pop() ?? "";
if (field === "licenseType" || field === "LicenseType") {
return LICENSE_TYPE_LABELS[key] ?? raw;
}
if (field === "registrationState") {
return REGISTRATION_STATE_LABELS[key] ?? raw;
}
if (field === "plateKind") return PLATE_KIND_LABELS[key] ?? raw;
if (field === "role" || field.endsWith("Role")) {
return PARTICIPANT_ROLE_VALUE_LABELS[key] ?? raw;
}
if (field === "blameRequestType" || field === "fileType") {
return CASE_TYPE_LABELS[key] ?? raw;
}
if (
(field === "carType" || field === "type") &&
path.toLowerCase().includes("vehicle")
) {
return VEHICLE_TYPE_LABELS[key] ?? raw;
}
if (field === "typeOfDamage") return DAMAGE_TYPE_LABELS[key] ?? raw;
if (field === "error") return INQUIRY_ERROR_LABELS[key] ?? raw;
if (/status$/i.test(field)) {
return STATUS_LABELS[raw] ?? VALIDITY_LABELS[key] ?? raw;
}
if (raw === "true" || raw === "false") return STATUS_LABELS[raw];
return raw;
}
export function persianStatus(value: unknown): string | undefined {
if (value === undefined || value === null || value === "") return undefined;
const key = String(value);