forked from Shared/esg
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)
214 lines
5.9 KiB
TypeScript
214 lines
5.9 KiB
TypeScript
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'
|
||
);
|
||
}
|