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:
220
src/common/utils/inquiry-error.ts
Normal file
220
src/common/utils/inquiry-error.ts
Normal 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 "انجام استعلام با خطا مواجه شد. لطفاً دوباره تلاش کنید.";
|
||||
}
|
||||
Reference in New Issue
Block a user