Запись к врачу прямо из личного кабинета
Запись к врачу по корпоративному ДМС раньше означала переписку с Telegram-ботом. Я перенёс этот процесс в личный кабинет — в форму из пяти шагов, которая за считанные секунды передаёт заявку операторам. Самое интересное здесь было решить, как мало информации браузеру вообще разрешено отправлять.
Если у вас есть корпоративная медицинская страховка и вам нужно попасть к врачу, раньше процесс выглядел так: открыть Telegram, найти нужного бота и отвечать на его вопросы по одному сообщению за раз. Бот передавал ответы в CRM компании, а CRM отправляла заявку в группу операторов. После этого кто-то связывался с вами.
Это работало. Но это была единственная часть вашей страховки, которая существовала за пределами личного кабинета — и при этом она спрашивала то, что компания уже знала о вас.
Задача была перенести в кабинет именно сбор этой информации. Не всю цепочку целиком: только ту часть, где пользователь отвечает на вопросы.
Знать, что не нужно строить
Первым полезным решением было ограничить объём работы.
Вся цепочка после CRM уже работала: она присваивает каждой заявке номер, отправляет её в группу операторов и отслеживает, кто её взял в работу. Переписывать всё это означало бы создать две системы, которые делают одно и то же и через неделю начинают расходиться в поведении.
Поэтому уведомления остаются не у нас. Мы не храним токен бота, не знаем ID группы и вообще не вызываем Telegram. Мы передаём заявку в CRM точно так же, как это раньше делал бот, а дальше вся существующая цепочка продолжает работать без изменений. Сообщение, которое появляется у операторов в группе, остаётся таким же, как и раньше.
Вместо бота появились один endpoint и одна форма.
Шесть полей — и ни одного лишнего
Форма спрашивает регион, клинику, специалиста, жалобу, дату и время. Пять ответов пользователя, которые после разделения даты и времени превращаются в шесть полей на уровне запроса.
Всё остальное — ФИО, номер телефона, PINFL, дата рождения, возраст и номер полиса — сервер получает самостоятельно из профиля пользователя, который уже прошёл проверку через национальную систему идентификации. Браузер их не отправляет, а даже если бы отправил, сервер их проигнорировал бы: в модели запроса таких полей нет, поэтому они отбрасываются ещё до выполнения основной логики.
Это не просто аккуратность в коде. Эти поля определяют, для кого создаётся запись на приём. Если браузер сможет сам указать их значения, достаточно действующей сессии, чтобы попытаться записать другого человека к врачу от имени чужого полиса.
Единственное исключение — номер полиса, и оно сделано намеренно: страница может его отправить, но сервер сравнивает его с картой, которую сам нашёл для текущего пользователя. Совпадает — продолжаем. Нет — запрос отклоняется. Это не позволяет перенаправить запись на чужую карту и защищает от устаревшей вкладки, в которой карта пользователя уже изменилась после загрузки страницы.
Две строки вместо одного timestamp
В форме пользователь выбирает одну дату и время, потому что для него это один момент. Но на сервер они отправляются двумя отдельными строками: датой и временем.
На первый взгляд это лишняя работа, но она предотвращает вполне конкретный и неприятный класс ошибок. Объединённый timestamp может где-то в цепочке быть распарсен и сериализован заново — и тогда условные 15:00 внезапно превращаются в 10:00 UTC, а оператор позвонит не в тот час, когда пациент ожидал приём. У двух обычных строк нет часового пояса, который можно случайно потерять.
Внутри приложения значение остаётся одним моментом времени, а разделение происходит только на самом последнем этапе — одно решение для пользователя, два поля для системы.
Подсказки, которые знают, какого врача вы выбрали
В компании нет единого справочника клиник, поэтому клиника, специалист и жалоба вводятся как свободный текст. Но свободное поле без каких-либо подсказок — не лучший интерфейс, поэтому каждое поле предлагает варианты, которые можно выбрать одним нажатием.
Список жалоб зависит от выбранного специалиста. Выбираете стоматолога — получаете зубную боль, травму, профилактический осмотр. Выбираете кардиолога — зубная боль исчезает. Связь построена на стабильных идентификаторах, а не на видимых названиях, поэтому перевод текста на другой язык не ломает соответствие, а вариант «Другое» доступен всегда — даже если специалист был введён вручную, где фильтрация специально не сужает список.
Мастер также переживает перезагрузку страницы. Ответы сохраняются для текущей вкладки и восстанавливаются при возвращении. При этом они привязаны к аккаунту, которому принадлежат, поэтому данные предыдущего пользователя не будут случайно восстановлены для другого человека.
Сохраняем копию того, чем мы не владеем
CRM является владельцем самой записи на приём: она присваивает номер, отслеживает статус и решает, кто будет заниматься заявкой. Но личному кабинету нужно показать пользователю, что он записал, а у CRM нет endpoint, через который можно получить эту информацию обратно.
Поэтому после успешной отправки — и только после неё — мы сохраняем копию у себя. В ней есть номер, который вернула CRM и который впоследствии назовёт оператор, и запись привязана к медицинской карте, а не к аккаунту пользователя. Одна карта может покрывать членов семьи, поэтому запись, созданная для родственника, всё равно должна находиться внутри этой карты.
Если сохранение нашей копии не удалось, сама запись всё равно считается успешной. Заявка уже существует в CRM, и было бы неправильным сообщать пользователю об ошибке только потому, что наша собственная система учёта дала сбой. Ошибка просто записывается в лог.
Ошибка, которая ничего не объясняла
Затем появилась ситуация: CRM отклонила заявку, а пользователь увидел сообщение «Произошла непредвиденная ошибка».
Технически это было правдой и совершенно бесполезно для человека. Хуже того, лог тоже не помогал: он сохранял ответ CRM, но не то, что мы отправили, поэтому, чтобы воспроизвести проблему, приходилось угадывать, какие данные были в форме.
На самом деле CRM уже возвращала понятную причину. Мы просто её выбрасывали. Теперь, если CRM отклоняет запрос, пользователь видит её собственное объяснение — а в лог записываются обе стороны, наш запрос и ответ CRM, при этом PINFL и номер телефона маскируются, а имя вообще не сохраняется. Для отладки значения поля не нужно знать личность человека.
Но тут обнаружился ещё один уровень той же проблемы. Иногда CRM отвечает: «У этого человека уже есть открытая заявка» — это нормальный результат, а не ошибка, и страница должна спокойно показать существующий номер заявки. Код именно это и делал, но пользователь всё равно видел серверную ошибку.
Причина оказалась в общем helper'е обработки ответов: он проверяет HTTP-статусы по списку разрешённых значений, а статус 409 Conflict в этот список просто не попал. Всё неизвестное автоматически превращалось в Internal Server Error. И это касалось не только записи к врачу — та же проблема уже существовала ещё в пяти сценариях: при регистрации с уже существующим номером, при несовпадении данных идентификации, и все они показывались пользователю так, будто сервер сломался. Одна отсутствующая запись в одном общем списке.
Что считается успешной заявкой
Последнее правило здесь самое важное: заявка считается успешной только тогда, когда CRM сама подтверждает её и возвращает номер.
Не потому, что запрос быстро завершился. Не потому, что ответ выглядит правильным. Если CRM не подтвердила создание заявки, пользователь должен увидеть ошибку — иначе можно показать экран успешной записи, которой на самом деле не существует. Автоматически повторять такой запрос мы тоже не стали: повтор после таймаута — отличный способ однажды получить две записи, когда два врача будут ожидать одного пациента.
Почему я доволен результатом
Теперь человек записывается к врачу прямо из того аккаунта, где уже находится его страховка, используя пять вещей, которые действительно должен указать сам пользователь. А операторы получают то же сообщение в той же группе, которую они привыкли отслеживать.
Главный показатель этой работы — насколько мало пришлось менять в существующей системе: одна форма, один endpoint и одна строка для нашей собственной записи. Всё, что уже работало в компании, мы оставили на месте.