diff --git a/lib/features/consultant/become_consultant/presentation/pages/passport_data_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/passport_data_screen.dart index a14a9399..4cde5a95 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/passport_data_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/passport_data_screen.dart @@ -69,6 +69,9 @@ class _PassportDataScreenState extends State { filter: {'#': RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy, ); + final _alfaPersonNameFormatter = FilteringTextInputFormatter.allow( + RegExp(r'[a-zA-Zа-яА-ЯёЁ ,.-]'), + ); @override void initState() { @@ -301,6 +304,8 @@ class _PassportDataScreenState extends State { child: CustomTextFormField( hintText: 'Фамилия', controller: lastNameController, + maxLength: 50, + inputFormatters: [_alfaPersonNameFormatter], hasError: errors.containsKey('last_name'), onChanged: (_) => _syncToCubit(), ), @@ -311,6 +316,8 @@ class _PassportDataScreenState extends State { child: CustomTextFormField( hintText: 'Имя', controller: firstNameController, + maxLength: 50, + inputFormatters: [_alfaPersonNameFormatter], hasError: errors.containsKey('first_name'), onChanged: (_) => _syncToCubit(), ), @@ -321,6 +328,8 @@ class _PassportDataScreenState extends State { child: CustomTextFormField( hintText: 'Отчество', controller: middleNameController, + maxLength: 50, + inputFormatters: [_alfaPersonNameFormatter], hasError: errors.containsKey('middle_name'), onChanged: (_) => _syncToCubit(), ), diff --git a/lib/features/consultant/become_consultant/utils/laravel_validation_errors.dart b/lib/features/consultant/become_consultant/utils/laravel_validation_errors.dart index d4b0d947..97be0dc1 100644 --- a/lib/features/consultant/become_consultant/utils/laravel_validation_errors.dart +++ b/lib/features/consultant/become_consultant/utils/laravel_validation_errors.dart @@ -18,7 +18,8 @@ class LaravelValidationErrors { } const _genericValidationSummary = 'Проверьте введённые данные'; -const _genericNetworkSummary = 'Не удалось отправить данные. Попробуйте ещё раз'; +const _genericNetworkSummary = + 'Не удалось отправить данные. Попробуйте ещё раз'; /// Parses Laravel `errors` map and maps them to user-facing Russian texts. /// Raw backend messages are never shown in the UI. @@ -85,8 +86,7 @@ String mapConsultantValidationMessage({ raw.contains('required') || raw.contains('обязательн') || raw.contains('required_if'); - final isAccepted = - raw.contains('accepted') || field.contains('agreement'); + final isAccepted = raw.contains('accepted') || field.contains('agreement'); final isUrl = raw.contains('url') || field.contains('link'); final isFormat = raw.contains('format') || @@ -154,6 +154,9 @@ String mapConsultantValidationMessage({ 'passport_number' => 'Некорректный номер паспорта', 'passport_issue_date' => 'Некорректная дата выдачи', 'passport_issuer_code' => 'Некорректный код подразделения', + 'last_name' => 'Некорректная фамилия', + 'first_name' => 'Некорректное имя', + 'middle_name' => 'Некорректное отчество', 'birth_date' => 'Некорректная дата рождения', 'citizenship' => 'Некорректный код гражданства', 'sbp_phone' => 'Некорректный телефон СБП', diff --git a/lib/features/consultant/become_consultant/utils/passport_data_validator.dart b/lib/features/consultant/become_consultant/utils/passport_data_validator.dart index 73c0f1a6..6b71299f 100644 --- a/lib/features/consultant/become_consultant/utils/passport_data_validator.dart +++ b/lib/features/consultant/become_consultant/utils/passport_data_validator.dart @@ -8,6 +8,9 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consulta abstract final class PassportDataValidator { static const futureDateMessage = 'Дата не может быть в будущем'; + /// Alfa Bank `personalInformation.{last,first,middle}Name` pattern. + static final alfaPersonNamePattern = RegExp(r'^[a-zA-Zа-яА-ЯёЁ ,.-]{1,50}$'); + static Map validate( ConsultantProfileModel consultant, { DateTime? now, @@ -15,17 +18,28 @@ abstract final class PassportDataValidator { final today = now ?? DateTime.now(); final errors = {}; - _requireText( + _requirePattern( errors, key: 'last_name', value: consultant.lastName, + pattern: alfaPersonNamePattern, emptyMessage: 'Укажите фамилию', + invalidMessage: 'Некорректная фамилия', ); - _requireText( + _requirePattern( errors, key: 'first_name', value: consultant.firstName, + pattern: alfaPersonNamePattern, emptyMessage: 'Укажите имя', + invalidMessage: 'Некорректное имя', + ); + _optionalPattern( + errors, + key: 'middle_name', + value: consultant.middleName, + pattern: alfaPersonNamePattern, + invalidMessage: 'Некорректное отчество', ); _requirePattern( errors, @@ -148,6 +162,22 @@ abstract final class PassportDataValidator { } } + static void _optionalPattern( + Map errors, { + required String key, + required String? value, + required RegExp pattern, + required String invalidMessage, + }) { + final raw = value?.trim() ?? ''; + if (raw.isEmpty) { + return; + } + if (!pattern.hasMatch(raw)) { + errors[key] = invalidMessage; + } + } + static void _requireDate( Map errors, { required String key, diff --git a/test/features/consultant/become_consultant/utils/laravel_validation_errors_test.dart b/test/features/consultant/become_consultant/utils/laravel_validation_errors_test.dart index 4470e4e3..88a0d328 100644 --- a/test/features/consultant/become_consultant/utils/laravel_validation_errors_test.dart +++ b/test/features/consultant/become_consultant/utils/laravel_validation_errors_test.dart @@ -49,7 +49,26 @@ void main() { ); expect(parsed.fieldErrors['last_name'], 'Укажите фамилию'); - expect(parsed.fieldErrors['agreement_accepted'], 'Примите условия оферты'); + expect( + parsed.fieldErrors['agreement_accepted'], + 'Примите условия оферты', + ); + }); + + test('maps name format errors', () { + final parsed = parseLaravelValidationErrors( + dio422({ + 'errors': { + 'last_name': ['The last name format is invalid.'], + 'first_name': ['The first name format is invalid.'], + 'middle_name': ['The middle name format is invalid.'], + }, + }), + ); + + expect(parsed.fieldErrors['last_name'], 'Некорректная фамилия'); + expect(parsed.fieldErrors['first_name'], 'Некорректное имя'); + expect(parsed.fieldErrors['middle_name'], 'Некорректное отчество'); }); test('falls back to generic message for non-422 DioException', () { diff --git a/test/features/consultant/become_consultant/utils/passport_data_validator_test.dart b/test/features/consultant/become_consultant/utils/passport_data_validator_test.dart index 3e32f758..fd568e36 100644 --- a/test/features/consultant/become_consultant/utils/passport_data_validator_test.dart +++ b/test/features/consultant/become_consultant/utils/passport_data_validator_test.dart @@ -25,6 +25,34 @@ void main() { expect(PassportDataValidator.validate(valid), isEmpty); }); + test('rejects names that do not match Alfa Bank pattern', () { + final errors = PassportDataValidator.validate( + valid.copyWith( + lastName: 'Regress14LastName', + firstName: 'Regress14Name', + middleName: 'Regress14middleName', + ), + ); + + expect(errors['last_name'], 'Некорректная фамилия'); + expect(errors['first_name'], 'Некорректное имя'); + expect(errors['middle_name'], 'Некорректное отчество'); + }); + + test('allows hyphenated names and empty middle name', () { + final errors = PassportDataValidator.validate( + valid.copyWith( + lastName: 'Салтыков-Щедрин', + firstName: 'Mary Jane', + middleName: '', + ), + ); + + expect(errors.containsKey('last_name'), isFalse); + expect(errors.containsKey('first_name'), isFalse); + expect(errors.containsKey('middle_name'), isFalse); + }); + test('requires empty text fields', () { final errors = PassportDataValidator.validate( valid.copyWith(lastName: '', firstName: null),