Files
esg/src/common/helpers/validation-error.helper.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

214 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ValidationError } from 'class-validator';
import { NormalizedErrorDto } from '../dto/normalized-error.dto';
import { buildNormalizedError } from '../constants/error-messages';
export interface ValidationFieldError {
field: string;
constraints: string[];
}
const FIELD_LABELS_FA: Record<string, string> = {
nationalCode: 'کد ملی',
postalCode: 'کد پستی',
birthDate: 'تاریخ تولد',
mobileNo: 'شماره موبایل',
sheba: 'شماره شبا',
chassisNo: 'شماره شاسی',
plk1: 'بخش اول پلاک',
plk2: 'حرف پلاک',
plk3: 'بخش سوم پلاک',
plksrl: 'سری پلاک',
email: 'ایمیل',
password: 'رمز عبور',
newPassword: 'رمز عبور جدید',
role: 'نقش کاربر',
clientType: 'نوع کاربر',
username: 'نام کاربری',
fullName: 'نام کامل',
};
const CONSTRAINT_PRIORITY = [
'isNotEmpty',
'matches',
'isLength',
'length',
'minLength',
'maxLength',
'isEmail',
'isEnum',
'isIn',
'isString',
'isInt',
'isBoolean',
'isArray',
];
function pickConstraint(constraints: Record<string, string>): string {
for (const key of CONSTRAINT_PRIORITY) {
if (constraints[key]) {
return constraints[key];
}
}
return Object.values(constraints)[0] ?? 'Invalid value';
}
function fieldLabelFa(field: string): string {
return FIELD_LABELS_FA[field] ?? field;
}
function translateValidationConstraint(field: string, constraint: string): string {
const label = fieldLabelFa(field);
if (constraint.includes('must be exactly 10 digits')) {
return `${label} باید دقیقا ۱۰ رقم باشد`;
}
if (constraint.includes('birthDate must be in YYYY-MM-DD format')) {
return 'تاریخ تولد باید با فرمت YYYY-MM-DD باشد';
}
if (constraint.includes('birthDate must be in YYYY-MM-DD or YYYYMMDD format')) {
return 'تاریخ تولد باید با فرمت YYYY-MM-DD یا YYYYMMDD باشد';
}
if (constraint.includes('mobileNo must be an Iranian mobile number')) {
return 'شماره موبایل باید معتبر و با 09 شروع شود';
}
if (constraint.includes('sheba must start with IR and contain 24 digits')) {
return 'شماره شبا باید با IR شروع شود و ۲۴ رقم داشته باشد';
}
const exactDigits = constraint.match(/must be exactly (\d+) digits/);
if (exactDigits?.[1]) {
return `${label} باید دقیقا ${exactDigits[1]} رقم باشد`;
}
const minLength = constraint.match(/must be longer than or equal to (\d+) characters/);
if (minLength?.[1]) {
return `${label} باید حداقل ${minLength[1]} کاراکتر باشد`;
}
if (constraint.includes('must be an email')) {
return 'ایمیل باید معتبر باشد';
}
if (constraint.includes('must be one of the following values')) {
return `${label} مقدار معتبری ندارد`;
}
if (constraint.includes('should not be empty')) {
return `${label} الزامی است`;
}
if (constraint.includes('must be a string')) {
return `${label} باید متن باشد`;
}
if (constraint.includes('must be an integer number')) {
return `${label} باید عدد صحیح باشد`;
}
if (constraint.includes('must be a boolean value')) {
return `${label} باید مقدار درست یا نادرست باشد`;
}
if (constraint.includes('must be an array')) {
return `${label} باید آرایه باشد`;
}
return `${label} معتبر نیست`;
}
export function flattenValidationErrors(
errors: ValidationError[],
parentPath = '',
): ValidationFieldError[] {
const result: ValidationFieldError[] = [];
for (const error of errors) {
const field = parentPath ? `${parentPath}.${error.property}` : error.property;
if (error.constraints) {
result.push({
field,
constraints: [pickConstraint(error.constraints)],
});
}
if (error.children?.length) {
result.push(...flattenValidationErrors(error.children, field));
}
}
return result;
}
export function buildValidationErrorResponse(errors: ValidationError[]): {
normalizedError: NormalizedErrorDto;
details: ValidationFieldError[];
} {
const details = flattenValidationErrors(errors).slice(0, 1);
const selected = details[0];
const summary = selected?.constraints[0] ?? 'Request validation failed';
const messageFa = selected
? translateValidationConstraint(selected.field, selected.constraints[0] ?? '')
: 'اطلاعات ورودی نامعتبر است';
return {
normalizedError: buildNormalizedError('VALIDATION_ERROR', {
message: summary,
messageFa,
details,
}),
details,
};
}
export function formatHttpExceptionMessage(exception: unknown): string {
if (!(exception && typeof exception === 'object' && 'getResponse' in exception)) {
return exception instanceof Error ? exception.message : String(exception);
}
const response = (exception as { getResponse: () => unknown }).getResponse();
if (typeof response === 'string') {
return response;
}
if (typeof response !== 'object' || response === null) {
return exception instanceof Error ? exception.message : 'Request failed';
}
const body = response as {
message?: string | string[];
error?: unknown;
};
if (isNormalizedError(body.error)) {
return body.error.message;
}
if (Array.isArray(body.message)) {
return body.message.join('; ');
}
if (typeof body.message === 'string' && body.message.length > 0) {
return body.message;
}
return exception instanceof Error ? exception.message : 'Request failed';
}
export function isNormalizedError(value: unknown): value is NormalizedErrorDto {
return (
typeof value === 'object' &&
value !== null &&
'code' in value &&
'message' in value &&
typeof (value as NormalizedErrorDto).code === 'string' &&
typeof (value as NormalizedErrorDto).message === 'string'
);
}