UC-IDN-004. Смена телефона / email
Назначение
Пользователь самостоятельно меняет контакты учётки: телефон (код приходит на новый номер) или email
(код приходит на новый адрес) — с подтверждением владения новым контактом. Вызывает аутентифицированный
владелец учётки. Для менеджеров застройщика self-service запрещён: их контакты управляются внешней системой
(ContactsManagedBy = TalentSync, см. A3). Связан с контекстом identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-004 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор / Агент / Агентство / МенеджерПИН / СлужбаЗаботы (self-service). Девелопер (менеджер застройщика) — ЗАПРЕЩЕНО (A3) |
| Permissions / scope | identity.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» |
Основной Поток (смена телефона)
- Пользователь вводит новый номер в форме ЛК.
- Система принимает запрос
RequestPhoneChangeForm { newPhoneNumber (1..32) }(актор — из JWT). - Проверить: формат номера (нормализация E.164);
ContactsManagedBy = SelfService(INV-4) — иначе отказIDENTITY_CONTACTS_MANAGED_EXTERNALLY; новый номер не занят среди не-архивных учёток (INV-1) — иначеIDENTITY_USER_PHONE_TAKEN;newPhoneNumber != user.PhoneNumber; лимит кодов по новому номеру и IP не исчерпан (INV-S4). - Погасить предыдущий Pending цели
PhoneChange(Superseded, INV-S1); создатьSignInConfirmationRequest (Purpose = PhoneChange, UserId, PhoneNumber = newPhoneNumber — куда уходит код, NewPhoneNumber = newPhoneNumber, код хранится хэшем, ExpiresAt = now + 5 мин)(INV-S8:NewPhoneNumberобязателен при этой цели). - Зафиксировать факт: событие
ConfirmationCodeRequestedEvent→ СМС на новый номер. - Система возвращает
{ phoneChangeConfirmationRequestId, realConfirmationCodeDispatched }. - Пользователь вводит код, пришедший на новый номер (доказательство владения).
- Система принимает подтверждение
ConfirmPhoneChangeForm { newPhoneNumber (1..32), code (1..16) }. - Найти активный запрос по (
UserId,Purpose = PhoneChange,Status = Pending); проверить совпадениеrequest.NewPhoneNumber == normalize(newPhoneNumber); INV-S2/S3/S5 (попытки, TTL, постоянное время); повторно проверить уникальность номера (гонку с параллельной регистрацией окончательно разрешает уникальный индексux_users_phone_number). - Подтвердить запрос; сменить телефон:
PhoneNumber = NewPhoneNumber,PhoneNumberConfirmedAt = now,Version + 1; отозвать все ActiveRefreshSessionпользователя, кроме текущей (INV-S7, причина — security change). Подтверждение, смена телефона, отзыв сессий и публикация события — в одном согласованном изменении (уникальный индекс телефона — финальный арбитр гонки). - Зафиксировать факты:
UserContactChangedEvent (Phone, old → new, маскированные),SessionRevokedEventпо каждой отозванной сессии; интеграционноеUserUpsertedKafkaEvent→pin.identity.users.v1(snapshot с новымPhoneNumberдля доверенных consumer-ов crm/referral/messenger). - Система возвращает успех; интерфейс обновляет данные пользователя и показывает новый номер.
- Consumers обновляют реплики контактов (crm lookup, messenger).
Альтернативные Потоки
A1. Смена email
- Система принимает запрос
RequestEmailChangeForm { newEmail (1..256) }. - Проверить:
ContactsManagedBy = SelfService(INV-4); email не занят (INV-3) — иначеIDENTITY_USER_EMAIL_TAKEN; создатьSignInConfirmationRequest (Purpose = EmailChange, NewEmail = newEmail, PhoneNumber = user.PhoneNumber — для rate-limit); код уходит письмом наNewEmail. - Подтверждение
ConfirmEmailChangeForm { code (1..16) }→ сменить email:Email = NewEmail,EmailConfirmedAt = now; событиеUserContactChangedEvent (Email). Сессии не отзываются (email — не фактор входа). - Завершается шагами 12–13 основного потока.
A2. Новый номер/email уже занят
- Request-шаг возвращает
IDENTITY_USER_PHONE_TAKEN/IDENTITY_USER_EMAIL_TAKEN; код не отправляется. - Пользователь вводит другой контакт. Возврат к шагу 1. (Merge учёток при легитимном конфликте — ручной процесс заботы, README «Известные ограничения».)
A3. Менеджер застройщика (ContactsManagedBy = TalentSync)
- Request-шаг отклоняется guard-ом INV-4 →
IDENTITY_CONTACTS_MANAGED_EXTERNALLY(403). - Интерфейс показывает: «Контакты управляются системой застройщика» и скрывает кнопки изменения (по признаку из профиля). Актуализация контактов — только синком Talent (UC-IDN-007).
A4. Неверный/истёкший код
- 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 | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Формат номера/email | Подсветить поле |
IDENTITY_CONTACTS_MANAGED_EXTERNALLY | 403 | ContactsManagedBy = TalentSync (INV-4) | Скрыть форму, показать пояснение |
IDENTITY_USER_PHONE_TAKEN | 409 | Номер занят (INV-1) | «Номер уже используется» |
IDENTITY_USER_EMAIL_TAKEN | 409 | Email занят (INV-3) | «Email уже используется» |
IDENTITY_CONFIRMATION_RATE_LIMITED | 429 | INV-S4 | Таймер повторной отправки |
IDENTITY_CONFIRMATION_INVALID/EXPIRED/EXHAUSTED | 400 | INV-S5/S3/S2 | Как в UC-IDN-001 |
401 UNAUTHORIZED | 401 | Нет JWT | Форма авторизации |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
ConfirmationCodeRequestedEvent | request-шаг (СМС на новый номер / письмо на новый email) | handler verifier |
UserContactChangedEvent | confirm-шаг (вид: Phone/Email; значения маскируются в логах) | in-proc аудит |
SessionRevokedEvent | Отзыв сессий при смене телефона (INV-S7) | in-proc |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
UserUpsertedKafkaEvent.v1 | pin.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) | тест конфликта |
| AC3 | TalentSync-пользователь получает 403 на оба request-а (INV-4) | тест менеджера застройщика |
| AC4 | Смена телефона отзывает остальные Active-сессии (INV-S7) | тест: вторая сессия получает 401 |
| AC5 | Гонка «confirm против параллельной регистрации того же номера» разрешается unique-индексом — ровно один победитель | конкурентный тест |
| AC6 | Смена email не трогает сессии и телефон | тест A1 |
| AC7 | Вход по СМС после смены работает только на новый номер | сквозной тест с UC-IDN-001 |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ProfileEntity
- API (1): GET /api/v1/profile/me — Получение своего профиля ЛК (getMyProfile)
- Use Cases (2): UC-PRF-005. Запрет редактирования профиля для менеджера застройщика (TalentSync), UC-PRF-006. Смена email/phone из ЛК (делегирование в identity + обновление реплики)
- index.md (2): Identity Service (Pin.Identity), Profile Service (Pin.Profile)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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-пользователей).