← назад в блог
daily26 августа 2026 г./7 мин чтения

Всё, что было не так с одной маленькой формой

Форма «Моя семья» в личном кабинете состояла всего из нескольких полей, но проблема нашлась на каждом уровне: можно было добавить самого себя как родственника, форма требовала ID, который никто не помнит, невозможные даты приводили к ошибке, сообщения игнорировали выбранный язык, выпадающий список не закрывался, а Backspace не удалял символ. Это история полного ремонта одной обманчиво простой формы — от backend-правил и интеграции с сервисами до состояния и frontend-полировки.

В личном кабинете есть раздел «Семья»: добавляешь родственников один раз, и их данные готовы везде, где они понадобятся для оформления полиса. Форма запрашивала три вещи — национальный идентификационный номер родственника, серию и номер паспорта — и отправляла их в государственный сервис данных, который отвечает только тогда, когда все три значения совпадают с одним реальным человеком. Имя, дата рождения, водительское удостоверение приходят от государства уже проверенными — их нельзя ввести вручную.

Хорошая архитектура. Но перед ней было две проблемы.

Первая была зарегистрирована как баг: можно было добавить самого себя как собственного родственника. Выбираешь «отец», вводишь свои документы — и вот ты уже сам себе родственник. Ничего из последующего процесса к такому сценарию готово не было.

Вторую никто не считал багом, потому что она так не выглядела: идентификационный номер состоит из четырнадцати цифр, а люди не знают ID своих родственников наизусть. Номер паспорта супруга они могут помнить или найти на старой фотографии. Даты рождения знают отлично. А поле ID становилось местом, где форма либо заставляла искать физическую карточку, либо теряла пользователя.

Нельзя быть самому себе родственником

Исправление self-add получилось именно таким, каким и должно быть: собственный подтверждённый ID аккаунта уже хранится в профиле — тот же самый field использует сценарий добавления автомобиля, чтобы определить, принадлежит ли машина пользователю. Сравниваем его с переданным значением и при совпадении сразу отказываем с понятным сообщением. Три строки, расположенные до обращения к государственному сервису, потому что такой запрос тарифицируется и нет смысла платить за lookup, результат которого мы заранее знаем и всё равно не примем.

Выпадающий список также предлагал «я сам» как тип родства — справочник на backend общий с другим сценарием, где страховать самого себя совершенно нормально. Мы просто убрали этот вариант из списка доступных опций, но только на уровне отображения вариантов: существующие записи, в которых такой тип родства легитимно сохранён, по-прежнему отображаются корректно.

Меняем ID на дату рождения

Тогда возник более важный вопрос: зачем вообще спрашивать ID?

У государственного сервиса есть два способа поиска. Первый, который мы использовали, требует ID и паспорт. Второй принимает дату рождения и паспорт — и в ответе сам возвращает ID. Ещё два сценария в кодовой базе уже использовали второй вариант. Перевод формы «Семья» на него означает, что человек вводит дату рождения, которую он знает, вместо номера, который не помнит, а система всё равно получает подтверждённый ID — только уже от государства, а не от человека.

Становится ли такая проверка слабее? Едва ли, но здесь важно честно обозначить границу. Национальный ID буквально содержит дату рождения — шесть его цифр кодируют день рождения. То есть знание ID фактически означает знание даты рождения. Оба способа требуют, чтобы все введённые данные совпали с одним реальным человеком в государственных записях. А тот, у кого есть номер вашего паспорта, в любом случае получил его из документа, где указана ваша дата рождения. Та же модель угроз, но на одно невозможное поле меньше.

Одно следствие потребовало отдельного внимания: проверка на добавление самого себя сравнивает ID, а клиент теперь ID не отправляет. Поэтому проверка переместилась сразу после ответа государства — возвращённый ID сравнивается с ID из профиля. Ввести собственный паспорт и дату рождения всё ещё нельзя — просто теперь проверка находится на один шаг позже. Проверка дубликатов теперь выполняется дважды: бесплатная проверка по паспорту до платного запроса и проверка по ID после него. Вторая дополнительно ловит попытку повторно добавить того же человека уже с новым паспортом.

Даты, которых не существует

Поле даты рождения открыло проблему, которой раньше не было с ID: невозможные значения. Нормализатор даты был обычным pattern matcher'ом — он спокойно превращал 31 февраля в строку, которую затем отвергала база данных. Пользователь вместо сообщения о валидации получал обычную серверную ошибку.

