Назначение: дать менеджеру единую карту пользовательских сценариев, границ систем, интеграций и поведения при ошибках — без необходимости читать код.
Статус документа:
as isпо текущей реализации в репозитории на 26.08.2026. Неподтверждённое внешнее поведение и решения, которые нельзя вывести из кода, вынесены в раздел «Открытые решения».В области документа: доступ к public/private лендингу, OTP, существующий и новый клиент, Post-Auth интеграции, публикация лендинга и регистрация программы в Gena.
Вне области: детальный Keycloak SSO, создание пакетов промокодов и инфраструктура деплоя. Они описаны в общей архитектурной документации.
cardholders[], Cards без отдельного экрана выбора привязывает cardholders[0] — независимо от количества карт.get-card-profile. Пользовательский success невозможен без профиля с barcode.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 письма не отправляет |
| Финал на экране | «Вы уже зарегистрированы» + штрихкод | «Карта выпущена!» + штрихкод | Статус «Опубликовано» в админке |
| Контур | Ответственность |
|---|---|
| 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. |
Важные правила доступа:
external_loyalty_id.multi использование фиксируется уже при успешном входе на лендинг, до завершения регистрации. Если пользователь уйдёт или получит ошибку позже, код уже останется использованным.Известная уязвимость (Security Gap): в браузере сохраняется простая отметка
authorized, и сервер открывает контент по заголовку от браузера без проверки цифровой подписи. Этого достаточно для скрытия контента в интерфейсе от обычного пользователя, но технически продвинутый пользователь может получить доступ к странице без ввода промокода. До исправления это не обеспечивает 100% защиту закрытого лендинга.
| Вопрос | Существующий клиент | Новый клиент |
|---|---|---|
| Как определяется | 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 |
| Элемент | 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.
Эта схема показывает интеграционные вызовы успешного сценария. Ошибки и их последствия собраны отдельно в разделе 9.
AuthApp cookies не передаются во Frontend. Они используются внутри backend session для последовательных вызовов и очищаются после финального результата. При этом телефон, Email, consent и metadata могут оставаться в session до следующего сброса или истечения самой сессии.
Пользовательская регистрация и регистрация промо-программы — два разных flow. В Gena напрямую ходит только Cards Backend во время CMS-публикации.
| Статус | Что произошло | Что делать менеджеру |
|---|---|---|
not_registered |
Черновик ещё не отправлялся | Завершить настройку и опубликовать |
registering |
Запрос выполняется или состояние зависло | Не повторять Publish; дождаться результата, при зависании обратиться к разработчику |
registered |
Gena подтвердила создание программы | Можно публиковать повторно; Gena повторно не вызывается |
failed |
Получена подтверждённая ошибка | Исправить указанную причину и повторить Publish |
unknown |
Неясно, создала ли Gena программу | Не повторять Publish; нужна ручная сверка с Gena |
legacy_manual |
Старая программа заведена вручную | Gena не вызывается, изменения синхронизируются вручную |
Правила, которые важно знать:
slug, даты начала/окончания и offer.code.registered повторный Publish не обновляет данные в Gena. Для обычного лендинга локальное изменение дат или программы сопровождается предупреждением; у мастер-шаблона программа блокируется отдельным правилом. Любые допустимые изменения в Gena обновляются вручную.| Данные | Источник | Куда передаются | Что хранит 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 |
| 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-поведение.
| Инициатор → получатель | 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 из браузера нет.
POST /auth/api/v1/public/send_otp){
"result": "success",
"methods_of_auth": {
"retry_timeout_seconds": 60,
"list": ["sms"]
}
}
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": "Неверный код"
}
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
}
}
POST /auth/api/v1/auth/endpoints/add-card-to-loyalty){
"loyalty_id": "bonus3000",
"external_loyalty_id": "PROMO-2026"
}
{
"result": "success"
}
POST /auth/api/v1/auth/endpoints/update-permission){
"notifyByEmail": true,
"notifyBySMS": true,
"notifyByPush": false
}
{
"result": "success"
}
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"
}
| Ситуация | Что видит пользователь / менеджер | Итог | Повтор |
|---|---|---|---|
| Нет 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 |
cardholders[] всегда приводит к auto-bind первой карты.barcode берётся только из get-card-profile.update-permission вызывается только при consentAds=true.external_loyalty_id, только если текущий lookup распознал private по slug === loyalty_id.| Вопрос | Текущее поведение | Что нужно зафиксировать |
|---|---|---|
Какое 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 и каналов доставки.
При расхождении с более старыми статусными документами этот файл описывает текущее поведение кода. Целевые изменения должны фиксироваться отдельным решением и только затем переноситься в реализацию и схему.