Регистрация пользователя и интеграции METRO Cards

Назначение: дать менеджеру единую карту пользовательских сценариев, границ систем, интеграций и поведения при ошибках — без необходимости читать код.

Статус документа: as is по текущей реализации в репозитории на 26.08.2026. Неподтверждённое внешнее поведение и решения, которые нельзя вывести из кода, вынесены в раздел «Открытые решения».

В области документа: доступ к public/private лендингу, OTP, существующий и новый клиент, Post-Auth интеграции, публикация лендинга и регистрация программы в Gena.

Вне области: детальный Keycloak SSO, создание пакетов промокодов и инфраструктура деплоя. Они описаны в общей архитектурной документации.


1. Главное за минуту

  1. Браузер не обращается напрямую к AuthApp, Gena или CRM. Все запросы идут через Nuxt Frontend и Cards Backend.
  2. Сохранение черновика ничего не отправляет в Gena. Запрос выполняется перед первой публикацией лендинга и повторяется только после подтверждённой ошибки.
  3. После SMS-подтверждения AuthApp определяет ветку: существующий клиент или регистрация нового.
  4. Если AuthApp вернул непустой cardholders[], Cards без отдельного экрана выбора привязывает cardholders[0] — независимо от количества карт.
  5. Канонический источник активной карты — get-card-profile. Пользовательский success невозможен без профиля с barcode.
  6. После получения карты Cards пытается привязать её к акции и передать рекламные разрешения. Сбой этих шагов после авторизации (Post-Auth) сейчас фиксируется в логах, но не показывает ошибку пользователю (он видит успешный экран).
  7. Экран результата настраивается в CMS: это может быть текстовое сообщение или карточка со штрихкодом.
  8. Отправка Email/SMS с картой не выполняется сервисом Cards. Сообщения отправляет внутренний почтовый контур METRO / CRM, а Cards не получает подтверждение доставки.
  9. Текущую проверку доступа к закрытому лендингу нельзя считать полноценной защитой: сервер доверяет клиентской отметке authorized без цифровой подписи.

Ключевое различие: «пользователь получил успешный экран» не означает автоматически, что карта точно привязана к акции, разрешения обновлены и Email/SMS доставлены.

Экспресс-шпаргалка по трём сценариям

Параметр / Вопрос 1. Существующий клиент (existing) 2. Новый клиент (new) 3. Публикация лендинга (CMS)
Кто инициирует Посетитель лендинга Посетитель лендинга Контент-менеджер в Strapi
Шаг 1 (Вход) Телефон + SMS-код Телефон + SMS-код Настройка дат, оффера и slug
Проверка клиента В AuthApp через verify_otp (клиент найден) В AuthApp через verify_otp (клиент не найден)
Шаг 2 (Анкета) Не показывается (сразу экран успеха) Показывается: Имя, Фамилия, ДР, Email, чекбокс
Куда шлются данные В AuthApp (bind + get-card-profile) В AuthApp (POST /register + get-card-profile) В Gena-service (POST /loyalty_add_programm/)
Обработка Email Email с формы игнорируется (защита от подмены) Email обязателен и сохраняется в профиль METRO Поле «Шаблон письма» удалено из CMS
Выпуск карты Карта уже есть; берётся активный barcode Выпускается новая карта в CRM через AuthApp
Привязка к акции POST /add-card-to-loyalty с кодом лендинга POST /add-card-to-loyalty с кодом лендинга Программа лояльности регистрируется в Gena
Рекламные согласия POST /update-permission (если consentAds = true) POST /update-permission (если consentAds = true)
Прямой вызов Gena НЕТ (все запросы идут строго в AuthApp) НЕТ (все запросы идут строго в AuthApp) ДА (один раз бэкендом CMS при Publish)
Отправка Email-письма Шлет CRM METRO на email из базы CRM Шлет CRM METRO на email из анкеты Cards письма не отправляет
Финал на экране «Вы уже зарегистрированы» + штрихкод «Карта выпущена!» + штрихкод Статус «Опубликовано» в админке

2. Карта систем и границы ответственности

[Diagram]

Кто чем владеет

Контур Ответственность
Nuxt Frontend Показывает состояние лендинга и шаги формы, валидирует поля, получает captcha token, вызывает только Cards API.
Cards Backend / Strapi Проверяет промокоды и капчу, хранит промежуточную backend session, оркестрирует AuthApp, публикует контент, вызывает Gena.
AuthApp Отправляет OTP, идентифицирует клиента, привязывает/выпускает карту, отдаёт активный профиль, принимает loyalty и permissions.
Gena-service Регистрирует саму промо-программу при CMS-публикации. В пользовательском flow напрямую не участвует.
CRM / loyalty / каналы сообщений Находятся за внешним контуром METRO. Их внутренний маршрут и доставка сообщений не наблюдаются Cards.