Теперь сервер проверяет, существует ли такая дата в календаре, не находится ли она в будущем и не раньше ли 1900 года. И всё это происходит до платного lookup, поэтому мусорный запрос ничего не стоит. Календарь в форме теперь использует те же ограничения, поэтому интерфейс физически не позволяет сформировать запрос, который сервер всё равно отклонит. Обе стороны согласованы, потому что применяют одно и то же правило — каждая на своём языке.

Ошибка, которая говорила только по-русски

Первый ручной тест на узбекском выявил интересную проблему: неудачный lookup возвращал сообщение об ошибке на русском. Это был не наш текст — собственное сообщение государственного сервиса без изменений проходило через все слои и попадало прямо на экран, полностью игнорируя язык, который выбрал пользователь.

В калькуляторах этой проблемы никогда не было, и причина оказалась немного неудобной: они вообще отбрасывают backend-сообщения об ошибках и для любой проблемы показывают одно общее переведённое «не найдено». С языком всё честно, но причина ошибки становится слишком размытой. Форма кабинета делает наоборот — показывает конкретное серверное сообщение для каждой ошибки. А значит, сервер должен говорить на языке пользователя. Поэтому теперь каждое сообщение на этом пути проходит через локализованный каталог, ключом которого служит language header запроса: обязательные поля, дубликаты, попытка добавить самого себя, объект не найден. Raw-текст провайдера перехватывается и заменяется нашим сообщением. Есть один компромисс: если государственный сервис недоступен, пользователь тоже увидит сообщение вроде «проверьте дату и паспорт». Оно будет немного неточным, но повторить запрос в любом случае будет правильным действием.

Select, который не хотел закрываться

Frontend-полировка обнаружила собственные баги. Dropdown с типом родства подтверждал выбор галочкой — и продолжал оставаться открытым. Причину пришлось действительно поискать: select находился внутри HTML-элемента label, а label передаёт клик по любой области внутри себя своей первой кнопке. Пользователь нажимает на option, меню закрывается, label передаёт второй клик trigger'у, меню снова открывается — всё это происходит в одном frame и выглядит ровно как «он вообще не закрылся». Один event modifier прекращает это перенаправление. Исправление сделано непосредственно в компоненте, поэтому все будущие места его использования будут защищены независимо от того, внутри чего он окажется.

На телефонах был ещё один вариант той же проблемы: dropdown открывался вниз около нижней границы экрана и выходил за неё, из-за чего последние варианты оказывались под элементами браузера. Теперь при открытии компонент измеряет свободное пространство вокруг себя, разворачивается вверх, если сверху больше места, и ограничивает высоту тем пространством, которое реально доступно. Список всегда прокручивается внутри себя и больше никогда не уходит за пределы экрана.

Если удаляешь цифру, должна удалиться цифра

В поле даты была самая странная ошибка: Backspace не работал на некоторых позициях. Input mask переформатировал значение после каждого нажатия клавиши и автоматически добавлял разделитель после дня и месяца. Поэтому когда Backspace удалял этот разделитель, mask тут же вставляла его обратно. Нажатие клавиши отменялось кодом, который должен был помогать пользователю. Общее решение для этой проблемы уже существовало в другой части кодовой базы — client-only directive. Но при попытке использовать его здесь он ломал нашу server-rendered страницу. Замена оказалась проще: небольшой handler определяет, является ли текущее нажатие удалением — браузер сообщает это в event, — и просто не позволяет снова добавить то, что пользователь только что удалил. Заодно исправили ещё одну тихую несовместимость: mask вставляла слэши, хотя формат поля ожидал точки. Поэтому полностью введённые даты до этого вообще не всегда корректно парсились.

Почему я доволен результатом

Теперь форма спрашивает только то, что человек действительно знает: дату рождения, паспорт и степень родства. Всё, что идентифицирует человека, приходит от государства уже проверенным. Всё, что можно отклонить, отклоняется до того, как это начинает стоить денег. Каждая ошибка говорит на языке, который выбрал пользователь. А два бага, которые я запомню лучше всего, — label, перенаправляющий фантомный клик, и mask, отменяющая Backspace, — оба оказались случаями, когда «полезный» код помогал слишком усердно. Наверное, это самый человеческий вид бага из всех.

Всё, что было не так с одной маленькой формой · Axror