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

UC-IDN-004. Смена телефона / email

Назначение

Пользователь самостоятельно меняет контакты учётки: телефон (код приходит на новый номер) или email (код приходит на новый адрес) — с подтверждением владения новым контактом. Вызывает аутентифицированный владелец учётки. Для менеджеров застройщика self-service запрещён: их контакты управляются внешней системой (ContactsManagedBy = TalentSync, см. A3). Связан с контекстом identity.

Metadata

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

Актор

ПолеЗначение
РольИнвестор / Агент / Агентство / МенеджерПИН / СлужбаЗаботы (self-service). Девелопер (менеджер застройщика) — ЗАПРЕЩЕНО (A3)
Permissions / scopeidentity.security.manage-self (self-scope)
Tenantне влияет: self-операция, субъект = актор (ADR-0052)
Record-levelтолько собственная учётка

Предусловие

  • Пользователь аутентифицирован, Status = Active.
  • ContactsManagedBy = SelfService (INV-4; иначе A3).
  • Новый телефон/email не занят другой не-архивной учёткой (INV-1/INV-3).
  • Rate-limit кодов не исчерпан (INV-S4).

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК → «Контакты» → «Изменить телефон» / «Изменить email»

Основной Поток (смена телефона)

  1. Пользователь вводит новый номер в форме ЛК.
  2. Система принимает запрос RequestPhoneChangeForm { newPhoneNumber (1..32) } (актор — из JWT).
  3. Проверить: формат номера (нормализация E.164); ContactsManagedBy = SelfService (INV-4) — иначе отказ IDENTITY_CONTACTS_MANAGED_EXTERNALLY; новый номер не занят среди не-архивных учёток (INV-1) — иначе IDENTITY_USER_PHONE_TAKEN; newPhoneNumber != user.PhoneNumber; лимит кодов по новому номеру и IP не исчерпан (INV-S4).
  4. Погасить предыдущий Pending цели PhoneChange (Superseded, INV-S1); создать SignInConfirmationRequest (Purpose = PhoneChange, UserId, PhoneNumber = newPhoneNumber — куда уходит код, NewPhoneNumber = newPhoneNumber, код хранится хэшем, ExpiresAt = now + 5 мин) (INV-S8: NewPhoneNumber обязателен при этой цели).
  5. Зафиксировать факт: событие ConfirmationCodeRequestedEvent → СМС на новый номер.
  6. Система возвращает { phoneChangeConfirmationRequestId, realConfirmationCodeDispatched }.
  7. Пользователь вводит код, пришедший на новый номер (доказательство владения).
  8. Система принимает подтверждение ConfirmPhoneChangeForm { newPhoneNumber (1..32), code (1..16) }.
  9. Найти активный запрос по (UserId, Purpose = PhoneChange, Status = Pending); проверить совпадение request.NewPhoneNumber == normalize(newPhoneNumber); INV-S2/S3/S5 (попытки, TTL, постоянное время); повторно проверить уникальность номера (гонку с параллельной регистрацией окончательно разрешает уникальный индекс ux_users_phone_number).
  10. Подтвердить запрос; сменить телефон: PhoneNumber = NewPhoneNumber, PhoneNumberConfirmedAt = now, Version + 1; отозвать все Active RefreshSession пользователя, кроме текущей (INV-S7, причина — security change). Подтверждение, смена телефона, отзыв сессий и публикация события — в одном согласованном изменении (уникальный индекс телефона — финальный арбитр гонки).
  11. Зафиксировать факты: UserContactChangedEvent (Phone, old → new, маскированные), SessionRevokedEvent по каждой отозванной сессии; интеграционное UserUpsertedKafkaEventpin.identity.users.v1 (snapshot с новым PhoneNumber для доверенных consumer-ов crm/referral/messenger).
  12. Система возвращает успех; интерфейс обновляет данные пользователя и показывает новый номер.
  13. Consumers обновляют реплики контактов (crm lookup, messenger).

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

A1. Смена email

  1. Система принимает запрос RequestEmailChangeForm { newEmail (1..256) }.
  2. Проверить: ContactsManagedBy = SelfService (INV-4); email не занят (INV-3) — иначе IDENTITY_USER_EMAIL_TAKEN; создать SignInConfirmationRequest (Purpose = EmailChange, NewEmail = newEmail, PhoneNumber = user.PhoneNumber — для rate-limit); код уходит письмом на NewEmail.
  3. Подтверждение ConfirmEmailChangeForm { code (1..16) } → сменить email: Email = NewEmail, EmailConfirmedAt = now; событие UserContactChangedEvent (Email). Сессии не отзываются (email — не фактор входа).
  4. Завершается шагами 12–13 основного потока.

A2. Новый номер/email уже занят

  1. Request-шаг возвращает IDENTITY_USER_PHONE_TAKEN / IDENTITY_USER_EMAIL_TAKEN; код не отправляется.
  2. Пользователь вводит другой контакт. Возврат к шагу 1. (Merge учёток при легитимном конфликте — ручной процесс заботы, README «Известные ограничения».)

