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

UC-IDN-001. Вход/регистрация по СМС (с заполнением имени и фиксацией реферальной ссылки)

Назначение

Основной пользовательский сценарий платформы: вход или автоматическая регистрация по номеру телефона и СМС-коду. Вызывает гость с формы входа; на выходе получает access-токен и, при первом обращении, готовую учётку инвестора. Дополнительно поддерживает ленивое заполнение имени (FillName) и фиксацию реферальной ссылки (referrerUrl) для последующей привязки реферера. Связан с контекстом identity.

Metadata

ПолеЗначение
IDUC-IDN-001
Контекстidentity (см. docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольГость (до confirm) → Инвестор / Агент / Девелопер / МенеджерПИН (после confirm)
Permissions / scopeдо confirm — permissions не требуются (анонимные операции входа); после регистрации инвестору выдаётся системная роль investor в scope Platform
TenantTenantId = null для инвестора; для пользователя компании — TenantId = CompanyId (ADR-0052)
Record-levelSignInConfirmationRequest изолируется по PhoneNumber; учётка — по UserId

Предусловие

  • СМС-verifier доступен ИЛИ включён тест-контур (RealCodeDispatched = false).
  • Для существующего пользователя: UserEntity.Status ∈ {Invited, Active}, LockoutUntil не в будущем (INV-8 user.md).
  • Для пользователя компании: Company.Status = Active (INV-9 user.md).
  • Rate-limit по номеру/IP не исчерпан (INV-S4 user-session.md: ≤ 1 отправки/60 сек, ≤ 5/час).

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементФорма/модалка авторизации (лендинг /, гейт каталога /lots, кнопки «Забронировать»/«Заявка на презентацию»)

Основной Поток

  1. Пользователь вводит номер телефона (опционально имя) в форме авторизации; интерфейс подставляет реферальную ссылку (referrerUrl — slug реферальной ссылки, ≤ 64 симв.), если пользователь пришёл по реферальному переходу.
  2. Система принимает запрос входа/регистрации от гостя с телом SignUpOrSignInForm { phoneNumber (1..32), firstName? (≤128), sendSmsCode? (bool), referrerUrl? (≤64) }.
  3. Проверить вход и предусловия: формат телефона (нормализация в E.164); лимит отправки кодов по номеру и IP не превышен (INV-S4) — иначе отказ IDENTITY_CONFIRMATION_RATE_LIMITED.
  4. Найти пользователя по номеру телефона. Отсутствие означает регистрацию, наличие — вход (INV-1: один пользователь на номер).
  5. Если пользователя нет → создать инвестора (Type = Investor, CompanyId = null, RegisteredVia = SmsSignUp, LastName может быть пустым — INV-2), зафиксировать ReferrerUrl, выдать системную роль investor в scope Platform; outcome = Registered. Если пользователь есть → outcome = SignedIn и выбрать следующий шаг nextStep: RequiresSmsCode (при sendSmsCode = true или отсутствии пароля/пин-кода) либо RequiresPassword / RequiresPinCode (по наличию hasPassword / hasPinCode).
  6. Для СМС-шага погасить предыдущий незавершённый запрос кода (Pending → Superseded, INV-S1) и создать новый SignInConfirmationRequest (Purpose = SignIn, PhoneNumber, код CSPRNG 4–6 цифр хранится хэшем HMAC-SHA256, ExpiresAt = now + 5 мин).
  7. Зафиксировать факты: при регистрации — создание учётки и интеграционное событие UserUpsertedKafkaEventpin.identity.users.v1 (со снапшотом и ReferrerUrl для контекста referral); при создании Pending-запроса — отправка СМС verifier-ом (событие ConfirmationCodeRequestedEvent). Учётка, назначение роли, запрос кода и публикация события фиксируются в одном согласованном изменении.
  8. Система возвращает { outcome, nextStep, nextAction?, signInConfirmationRequestId?, realConfirmationCodeDispatched? }.
  9. Интерфейс показывает поле ввода кода (nextStep = RequiresSmsCode); пользователь вводит код из СМС.
  10. Система принимает подтверждение ConfirmSignInForm { phoneNumber, signInConfirmationRequestId, confirmationCode (≤16), firstName?, referrerUrl? }.
  11. Проверить запрос кода по идентификатору: статус Pending и ExpiresAt > now (INV-S3); увеличить счётчик попыток ДО сравнения (INV-S2, до 5 попыток); сравнить код в постоянном времени (INV-S5). Найти пользователя по номеру и проверить: Status ∉ {Blocked, Archived} (иначе IDENTITY_USER_BLOCKED), LockoutUntil не в будущем (INV-8, иначе IDENTITY_USER_LOCKED_OUT), компания пользователя Active (INV-9, иначе IDENTITY_COMPANY_SUSPENDED).
  12. Подтвердить запрос: Pending → Confirmed, ConfirmedAt = now; заполнить имя только если оно пустое (FillNameIfEmpty, INV-11 — иначе имя молча не меняется); при первом входе Invited → Active; сбросить FailedAccessAttempts; LastSignInAt = now; выдать access-JWT и создать RefreshSession (SignInMethod = Sms, устройство из UA/IP, ExpiresAt = now + 30 дней); определить nextAction для интерфейса (FillName — имя пустое / FillPinCode / FillEmail / FillPassword).
  13. Зафиксировать факт входа: события SignInConfirmedEvent, UserSignedInEvent и интеграционное UserSignedInKafkaEventpin.identity.sign-ins.v1 (UserId, SessionId, SignInMethod = Sms, Device.Platform). Подтверждение, обновление учётки, создание сессии и публикация события — в одном согласованном изменении.
  14. Система возвращает { accessToken, nextAction? }.
  15. Интерфейс сохраняет токен, обновляет данные текущего пользователя, закрывает форму; при nextAction = FillName показывает форму имени (не блокирует).
  16. Контекст referral по событию pin.identity.users.v1 выполняет привязку реферера; user-activity по pin.identity.sign-ins.v1 связывает анонимную сессию.

Альтернативные Потоки

A1. У пользователя установлен пароль/пин-код и sendSmsCode не запрошен

  1. Шаг 5 возвращает nextStep = RequiresPassword (или RequiresPinCode) без создания SignInConfirmationRequest и без отправки СМС.
  2. Продолжение — UC-IDN-002 (вход по паролю/пин-коду).

A2. Неверный код (< 5 попыток)

  1. Шаг 11: код не совпал → счётчик попыток увеличен, отказ IDENTITY_CONFIRMATION_INVALID.
  2. Интерфейс показывает ошибку, пользователь повторяет ввод. Возврат к шагу 9.

A3. Код истёк или попытки исчерпаны

  1. ExpiresAt <= nowIDENTITY_CONFIRMATION_EXPIRED; 5-я неверная попытка → статус Exhausted, IDENTITY_CONFIRMATION_EXHAUSTED.
  2. Интерфейс предлагает «Отправить код повторно» → новый запрос входа (старый Pending гасится Superseded, INV-S1). Возврат к шагу 2.

A4. Повторная отправка кода

  1. Интерфейс запрашивает повторную отправку кода для уже известного номера (RequestSignInCodeForm { phoneNumber }) — тот же request-шаг; результат { nextStep, signInConfirmationRequestId?, realConfirmationCodeDispatched? }. Возврат к шагу 9.

A5. Ленивая регистрация ранее создала учётку (бронь/заявка по ФИО+телефону)

  1. Шаг 4 находит пользователя RegisteredVia = LazyRegistration с пустым именем → outcome = SignedIn.
  2. Шаг 12: заполнение имени из формы confirm (INV-11). Поток не меняется.

A6. Конкурентный повторный запрос кода

  1. Две вкладки/устройства почти одновременно запрашивают код на один номер (SignIn). INV-S1 гарантирует единственный Pending на (PhoneNumber, SignIn): побеждает последний зафиксированный запрос, ранее созданный переводится Pending → Superseded.
  2. Confirm кодом из «проигравшего» (Superseded) запроса → статус ≠ PendingIDENTITY_CONFIRMATION_INVALID (или IDENTITY_CONFIRMATION_EXPIRED). Пользователь вводит код из последней СМС. Возврат к шагу 9.

A7. Повторное подтверждение одним кодом (double submit)

  1. Первый confirm переводит запрос Pending → Confirmed и создаёт одну RefreshSession.
  2. Второй confirm того же signInConfirmationRequestId видит статус ≠ PendingIDENTITY_CONFIRMATION_EXPIRED/IDENTITY_CONFIRMATION_INVALID; вторая сессия не выдаётся (идемпотентность входа: один запрос ⇒ не более одной сессии).

A8. Учётка заблокирована/архивирована между request и confirm

  1. Между шагами 7 и 11 администратор/синк блокирует учётку (Active → Blocked) или архивирует её.
  2. Шаг 11 проверяет статус на момент подтвержденияIDENTITY_USER_BLOCKED (для Archived — тот же код, вход недостижим); сессия не создаётся, ранее валидный код бесполезен.

Постусловие

  • Учётка UserEntity существует (Type = Investor, Status = Active, PhoneNumberConfirmedAt заполнен), либо существующая переведена Invited → Active.
  • SignInConfirmationRequest в статусе Confirmed; выдан access-JWT; создана RefreshSession (Active).
  • При регистрации создано RoleAssignment (investor, Platform, Active); ReferrerUrl зафиксирован.
  • Асинхронные следствия — привязка реферера в referral (по pin.identity.users.v1) и связывание анонимной сессии в user-activity (по pin.identity.sign-ins.v1) — eventual (окно — секунды): вход и выдача accessToken от них не зависят, невалидный referrerUrl вход не откатывает.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400Формат телефона/полей формыПодсветить поле
IDENTITY_CONFIRMATION_RATE_LIMITED429Превышен лимит отправки кодов (INV-S4)Показать таймер повторной отправки
IDENTITY_CONFIRMATION_INVALID400Неверный код (INV-S5)Ошибка под полем кода, остаток попыток
IDENTITY_CONFIRMATION_EXPIRED400ExpiresAt <= now (INV-S3)Кнопка «Отправить код повторно»
IDENTITY_CONFIRMATION_EXHAUSTED4005 неверных попыток (INV-S2)Кнопка «Отправить код повторно»
IDENTITY_USER_BLOCKED403Status = BlockedСообщение «обратитесь в поддержку»
IDENTITY_USER_LOCKED_OUT423LockoutUntil > now (INV-8)Сообщение с временем разблокировки
IDENTITY_COMPANY_SUSPENDED403Компания пользователя Suspended (INV-9)Сообщение «доступ компании приостановлен»

Доменные События

СобытиеКогда поднимаетсяSubscribers
UserRegisteredEventСоздание учётки на request-шагеin-proc: инициализация дефолтов
ConfirmationCodeRequestedEventСоздание Pending-запросаhandler отправки СМС через verifier
SignInConfirmedEvent / UserSignedInEventУспешный confirmin-proc аудит

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
UserUpsertedKafkaEvent.v1pin.identity.users.v1Регистрация (шаг 7, вместе с ReferrerUrl)referral, crm, profile, messenger, user-activity, notifications
UserSignedInKafkaEvent.v1pin.identity.sign-ins.v1Успешный confirm (шаг 13)user-activity (anonymous_id), notifications (новое устройство)

Acceptance Criteria

#КритерийПроверка
AC1Новый номер: outcome = Registered, создаётся Investor с RoleAssignment(investor, Platform)интеграционный тест регистрации + запрос назначений
AC2Существующий номер: outcome = SignedIn, дубль не создаётся (INV-1)повторный вызов входа/регистрации
AC3Верный код → accessToken; неверный ×5 → Exhausted и запрет дальнейших confirmтест лимита попыток
AC4firstName в confirm заполняет только пустое имя (INV-11)тест: имя задано → не меняется
AC5referrerUrl попадает в UserUpsertedKafkaEvent только при регистрацииконтракт-тест события
AC6Повторный request гасит предыдущий Pending (Superseded, INV-S1)два request подряд, confirm первым кодом → fail
AC7Rate-limit: 2-я отправка в 60 сек → 429тест счётчика лимита
AC8Повторный confirm одним кодом не выдаёт вторую сессию (идемпотентность входа)двойной confirm одного signInConfirmationRequestId
AC9Блокировка между request и confirm → confirm возвращает IDENTITY_USER_BLOCKEDтест блокировки в середине flow

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы