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:
@@ -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/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) },
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user