3. Доступ к лендингу до регистрации

[Diagram]

Важные правила доступа:

Известная уязвимость (Security Gap): в браузере сохраняется простая отметка authorized, и сервер открывает контент по заголовку от браузера без проверки цифровой подписи. Этого достаточно для скрытия контента в интерфейсе от обычного пользователя, но технически продвинутый пользователь может получить доступ к странице без ввода промокода. До исправления это не обеспечивает 100% защиту закрытого лендинга.


4. Пользовательский flow регистрации

[Diagram]

Существующий и новый клиент

Вопрос Существующий клиент Новый клиент
Как определяется AuthApp вернул готовую auth-session или непустой cardholders[] AuthApp вернул пустой cardholders[] или сигнал регистрации
Нужен ли bind Да при любом непустом cardholders[]; берётся cardholders[0]. При готовой auth-session — нет Нет
Показывается ли анкета Нет Да
Что обязательно для регистрации Имя, фамилия, дата рождения и Email на уровне Cards Backend
Откуда берётся карта Активный профиль AuthApp Выпускается через AuthApp, затем читается активный профиль
Канонический номер карты barcode из get-card-profile barcode из get-card-profile
Post-Auth шаги Те же, что у нового клиента Те же, что у существующего клиента
Экран результата Настраиваемое сообщение или barcode-card Настраиваемое сообщение или barcode-card

Отличия UI-вариантов

Элемент Rich-форма Compact-форма
Первый шаг Телефон и Email Телефон
Email существующего клиента Пользователь вводит Email до OTP, но Cards не обновляет им профиль AuthApp/CRM Пользователь Email не вводит
Email нового клиента Берётся с первого шага Добавляется как обязательное поле анкеты
Рекламное согласие На contact-шаге; если показано, проверяется до завершения flow Для metropoliya24 находится на profile-шаге и обязательно; в остальных compact-сценариях contact-checkbox сейчас показывается, но его обязательность не проверяется
Success По умолчанию текст Текст или barcode_card; для metropoliya24 barcode-card включается принудительно

Backend вызывает update-permission только при consentAds=true. Если чекбокс не настроен или остался неотмеченным, вызов пропускается: существующие разрешения клиента не снимаются. Необязательный contact-checkbox в части compact-сценариев — текущее ограничение реализации, а не универсальное бизнес-правило.

На OTP-шаге пользователь может изменить телефон или запросить код повторно после таймера. Повторная отправка снова проходит captcha validation и общий rate limit.


5. Успешная техническая последовательность B2C

Эта схема показывает интеграционные вызовы успешного сценария. Ошибки и их последствия собраны отдельно в разделе 9.

[Diagram]

AuthApp cookies не передаются во Frontend. Они используются внутри backend session для последовательных вызовов и очищаются после финального результата. При этом телефон, Email, consent и metadata могут оставаться в session до следующего сброса или истечения самой сессии.


6. Публикация лендинга и Gena

Пользовательская регистрация и регистрация промо-программы — два разных flow. В Gena напрямую ходит только Cards Backend во время CMS-публикации.

[Diagram]

Что означают статусы Gena

Статус Что произошло Что делать менеджеру
not_registered Черновик ещё не отправлялся Завершить настройку и опубликовать
registering Запрос выполняется или состояние зависло Не повторять Publish; дождаться результата, при зависании обратиться к разработчику
registered Gena подтвердила создание программы Можно публиковать повторно; Gena повторно не вызывается
failed Получена подтверждённая ошибка Исправить указанную причину и повторить Publish
unknown Неясно, создала ли Gena программу Не повторять Publish; нужна ручная сверка с Gena
legacy_manual Старая программа заведена вручную Gena не вызывается, изменения синхронизируются вручную

Правила, которые важно знать:


7. Идентификаторы и данные

Данные Источник Куда передаются Что хранит Cards
slug CMS URL лендинга; Gena loy_cust_name PostgreSQL; после первой публикации фиксируется в publishedSlug
offer.code Справочник CMS Gena promo_events_desc PostgreSQL
Даты акции CMS Gena loyalty_date_from / loyalty_date_to PostgreSQL и snapshot последней попытки Gena
integrationSourceCode Настройка registration-блока; при отсутствии заполняется slug AuthApp loyalty_id В контенте лендинга и временно в backend session
promoCode Пользователь на private-лендинге AuthApp external_loyalty_id Состояние промокода в БД и значение в backend session
Телефон Пользователь AuthApp send_otp / verify_otp; при register связь сохраняется через AuthApp session, телефон не входит в body регистрации Не сохраняется в клиентскую таблицу Cards; временно находится в session и может попадать в operational logs
Email Rich contact-шаг или compact profile-шаг Для нового клиента — AuthApp register; для существующего профиль не обновляется Временно в backend session; клиентский профиль в БД Cards не создаётся
Имя и дата рождения Анкета нового клиента AuthApp register Не сохраняются как профиль клиента в PostgreSQL Cards
consentAds UI При true — AuthApp update-permission Временно в backend session
barcode AuthApp get-card-profile Frontend success response Не сохраняется как клиентский профиль в PostgreSQL Cards

