Files
esg/src/common/constants/error-messages.ts
s.hajizadeh 45ef396466 feat: add Persian error translations and comprehensive test suite
Implement normalized error handling with Persian translations across all
exception types, replace legacy NestJS exceptions with AppException,
and add unit, integration, and smoke tests.

- Add error catalog with gateway-owned codes and Persian messages
- Introduce AppException wrapping normalized error envelopes
- Add translateError helper for automatic messageFa population
- Remove claims module and update provider error normalization
- Add unit tests for error contracts and helper functions
- Add integration tests for admin and inquiry endpoints
- Add smoke tests for real provider connectivity
- Add test support utilities (auth mocks, assertions, app factory)
2026-06-29 17:25:19 +03:30

375 lines
13 KiB
TypeScript

import { HttpStatus } from '@nestjs/common';
import { NormalizedErrorDto } from '../dto/normalized-error.dto';
export interface ErrorCatalogEntry {
status: HttpStatus;
message: string;
messageFa: string;
}
export const ERROR_CATALOG = {
PROVIDER_TIMEOUT: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider request timed out',
messageFa: 'درخواست به سرویس‌دهنده زمان‌بر شد',
},
PROVIDER_ERROR: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider request failed',
messageFa: 'خطا در سرویس‌دهنده',
},
PROVIDER_NETWORK_ERROR: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider network request failed',
messageFa: 'ارتباط با سرویس‌دهنده برقرار نشد',
},
PROVIDER_BAD_RESPONSE: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider returned an invalid response',
messageFa: 'پاسخ سرویس‌دهنده معتبر نیست',
},
PROVIDER_AUTH_FAILED: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider authentication failed',
messageFa: 'احراز هویت سرویس‌دهنده ناموفق بود',
},
PROVIDER_REJECTED: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider rejected the request',
messageFa: 'سرویس‌دهنده درخواست را رد کرد',
},
NO_PROVIDERS: {
status: HttpStatus.UNPROCESSABLE_ENTITY,
message: 'No enabled providers configured for this inquiry',
messageFa: 'سرویس‌دهنده‌ای برای این استعلام فعال نیست',
},
ALL_PROVIDERS_FAILED: {
status: HttpStatus.UNPROCESSABLE_ENTITY,
message: 'All providers failed',
messageFa: 'تمامی سرویس‌دهنده‌ها با خطا مواجه شدند',
},
UNKNOWN_ERROR: {
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Unknown provider error',
messageFa: 'خطای ناشناخته در سرویس‌دهنده',
},
UNSUPPORTED_INQUIRY: {
status: HttpStatus.UNPROCESSABLE_ENTITY,
message: 'Provider does not support this inquiry type',
messageFa: 'سرویس‌دهنده از این نوع استعلام پشتیبانی نمی‌کند',
},
API_ERROR: {
status: HttpStatus.BAD_GATEWAY,
message: 'Provider API request failed',
messageFa: 'خطا در فراخوانی API سرویس‌دهنده',
},
HTTP_ERROR: {
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'HTTP request failed',
messageFa: 'درخواست ناموفق بود',
},
INTERNAL_ERROR: {
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Internal server error',
messageFa: 'خطای داخلی سرور',
},
NETWORK_ERROR: {
status: HttpStatus.BAD_GATEWAY,
message: 'Network request failed',
messageFa: 'خطا در اتصال به شبکه',
},
INQUIRY_FAILED: {
status: HttpStatus.UNPROCESSABLE_ENTITY,
message: 'Inquiry failed',
messageFa: 'استعلام با خطا مواجه شد',
},
INQUIRY_NO_MATCH: {
status: HttpStatus.OK,
message: 'Inquiry returned no matching result',
messageFa: 'نتیجه‌ای مطابق با اطلاعات وارد شده یافت نشد',
},
RECORD_NOT_FOUND: {
status: HttpStatus.OK,
message: 'Record not found',
messageFa: 'رکوردی یافت نشد',
},
SAYAH_ERROR: {
status: HttpStatus.BAD_GATEWAY,
message: 'Sayah inquiry failed',
messageFa: 'خطا در سرویس سیاح',
},
SHEBA_MISMATCH: {
status: HttpStatus.OK,
message: 'Submitted sheba does not match inquiry result',
messageFa: 'اطلاعات شبای وارد شده با نتیجه استعلام مطابقت ندارد',
},
VALIDATION_ERROR: {
status: HttpStatus.BAD_REQUEST,
message: 'Request validation failed',
messageFa: 'اطلاعات وارد شده معتبر نیست',
},
INVALID_CREDENTIALS: {
status: HttpStatus.UNAUTHORIZED,
message: 'Invalid credentials',
messageFa: 'نام کاربری یا رمز عبور صحیح نیست',
},
ACCOUNT_INACTIVE: {
status: HttpStatus.FORBIDDEN,
message: 'Account is inactive',
messageFa: 'حساب کاربری غیرفعال است',
},
ACCOUNT_BLOCKED: {
status: HttpStatus.FORBIDDEN,
message: 'Account is blocked',
messageFa: 'حساب کاربری مسدود شده است',
},
INVALID_REFRESH_TOKEN: {
status: HttpStatus.UNAUTHORIZED,
message: 'Invalid refresh token',
messageFa: 'توکن نوسازی نامعتبر است',
},
API_KEY_NOT_CONFIGURED: {
status: HttpStatus.UNAUTHORIZED,
message: 'API key authentication is not configured',
messageFa: 'احراز هویت با کلید API پیکربندی نشده است',
},
INVALID_API_KEY: {
status: HttpStatus.UNAUTHORIZED,
message: 'Invalid or missing API key',
messageFa: 'کلید API نامعتبر است یا ارسال نشده است',
},
UNAUTHORIZED: {
status: HttpStatus.UNAUTHORIZED,
message: 'Unauthorized',
messageFa: 'احراز هویت انجام نشده است',
},
AUTHENTICATION_REQUIRED: {
status: HttpStatus.FORBIDDEN,
message: 'Authentication required',
messageFa: 'احراز هویت الزامی است',
},
INSUFFICIENT_ROLE: {
status: HttpStatus.FORBIDDEN,
message: 'Insufficient role permissions',
messageFa: 'سطح دسترسی کافی نیست',
},
INSUFFICIENT_USER_MANAGEMENT_PERMISSION: {
status: HttpStatus.FORBIDDEN,
message: 'Insufficient permissions to manage users',
messageFa: 'دسترسی کافی برای مدیریت کاربران وجود ندارد',
},
SUPER_ADMIN_ROLE_REQUIRED: {
status: HttpStatus.FORBIDDEN,
message: 'Only SUPER_ADMIN can assign SUPER_ADMIN role',
messageFa: 'فقط مدیر ارشد می‌تواند نقش مدیر ارشد را اختصاص دهد',
},
USER_NOT_FOUND: {
status: HttpStatus.NOT_FOUND,
message: 'User not found',
messageFa: 'کاربر یافت نشد',
},
USERNAME_OR_EMAIL_EXISTS: {
status: HttpStatus.CONFLICT,
message: 'Username or email already exists',
messageFa: 'نام کاربری یا ایمیل قبلا ثبت شده است',
},
EMAIL_ALREADY_IN_USE: {
status: HttpStatus.CONFLICT,
message: 'Email already in use',
messageFa: 'این ایمیل قبلا استفاده شده است',
},
PASSWORD_TOO_SHORT: {
status: HttpStatus.CONFLICT,
message: 'Password must be at least 8 characters',
messageFa: 'رمز عبور باید حداقل ۸ کاراکتر باشد',
},
CANNOT_BLOCK_SELF: {
status: HttpStatus.BAD_REQUEST,
message: 'Cannot block your own account',
messageFa: 'امکان مسدود کردن حساب کاربری خودتان وجود ندارد',
},
RATE_LIMIT_EXCEEDED: {
status: HttpStatus.TOO_MANY_REQUESTS,
message: 'User rate limit exceeded',
messageFa: 'تعداد درخواست‌های مجاز کاربر به پایان رسیده است',
},
SHAHKAR_NO_MATCH: {
status: HttpStatus.OK,
message: 'No match found between the provided national code and mobile number',
messageFa: 'تطبیقی بین کد ملی و شماره موبایل وارد شده یافت نشد',
},
CAR_POLICY_NOT_FOUND: {
status: HttpStatus.OK,
message: 'No policy record found',
messageFa: 'سوابق بیمه‌نامه خودرو یافت نشد',
},
REAL_ESTATE_NOT_FOUND: {
status: HttpStatus.OK,
message: 'No property ownership found for the provided national code and postal code',
messageFa: 'ملکی برای کد ملی و کد پستی وارد شده یافت نشد',
},
REQUEST_FAILED: {
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Request failed',
messageFa: 'درخواست ناموفق بود',
},
} as const satisfies Record<string, ErrorCatalogEntry>;
export type AppErrorCode = keyof typeof ERROR_CATALOG;
export function isAppErrorCode(code: string): code is AppErrorCode {
return code in ERROR_CATALOG;
}
export function getErrorStatus(code: AppErrorCode): HttpStatus {
return ERROR_CATALOG[code].status;
}
export function buildNormalizedError(
code: AppErrorCode,
overrides: Partial<Omit<NormalizedErrorDto, 'code'>> = {},
): NormalizedErrorDto {
const entry = ERROR_CATALOG[code];
return {
code,
message: overrides.message ?? entry.message,
messageFa: overrides.messageFa ?? entry.messageFa,
providerMessage: overrides.providerMessage,
providerCode: overrides.providerCode,
providerTrackingCode: overrides.providerTrackingCode,
details: overrides.details,
conflict: overrides.conflict,
};
}
/**
* Resolve a Persian error message for a given code.
*
* @param code The NormalizedErrorDto.code value
* @param params Optional interpolation params (e.g. { provider, inquiryType })
* @returns The Persian message or undefined when no translation exists
*/
export function resolveErrorMessageFa(
code: string,
params?: Record<string, string>,
): string | undefined {
const entry = ERROR_CATALOG[code as AppErrorCode];
if (!entry) {
return undefined;
}
void params;
return entry.messageFa;
}
/**
* Map well-known provider error messages / codes that originate
* from upstream providers (returned as providerMessage) to their
* Persian equivalents when the provider itself does not return a
* Persian message.
*/
export const PROVIDER_MESSAGE_TRANSLATIONS: Record<string, string> = {
'No match found between the provided national code and mobile number':
'تطبیقی بین کد ملی و شماره موبایل وارد شده یافت نشد',
'Shahkar inquiry failed': 'خطا در استعلام شاهکار',
'No policy record found': 'سوابق بیمه‌نامه خودرو یافت نشد',
'Record not found': 'رکوردی یافت نشد',
'Request failed': 'درخواست ناموفق بود',
'No property ownership found for the provided national code and postal code':
'ملکی برای کد ملی و کد پستی وارد شده یافت نشد',
'Inquiry returned no matching result': 'نتیجه‌ای مطابق با اطلاعات وارد شده یافت نشد',
'Insufficient role permissions': 'سطح دسترسی کافی نیست',
'Invalid credentials': 'نام کاربری یا رمز عبور صحیح نیست',
'Account is inactive': 'حساب کاربری غیرفعال است',
'Account is blocked': 'حساب کاربری مسدود شده است',
'Invalid refresh token': 'توکن نوسازی نامعتبر است',
};
export const PROVIDER_PUBLIC_MESSAGE_TRANSLATIONS: Record<
string,
{ message: string; messageFa: string }
> = {
'کد ملی اشتباه است': {
message: 'Invalid national code',
messageFa: 'کد ملی اشتباه است',
},
'کدملی اشتباه است': {
message: 'Invalid national code',
messageFa: 'کد ملی اشتباه است',
},
};
export const MESSAGE_CODE_MAP: Record<string, AppErrorCode> = {
'Invalid credentials': 'INVALID_CREDENTIALS',
'Account is inactive': 'ACCOUNT_INACTIVE',
'Account is blocked': 'ACCOUNT_BLOCKED',
'Invalid refresh token': 'INVALID_REFRESH_TOKEN',
'API key authentication is not configured': 'API_KEY_NOT_CONFIGURED',
'Invalid or missing API key': 'INVALID_API_KEY',
Unauthorized: 'UNAUTHORIZED',
'Authentication required': 'AUTHENTICATION_REQUIRED',
'Insufficient role permissions': 'INSUFFICIENT_ROLE',
'Insufficient permissions to manage users': 'INSUFFICIENT_USER_MANAGEMENT_PERMISSION',
'Only SUPER_ADMIN can assign SUPER_ADMIN role': 'SUPER_ADMIN_ROLE_REQUIRED',
'User not found': 'USER_NOT_FOUND',
'Username or email already exists': 'USERNAME_OR_EMAIL_EXISTS',
'Email already in use': 'EMAIL_ALREADY_IN_USE',
'Password must be at least 8 characters': 'PASSWORD_TOO_SHORT',
'Cannot block your own account': 'CANNOT_BLOCK_SELF',
'User rate limit exceeded': 'RATE_LIMIT_EXCEEDED',
};
export function resolveErrorCodeFromMessage(message: string): AppErrorCode | undefined {
return MESSAGE_CODE_MAP[message.trim()];
}
/**
* Try to translate an upstream provider message to Persian.
*
* @param providerMessage The English message returned by the provider
* @returns The Persian equivalent or undefined when no mapping exists
*/
export function translateProviderMessage(providerMessage: string): string | undefined {
return PROVIDER_MESSAGE_TRANSLATIONS[providerMessage.trim()];
}
export function resolveProviderPublicMessage(
providerMessage: string | undefined,
fallbackMessage = 'Provider request failed',
): { message: string; messageFa: string } {
const trimmed = providerMessage?.trim();
if (!trimmed) {
return {
message: fallbackMessage,
messageFa: ERROR_CATALOG.PROVIDER_ERROR.messageFa,
};
}
const direct = PROVIDER_PUBLIC_MESSAGE_TRANSLATIONS[trimmed];
if (direct) {
return direct;
}
const messageFa = translateProviderMessage(trimmed);
if (messageFa) {
return {
message: trimmed,
messageFa,
};
}
if (/[آ-ی]/.test(trimmed)) {
return {
message: fallbackMessage,
messageFa: trimmed,
};
}
return {
message: trimmed,
messageFa: ERROR_CATALOG.PROVIDER_ERROR.messageFa,
};
}