UC-IDN-001. Вход/регистрация по СМС (с заполнением имени и фиксацией реферальной ссылки)
Назначение
Основной пользовательский сценарий платформы: вход или автоматическая регистрация по номеру телефона и
СМС-коду. Вызывает гость с формы входа; на выходе получает access-токен и, при первом обращении, готовую
учётку инвестора. Дополнительно поддерживает ленивое заполнение имени (FillName) и фиксацию реферальной
ссылки (referrerUrl) для последующей привязки реферера. Связан с контекстом identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-001 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Гость (до confirm) → Инвестор / Агент / Девелопер / МенеджерПИН (после confirm) |
| Permissions / scope | до confirm — permissions не требуются (анонимные операции входа); после регистрации инвестору выдаётся системная роль investor в scope Platform |
| Tenant | TenantId = null для инвестора; для пользователя компании — TenantId = CompanyId (ADR-0052) |
| Record-level | SignInConfirmationRequest изолируется по 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, кнопки «Забронировать»/«Заявка на презентацию») |
Основной Поток
- Пользователь вводит номер телефона (опционально имя) в форме авторизации; интерфейс подставляет
реферальную ссылку (
referrerUrl— slug реферальной ссылки, ≤ 64 симв.), если пользователь пришёл по реферальному переходу. - Система принимает запрос входа/регистрации от гостя с телом
SignUpOrSignInForm { phoneNumber (1..32), firstName? (≤128), sendSmsCode? (bool), referrerUrl? (≤64) }. - Проверить вход и предусловия: формат телефона (нормализация в E.164); лимит отправки кодов по номеру и
IP не превышен (INV-S4) — иначе отказ
IDENTITY_CONFIRMATION_RATE_LIMITED. - Найти пользователя по номеру телефона. Отсутствие означает регистрацию, наличие — вход (INV-1: один пользователь на номер).
- Если пользователя нет → создать инвестора (
Type = Investor,CompanyId = null,RegisteredVia = SmsSignUp,LastNameможет быть пустым — INV-2), зафиксироватьReferrerUrl, выдать системную рольinvestorв scopePlatform;outcome = Registered. Если пользователь есть →outcome = SignedInи выбрать следующий шагnextStep:RequiresSmsCode(приsendSmsCode = trueили отсутствии пароля/пин-кода) либоRequiresPassword/RequiresPinCode(по наличиюhasPassword/hasPinCode). - Для СМС-шага погасить предыдущий незавершённый запрос кода (
Pending → Superseded, INV-S1) и создать новыйSignInConfirmationRequest (Purpose = SignIn, PhoneNumber, код CSPRNG 4–6 цифр хранится хэшем HMAC-SHA256, ExpiresAt = now + 5 мин). - Зафиксировать факты: при регистрации — создание учётки и интеграционное событие
UserUpsertedKafkaEvent→pin.identity.users.v1(со снапшотом иReferrerUrlдля контекста referral); при создании Pending-запроса — отправка СМС verifier-ом (событиеConfirmationCodeRequestedEvent). Учётка, назначение роли, запрос кода и публикация события фиксируются в одном согласованном изменении. - Система возвращает
{ outcome, nextStep, nextAction?, signInConfirmationRequestId?, realConfirmationCodeDispatched? }. - Интерфейс показывает поле ввода кода (
nextStep = RequiresSmsCode); пользователь вводит код из СМС. - Система принимает подтверждение
ConfirmSignInForm { phoneNumber, signInConfirmationRequestId, confirmationCode (≤16), firstName?, referrerUrl? }. - Проверить запрос кода по идентификатору: статус
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). - Подтвердить запрос:
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). - Зафиксировать факт входа: события
SignInConfirmedEvent,UserSignedInEventи интеграционноеUserSignedInKafkaEvent→pin.identity.sign-ins.v1(UserId,SessionId,SignInMethod = Sms,Device.Platform). Подтверждение, обновление учётки, создание сессии и публикация события — в одном согласованном изменении. - Система возвращает
{ accessToken, nextAction? }. - Интерфейс сохраняет токен, обновляет данные текущего пользователя, закрывает форму; при
nextAction = FillNameпоказывает форму имени (не блокирует). - Контекст
referralпо событиюpin.identity.users.v1выполняет привязку реферера;user-activityпоpin.identity.sign-ins.v1связывает анонимную сессию.
Альтернативные Потоки
A1. У пользователя установлен пароль/пин-код и sendSmsCode не запрошен
- Шаг 5 возвращает
nextStep = RequiresPassword(илиRequiresPinCode) без созданияSignInConfirmationRequestи без отправки СМС. - Продолжение — UC-IDN-002 (вход по паролю/пин-коду).
A2. Неверный код (< 5 попыток)
- Шаг 11: код не совпал → счётчик попыток увеличен, отказ
IDENTITY_CONFIRMATION_INVALID. - Интерфейс показывает ошибку, пользователь повторяет ввод. Возврат к шагу 9.
A3. Код истёк или попытки исчерпаны
ExpiresAt <= now→IDENTITY_CONFIRMATION_EXPIRED; 5-я неверная попытка → статусExhausted,IDENTITY_CONFIRMATION_EXHAUSTED.- Интерфейс предлагает «Отправить код повторно» → новый запрос входа (старый Pending гасится
Superseded, INV-S1). Возврат к шагу 2.
A4. Повторная отправка кода
- Интерфейс запрашивает повторную отправку кода для уже известного номера
(
RequestSignInCodeForm { phoneNumber }) — тот же request-шаг; результат{ nextStep, signInConfirmationRequestId?, realConfirmationCodeDispatched? }. Возврат к шагу 9.
A5. Ленивая регистрация ранее создала учётку (бронь/заявка по ФИО+телефону)
- Шаг 4 находит пользователя
RegisteredVia = LazyRegistrationс пустым именем →outcome = SignedIn. - Шаг 12: заполнение имени из формы confirm (INV-11). Поток не меняется.
A6. Конкурентный повторный запрос кода
- Две вкладки/устройства почти одновременно запрашивают код на один номер (
SignIn). INV-S1 гарантирует единственныйPendingна (PhoneNumber,SignIn): побеждает последний зафиксированный запрос, ранее созданный переводитсяPending → Superseded. - Confirm кодом из «проигравшего» (Superseded) запроса → статус ≠
Pending→IDENTITY_CONFIRMATION_INVALID(илиIDENTITY_CONFIRMATION_EXPIRED). Пользователь вводит код из последней СМС. Возврат к шагу 9.
A7. Повторное подтверждение одним кодом (double submit)
- Первый confirm переводит запрос
Pending → Confirmedи создаёт однуRefreshSession. - Второй confirm того же
signInConfirmationRequestIdвидит статус ≠Pending→IDENTITY_CONFIRMATION_EXPIRED/IDENTITY_CONFIRMATION_INVALID; вторая сессия не выдаётся (идемпотентность входа: один запрос ⇒ не более одной сессии).
A8. Учётка заблокирована/архивирована между request и confirm
- Между шагами 7 и 11 администратор/синк блокирует учётку (
Active → Blocked) или архивирует её. - Шаг 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 | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Формат телефона/полей формы | Подсветить поле |
IDENTITY_CONFIRMATION_RATE_LIMITED | 429 | Превышен лимит отправки кодов (INV-S4) | Показать таймер повторной отправки |
IDENTITY_CONFIRMATION_INVALID | 400 | Неверный код (INV-S5) | Ошибка под полем кода, остаток попыток |
IDENTITY_CONFIRMATION_EXPIRED | 400 | ExpiresAt <= now (INV-S3) | Кнопка «Отправить код повторно» |
IDENTITY_CONFIRMATION_EXHAUSTED | 400 | 5 неверных попыток (INV-S2) | Кнопка «Отправить код повторно» |
IDENTITY_USER_BLOCKED | 403 | Status = Blocked | Сообщение «обратитесь в поддержку» |
IDENTITY_USER_LOCKED_OUT | 423 | LockoutUntil > now (INV-8) | Сообщение с временем разблокировки |
IDENTITY_COMPANY_SUSPENDED | 403 | Компания пользователя Suspended (INV-9) | Сообщение «доступ компании приостановлен» |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
UserRegisteredEvent | Создание учётки на request-шаге | in-proc: инициализация дефолтов |
ConfirmationCodeRequestedEvent | Создание Pending-запроса | handler отправки СМС через verifier |
SignInConfirmedEvent / UserSignedInEvent | Успешный confirm | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
UserUpsertedKafkaEvent.v1 | pin.identity.users.v1 | Регистрация (шаг 7, вместе с ReferrerUrl) | referral, crm, profile, messenger, user-activity, notifications |
UserSignedInKafkaEvent.v1 | pin.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 | тест лимита попыток |
| AC4 | firstName в confirm заполняет только пустое имя (INV-11) | тест: имя задано → не меняется |
| AC5 | referrerUrl попадает в UserUpsertedKafkaEvent только при регистрации | контракт-тест события |
| AC6 | Повторный request гасит предыдущий Pending (Superseded, INV-S1) | два request подряд, confirm первым кодом → fail |
| AC7 | Rate-limit: 2-я отправка в 60 сек → 429 | тест счётчика лимита |
| AC8 | Повторный confirm одним кодом не выдаёт вторую сессию (идемпотентность входа) | двойной confirm одного signInConfirmationRequestId |
| AC9 | Блокировка между request и confirm → confirm возвращает IDENTITY_USER_BLOCKED | тест блокировки в середине flow |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Правила реферальной привязки (attach-rules) — дом доменных правил
- Use Cases (5): UC-PRF-001. Заполнение профиля и персоны «Сам» (первый вход инвестора в ЛК), UC-REF-001. Создание реферальной ссылки с каналом и шаринг, UC-REF-002. Аноним открыл реферальную ссылку (cookie 30 дней, validateReferralLink), UC-REF-003. Регистрация по реферальной ссылке → связь + лид (правило R1), UC-REF-004. Авторизация со ссылкой в окне lazy-регистрации → связь + лид (правило R2)
- index.md (1): Identity Service (Pin.Identity)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: sign-up-or-sign-in.md, confirm-sign-in.md, request-sign-in-code.md.
- Domain entity: ../domain/user.md, ../domain/user-session.md, ../domain/role.md.
- Frontend: форма/модалка авторизации.
- State machine: stateDiagram в ../domain/user-session.md.
- Связанные UC: UC-IDN-002 (пароль), реферальная привязка —
docs/06-services/referral/.