import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; import { isValidPhoneNumber, parsePhoneNumberFromString } from 'libphonenumber-js'; /** * Country-aware phone validation. The value is expected as a full international * number (E.164, e.g. "+251911223344"), so the country is derived from the * value itself — no separate country field needed. */ @ValidatorConstraint({ name: 'IsValidPhone', async: false }) export class IsValidPhoneConstraint implements ValidatorConstraintInterface { validate(value: unknown): boolean { // Empty is allowed here; pair with @IsOptional / @IsNotEmpty as needed. if (value === undefined || value === null || value === '') return true; if (typeof value !== 'string') return false; return isValidPhoneNumber(value); } defaultMessage(args: ValidationArguments): string { return `${args.property} must be a valid international phone number (E.164, e.g. +251911223344)`; } } /** Class-validator decorator wrapping the country-aware phone constraint. */ export function IsValidPhone(validationOptions?: ValidationOptions) { return function (object: object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName, options: validationOptions, constraints: [], validator: IsValidPhoneConstraint, }); }; } /** * Normalize a phone string to canonical E.164. Returns the canonical form when * parseable, otherwise the trimmed original (tolerant — never throws), or the * value unchanged when empty/nullish. */ export function normalizeE164( value: string | null | undefined, ): string | null | undefined { if (value === undefined || value === null || value === '') return value; const parsed = parsePhoneNumberFromString(value); return parsed?.isValid() ? parsed.number : value.trim(); }