slug и integrationSourceCode формально являются разными полями: первый идентифицирует URL и программу в Gena, второй становится loyalty_id в AuthApp. Однако текущий код определяет private-режим поиском slug === loyalty_id, а Frontend использует integrationSourceCode и как feature flag сценария. Поэтому отдельное значение технически допустимо в CMS, но небезопасно без проверки: может потеряться external_loyalty_id или измениться UI-поведение.


8. Каталог интеграционных вызовов

Инициатор → получатель Endpoint Когда вызывается Результат Поведение при ошибке
Frontend → Cards Backend POST /api/auth/send-otp Старт регистрации Запускает captcha validation и OTP Ошибка видна пользователю; OTP не отправляется
Cards Backend → Yandex GET /validate Перед каждым send_otp Подтверждает captcha token Fail-closed: 400 или 503
Cards Backend → AuthApp POST /auth/api/v1/public/send_otp После валидной капчи Отправка OTP Пользователь получает service error
Frontend → Cards Backend POST /api/auth/verify-otp Ввод SMS-кода Определение ветки клиента Сейчас любая штатно возвращённая ошибка AuthApp маппится как INVALID_OTP
Cards Backend → AuthApp POST /auth/api/v1/public/verify_otp Проверка OTP Auth-session, cardholders[] или регистрация Основной flow останавливается
Cards Backend → AuthApp POST /auth/api/v1/bind При любом непустом cardholders[] Привязка cardholders[0] Основной flow останавливается
Frontend → Cards Backend POST /api/auth/register Только новый клиент Валидация и регистрация профиля Ошибка видна пользователю
Cards Backend → AuthApp POST /auth/api/v1/register После анкеты Выпуск карты Основной flow останавливается
Cards Backend → AuthApp POST /auth/api/v1/auth/endpoints/get-card-profile После existing/register Канонический barcode Без профиля/barcode success не возвращается
Cards Backend → AuthApp POST /auth/api/v1/auth/endpoints/add-card-to-loyalty После получения карты, если есть integrationSourceCode Привязка карты к акции Warning в logs; success пользователя сохраняется
Cards Backend → AuthApp POST /auth/api/v1/auth/endpoints/update-permission Только при consentAds=true Email=true, SMS=true, Push=false Warning в logs; success пользователя сохраняется
Cards Backend → Gena POST /loyalty_add_programm/ До первой успешной CMS-публикации Создание промо-программы Публикация отменяется; failed или unknown

Прямых вызовов Gena из B2C-flow и прямых вызовов AuthApp из браузера нет.

8.1. Форматы ответов AuthApp и Gena (JSON-примеры)

1. Отправка SMS-кода (POST /auth/api/v1/public/send_otp)

{
  "result": "success",
  "methods_of_auth": {
    "retry_timeout_seconds": 60,
    "list": ["sms"]
  }
}

2. Проверка кода (POST /auth/api/v1/public/verify_otp)

{
  "cardholders": [
    {
      "cardholderId": 10023456,
      "id": 10023456,
      "authPersonFirstName": "Иван",
      "authPersonLastName": "Иванов"
    }
  ]
}
{}

(HTTP 200 с пустым JSON-объектом и выставленными сессионными cookies tmp/auth/cardholder)

{
  "result": "register"
}
{
  "result": "error",
  "message": "Неверный код"
}

3. Получение профиля и активной карты (POST /auth/api/v1/auth/endpoints/get-card-profile)

{
  "barcode": "2200001234567",
  "cardholder": {
    "cardholderId": 10023456,
    "authPersonFirstName": "Иван",
    "authPersonLastName": "Иванов",
    "birthDate": "1990-01-15",
    "mobilePhoneNo": "79132011194",
    "emailAddress": "ivan@example.com",
    "notifyBySMS": true,
    "notifyByEmail": true
  }
}

4. Привязка карты к акции (POST /auth/api/v1/auth/endpoints/add-card-to-loyalty)

{
  "loyalty_id": "bonus3000",
  "external_loyalty_id": "PROMO-2026"
}
{
  "result": "success"
}

5. Передача согласий на рекламу (POST /auth/api/v1/auth/endpoints/update-permission)

{
  "notifyByEmail": true,
  "notifyBySMS": true,
  "notifyByPush": false
}
{
  "result": "success"
}

6. Регистрация акции в Gena при публикации (POST /loyalty_add_programm/)

{
  "loy_cust_name": "metropoliya24",
  "promo_events_desc": "bonus3000_offer",
  "loyalty_date_from": "2026-05-01",
  "loyalty_date_to": "2026-12-31"
}
{
  "data": {
    "result": true
  },
  "message": "Programm added successfully"
}

