export type InquiryErrorContext = | "thirdPartyPlate" | "thirdPartyVin" | "carBodyPlate" | "carBodyVin" | "personalIdentity" | "drivingLicense" | "carOwnership" | "sheba" | "generic"; type UnknownRecord = Record; const NOT_FOUND_MESSAGES: Record = { thirdPartyPlate: "بیمه‌نامه شخص ثالثی مطابق پلاک و کد ملی واردشده یافت نشد.", thirdPartyVin: "بیمه‌نامه شخص ثالثی مطابق شماره شاسی (VIN) و کد ملی واردشده یافت نشد.", carBodyPlate: "بیمه‌نامه بدنه فعالی مطابق پلاک و کد ملی واردشده یافت نشد.", carBodyVin: "بیمه‌نامه بدنه فعالی مطابق شماره شاسی (VIN) و کد ملی واردشده یافت نشد.", personalIdentity: "اطلاعات هویتی مطابق کد ملی و تاریخ تولد واردشده یافت نشد.", drivingLicense: "گواهینامه‌ای مطابق کد ملی و شماره گواهینامه واردشده یافت نشد.", carOwnership: "مالکیتی مطابق پلاک و کد ملی واردشده یافت نشد.", sheba: "اطلاعاتی مطابق شماره شبا و کد ملی واردشده یافت نشد.", generic: "موردی مطابق اطلاعات واردشده یافت نشد.", }; const INVALID_MESSAGES: Record = { 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 { const records = errorRecords(error); // The normalized ESG/Parsian envelope carries the safe user-facing text in // messageFa while `message` and `providerMessage` may remain technical. // Search every envelope level for that explicit Persian field before // considering generic message fields on an outer object. for (const record of records) { for (const key of [ "messageFa", "MessageFa", "messageFA", "persianMessage", ] as const) { const message = cleanMessage(record[key]); if (message) return message; } } for (const record of records) { 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 codes = errorRecords(error).flatMap((record) => [record.code, record.providerCode] .map((code) => String(code ?? "").toUpperCase()) .filter(Boolean), ); return ( inquiryErrorStatus(error) === 404 || codes.some((code) => [ "NOT_FOUND", "POLICY_NOT_FOUND", "NO_POLICY", "RECORD_NOT_FOUND", "INQUIRY_NO_MATCH", ].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); // Persian text supplied by the provider is already the intended client // message. Preserve it verbatim instead of replacing it with a local // contextual fallback such as "inquiry not found". if (providerMessage && hasPersian(providerMessage)) return providerMessage; 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 "سرویس استعلام در دسترس نیست. لطفاً کمی بعد دوباره تلاش کنید."; } if (isInvalidInput(providerMessage) || inquiryErrorStatus(error) === 422) { return INVALID_MESSAGES[context]; } return "انجام استعلام با خطا مواجه شد. لطفاً دوباره تلاش کنید."; }