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

@@ -0,0 +1,220 @@
export type InquiryErrorContext =
| "thirdPartyPlate"
| "thirdPartyVin"
| "carBodyPlate"
| "carBodyVin"
| "personalIdentity"
| "drivingLicense"
| "carOwnership"
| "sheba"
| "generic";
type UnknownRecord = Record<string, unknown>;
const NOT_FOUND_MESSAGES: Record<InquiryErrorContext, string> = {
thirdPartyPlate: "بیمه‌نامه شخص ثالثی مطابق پلاک و کد ملی واردشده یافت نشد.",
thirdPartyVin:
"بیمه‌نامه شخص ثالثی مطابق شماره شاسی (VIN) و کد ملی واردشده یافت نشد.",
carBodyPlate: "بیمه‌نامه بدنه فعالی مطابق پلاک و کد ملی واردشده یافت نشد.",
carBodyVin:
"بیمه‌نامه بدنه فعالی مطابق شماره شاسی (VIN) و کد ملی واردشده یافت نشد.",
personalIdentity: "اطلاعات هویتی مطابق کد ملی و تاریخ تولد واردشده یافت نشد.",
drivingLicense:
"گواهینامه‌ای مطابق کد ملی و شماره گواهینامه واردشده یافت نشد.",
carOwnership: "مالکیتی مطابق پلاک و کد ملی واردشده یافت نشد.",
sheba: "اطلاعاتی مطابق شماره شبا و کد ملی واردشده یافت نشد.",
generic: "موردی مطابق اطلاعات واردشده یافت نشد.",
};
const INVALID_MESSAGES: Record<InquiryErrorContext, string> = {
thirdPartyPlate: "پلاک یا کد ملی واردشده برای استعلام شخص ثالث معتبر نیست.",
thirdPartyVin:
"شماره شاسی (VIN) یا کد ملی واردشده برای استعلام شخص ثالث معتبر نیست.",
carBodyPlate: "پلاک یا کد ملی واردشده برای استعلام بیمه بدنه معتبر نیست.",
carBodyVin:
"شماره شاسی (VIN) یا کد ملی واردشده برای استعلام بیمه بدنه معتبر نیست.",
personalIdentity: "کد ملی یا تاریخ تولد واردشده معتبر نیست.",
drivingLicense: "کد ملی یا شماره گواهینامه واردشده معتبر نیست.",
carOwnership: "پلاک یا کد ملی واردشده برای استعلام مالکیت معتبر نیست.",
sheba: "شماره شبا یا کد ملی واردشده معتبر نیست.",
generic: "اطلاعات ارسال‌شده برای استعلام معتبر نیست.",
};
const asRecord = (value: unknown): UnknownRecord | undefined =>
value && typeof value === "object" && !Array.isArray(value)
? (value as UnknownRecord)
: undefined;
const cleanMessage = (value: unknown): string => {
if (typeof value === "string") return value.trim();
if (Array.isArray(value)) {
return value
.filter((item): item is string => typeof item === "string")
.map((item) => item.trim())
.filter(Boolean)
.join("، ");
}
return "";
};
function errorRecords(error: unknown): UnknownRecord[] {
const root = asRecord(error);
if (!root) return [];
const response = asRecord(root.response);
const responseData = asRecord(response?.data);
const data = asRecord(root.data);
const nestedError = asRecord(root.Error) ?? asRecord(root.error);
const responseError =
asRecord(responseData?.Error) ?? asRecord(responseData?.error);
return [root, responseData, data, nestedError, responseError].filter(
(item): item is UnknownRecord => !!item,
);
}
export function inquiryErrorStatus(error: unknown): number | undefined {
for (const record of errorRecords(error)) {
const response = asRecord(record.response);
const value =
record.statusCode ?? record.status ?? response?.status ?? record.code;
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed >= 100 && parsed <= 599) {
return parsed;
}
}
return undefined;
}
export function extractInquiryProviderMessage(error: unknown): string {
for (const record of errorRecords(error)) {
for (const key of ["message", "Message", "detail", "title"] as const) {
const message = cleanMessage(record[key]);
if (message) return message;
}
const scalarData = cleanMessage(record.data);
if (scalarData) return scalarData;
}
return error instanceof Error ? error.message.trim() : cleanMessage(error);
}
export function isInquiryFailurePayload(value: unknown): boolean {
const root = asRecord(value);
if (!root) return false;
return (
root.success === false ||
root.isSuccess === false ||
root.IsSuccess === false ||
root.IsSucceed === false ||
root.ReturnValue === false ||
root.HasError === true ||
root.hasError === true ||
root.Error != null ||
root.error != null
);
}
export function isInquiryTimeout(error: unknown): boolean {
const root = asRecord(error);
const code = String(root?.code ?? "").toUpperCase();
const message = extractInquiryProviderMessage(error);
return (
["ECONNABORTED", "ETIMEDOUT", "ESOCKETTIMEDOUT"].includes(code) ||
/timeout|timed out|مهلت|زمان.*پایان/i.test(message)
);
}
const hasPersian = (value: string): boolean => /[\u0600-\u06ff]/.test(value);
const isNotFound = (error: unknown, message: string): boolean => {
const root = asRecord(error);
const responseData = asRecord(asRecord(root?.response)?.data);
const code = String(root?.code ?? responseData?.code ?? "").toUpperCase();
return (
inquiryErrorStatus(error) === 404 ||
["NOT_FOUND", "POLICY_NOT_FOUND", "NO_POLICY", "RECORD_NOT_FOUND"].includes(
code,
) ||
/\bnot[ -]?found\b|\bno (?:active |relevant )?(?:record|policy|item)\b|record\.not\.found|موردی یافت نشد|یافت نشد|پیدا نشد|فاقد بیمه(?:‌| )?نامه/i.test(
message,
)
);
};
const isInvalidInput = (message: string): boolean =>
/invalid|malformed|required|must contain|bad request|نامعتبر|الزامی|وارد نشده|صحیح نیست/i.test(
message,
);
const isUnavailable = (error: unknown, message: string): boolean => {
const root = asRecord(error);
const code = String(root?.code ?? "").toUpperCase();
const status = inquiryErrorStatus(error);
return (
isInquiryTimeout(error) ||
["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ERR_NETWORK"].includes(code) ||
status === 401 ||
status === 502 ||
status === 503 ||
status === 504 ||
/offline|unavailable|connection|socket|network|authentication|credentials|empty response|در دسترس نیست|عدم دسترسی/i.test(
message,
)
);
};
/**
* Converts provider and transport failures into a stable, user-facing Persian
* message. Provider details remain in server logs; raw English or technical
* messages are never returned to clients.
*/
export function getInquiryErrorMessage(
error: unknown,
context: InquiryErrorContext = "generic",
): string {
const providerMessage = extractInquiryProviderMessage(error);
if (isNotFound(error, providerMessage)) return NOT_FOUND_MESSAGES[context];
if (
context === "carOwnership" &&
/not the owner|مالک.*نیست/i.test(providerMessage)
) {
return "پلاک واردشده متعلق به کد ملی واردشده نیست.";
}
if (
context === "sheba" &&
/does not match|تطابق ندارد/i.test(providerMessage)
) {
return "شماره شبا متعلق به کد ملی واردشده نیست.";
}
if (
context === "drivingLicense" &&
/not valid|نامعتبر/i.test(providerMessage)
) {
return "گواهینامه واردشده معتبر نیست.";
}
if (
/policy insurer does not match|بیمه.*متعلق.*نیست/i.test(providerMessage)
) {
return "بیمه‌نامه یافت‌شده متعلق به شرکت بیمه این سامانه نیست.";
}
if (isInquiryTimeout(error)) {
return "زمان پاسخ‌گویی سرویس استعلام به پایان رسید. لطفاً دوباره تلاش کنید.";
}
if (isUnavailable(error, providerMessage)) {
return "سرویس استعلام در دسترس نیست. لطفاً کمی بعد دوباره تلاش کنید.";
}
// A provider's specific Persian validation/business message is already safe
// and more useful than replacing it with a broad local validation message.
if (providerMessage && hasPersian(providerMessage)) return providerMessage;
if (isInvalidInput(providerMessage) || inquiryErrorStatus(error) === 422) {
return INVALID_MESSAGES[context];
}
return "انجام استعلام با خطا مواجه شد. لطفاً دوباره تلاش کنید.";
}