9. Ошибки и операционное поведение

Ситуация Что видит пользователь / менеджер Итог Повтор
Нет captcha token или token невалиден Ошибка формы OTP не отправлен Можно повторить
SmartCaptcha: timeout 5 секунд, сетевая ошибка, non-2xx или отсутствует server key Сообщение о временной недоступности Fail-closed, OTP не отправлен После восстановления сервиса/конфигурации
Более 3 запросов SMS в минуту с одного IP 429 и текст лимита OTP не отправлен После окна лимита
Неверный OTP или более 5 проверок в минуту Ошибка на OTP-шаге Авторизация не завершена Новый код/после окна лимита
AuthApp вернул ошибку на verify_otp Сейчас обычно показывается «неверный код», даже если причина сервисная Flow не завершён После уточнения причины
Ошибка bind, register или get-card-profile Service error Success не показывается Пользователь повторяет flow
В профиле нет barcode Service error Success не показывается Нужна проверка AuthApp
Нет integrationSourceCode в runtime Пользователь этого не видит Loyalty attach пропускается, warning в logs Автоматического retry нет
Ошибка add-card-to-loyalty Пользователь всё равно видит success Карта могла не привязаться к акции Автоматического retry/status нет
Ошибка update-permission Пользователь всё равно видит success Разрешения могли не обновиться Автоматического retry/status нет
Gena вернула подтверждённую ошибку В CMS публикация отменена, статус failed Лендинг не опубликован После исправления
Ответ Gena неоднозначен или timeout В CMS статус unknown Лендинг не опубликован Только после ручной сверки
Ошибка Strapi после успешной Gena Программа уже есть, лендинг ещё не опубликован Статус Gena остаётся registered Publish повторно без вызова Gena

10. Подтверждено и требует решения

Подтверждено текущим кодом

Открытые решения для менеджера и владельцев интеграций

Вопрос Текущее поведение Что нужно зафиксировать
Какое loyalty_id верно для каждого лендинга Используется integrationSourceCode; в документах встречались разные варианты, например bonus3000 и Ris Единый справочник «лендинг → код программы» и ответственный за значения
Можно ли разделять slug и integrationSourceCode CMS разрешает указывать разные значения, но поиск закрытого лендинга и переключение настроек интерфейса связывают их Либо закрепить правило, что они совпадают, либо разделить поиск программы и логику интерфейса в коде
Достаточна ли защита закрытого лендинга Отметка доступа в браузере не защищена цифровой подписью Перенести авторизацию доступа на сторону сервера, если требуется строгая защита от взлома
Кто отправляет Email/SMS с картой Cards отдельный запрос на отправку не делает и статус доставки не получает Подтвердить, что письма отправляет CRM METRO, зафиксировать адресатов и гарантированное время доставки (SLA)
Допустим ли успех на экране при сбое привязки к акции Да, по принципу «постарались выполнить, но ошибку клиенту не показываем» (Best Effort) Оставить так или добавить фоновый повтор при сбое (Retry), мониторинг и статус для менеджера
Допустим ли авто-выбор первой карты при наличии нескольких Первая карта привязывается автоматически Подтверждение продуктом для всех типов лендингов
Когда погашать одноразовый промокод Сразу при успешном вводе промокода до регистрации Оставить так или перенести погашение на момент успешного выпуска карты
Должен ли запрос согласий передавать Push=false Сейчас передаёт отказ от Push явно Подтвердить, что это случайно не сбрасывает Push-согласие, выданное клиентом в мобильном приложении METRO
Должен ли чекбокс рекламы быть обязательным везде В metropoliya24 — обязателен; в других компактных сценариях чекбокс можно не отмечать Зафиксировать единое бизнес-правило для всех лендингов
Зачем Email существующего клиента на 1 шаге Поле обязательное на экране, но профиль клиента в базе METRO им не перезаписывается Убрать, сделать необязательным или описать отдельное назначение
Защита обязательных полей нового клиента в CMS Сервер требует имя, фамилию, дату рождения и Email, но контент-менеджер может случайно скрыть эти поля в блоке Добавить проверку обязательного набора полей перед публикацией в CMS
Статус сквозного тестирования (E2E) нового клиента Логика реализована в коде, но требуется подтверждение на живом тестовом стенде METRO Зафиксировать дату, тестовый стенд и результат финального сквозного теста

До закрытия этих решений схему следует считать точным описанием поведения Cards, но не подтверждением внутренних гарантий AuthApp, CRM, Gena и каналов доставки.


11. Технические источники

При расхождении с более старыми статусными документами этот файл описывает текущее поведение кода. Целевые изменения должны фиксироваться отдельным решением и только затем переноситься в реализацию и схему.