A3. Менеджер застройщика (ContactsManagedBy = TalentSync)

  1. Request-шаг отклоняется guard-ом INV-4 → IDENTITY_CONTACTS_MANAGED_EXTERNALLY (403).
  2. Интерфейс показывает: «Контакты управляются системой застройщика» и скрывает кнопки изменения (по признаку из профиля). Актуализация контактов — только синком Talent (UC-IDN-007).

A4. Неверный/истёкший код

  1. INV-S2/S3/S5 как в UC-IDN-001 A2/A3: IDENTITY_CONFIRMATION_INVALID / EXPIRED / EXHAUSTED; контакты не меняются; повторный request с шага 1.

Постусловие

  • Телефон: PhoneNumber = новый, PhoneNumberConfirmedAt = now; старый номер освобождён; все прочие Active-сессии отозваны (INV-S7); вход по СМС — только на новый номер.
  • Email: Email = новый, EmailConfirmedAt = now.
  • SignInConfirmationRequest в Confirmed; UserUpsertedKafkaEvent опубликован (для смены телефона).

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

КодHTTPУсловиеПоведение клиента
ValidationError400Формат номера/emailПодсветить поле
IDENTITY_CONTACTS_MANAGED_EXTERNALLY403ContactsManagedBy = TalentSync (INV-4)Скрыть форму, показать пояснение
IDENTITY_USER_PHONE_TAKEN409Номер занят (INV-1)«Номер уже используется»
IDENTITY_USER_EMAIL_TAKEN409Email занят (INV-3)«Email уже используется»
IDENTITY_CONFIRMATION_RATE_LIMITED429INV-S4Таймер повторной отправки
IDENTITY_CONFIRMATION_INVALID/EXPIRED/EXHAUSTED400INV-S5/S3/S2Как в UC-IDN-001
401 UNAUTHORIZED401Нет JWTФорма авторизации

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

СобытиеКогда поднимаетсяSubscribers
ConfirmationCodeRequestedEventrequest-шаг (СМС на новый номер / письмо на новый email)handler verifier
UserContactChangedEventconfirm-шаг (вид: Phone/Email; значения маскируются в логах)in-proc аудит
SessionRevokedEventОтзыв сессий при смене телефона (INV-S7)in-proc

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

СобытиеTopicКогда публикуетсяConsumers
UserUpsertedKafkaEvent.v1pin.identity.users.v1Успешная смена телефона (новый snapshot c PhoneNumber)crm (lookup), referral, messenger, profile, notifications
Гарантии доставки и согласованность реплик

UserUpsertedKafkaEvent — полный снапшот учётки с ключом партиционирования UserId (порядок событий на одного пользователя сохраняется); consumers идемпотентны по (UserId, Version) и применяют только более свежую версию. Смена email так же переопубликует снапшот (в нём — новый Email), поэтому реплики контактов в crm/profile/messenger сходятся к новому значению; отдельного события для email нет — источником служит тот же снапшот. До обработки события потребителями возможно окно рассинхронизации: часть реплик (lookup в crm, адресная книга messenger) короткое время отдаёт прежний контакт, тогда как identity (source of truth) уже принимает вход по СМС только на новый номер.

Арбитраж гонки на уникальный номер

Гонку «два параллельных confirm на один и тот же новый номер» и «confirm против регистрации того же номера» окончательно арбитрирует уникальный индекс ux_users_phone_number: ровно один победитель, проигравший получает IDENTITY_USER_PHONE_TAKEN.

Acceptance Criteria

#КритерийПроверка
AC1Код смены телефона доставляется на НОВЫЙ номер; подтверждение меняет PhoneNumberинтеграционный тест
AC2Занятый номер отклоняется на request-шаге, код не отправляется (INV-1)тест конфликта
AC3TalentSync-пользователь получает 403 на оба request-а (INV-4)тест менеджера застройщика
AC4Смена телефона отзывает остальные Active-сессии (INV-S7)тест: вторая сессия получает 401
AC5Гонка «confirm против параллельной регистрации того же номера» разрешается unique-индексом — ровно один победительконкурентный тест
AC6Смена email не трогает сессии и телефонтест A1
AC7Вход по СМС после смены работает только на новый номерсквозной тест с UC-IDN-001

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

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

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

  • API метод: identity/api/investor/request-phone-change.md, identity/api/investor/confirm-phone-change.md, identity/api/investor/request-email-change.md, identity/api/investor/confirm-email-change.md.
  • Domain entity: ../domain/user.md (INV-1/3/4), ../domain/user-session.md (Purpose = PhoneChange/EmailChange, INV-S7).
  • Правила синка (источник контактов менеджеров): ../domain/talent-replica.md.
  • Связанные UC: UC-IDN-001 (вход по новому номеру), UC-IDN-007 (контакты TalentSync-пользователей).