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

UC-PRF-006. Смена email/phone из ЛК (делегирование в identity + обновление реплики)

Назначение

Раздельная смена контактной информации (email, phone) из личного кабинета: сами операции выполняет identity (requestEmailChange/confirmEmailChange, requestPhoneChange/confirmPhoneChangeUC-IDN-004); роль контекста profile — отображение read-only реплики контактов и её актуализация консюмером identity-users-sync по событию pin.identity.users.v1. profile эти операции не дублирует (граница — ../domain/profile.md). Связан с контекстами profile и identity.

Metadata

ПолеЗначение
IDUC-PRF-006
Контекстprofile (реплика/consumer); операции — identity (см. docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор / Агент / Агентство / МенеджерПИН / СлужбаЗаботы (self-service, ContactsManagedBy = SelfService); Девелопер — запрещено (UC-PRF-005, UC-IDN-004 A3). Consumer-шаг — System
Permissions / scopeidentity-шаги: identity.security.manage-self; profile-отображение: profile.personal.view-self, profile.contacts.manage-self (право видеть кнопки «Изменить»)
Tenantself-операция, субъект = актор (ADR-0052); consumer — system-actor без tenant
Record-levelтолько собственная учётка/профиль (Id = UserId)

Предусловие

  • Пользователь аутентифицирован, Profile.Status = Active (INV-PRF-3).
  • ContactsManagedBy = SelfService (иначе A3 → UC-PRF-005).
  • Ограничения identity: новый контакт не занят (INV-1/INV-3 identity), rate-limit кодов не исчерпан.

Триггер

ПолеЗначение
ИсточникUI button; затем Kafka event (consumer-шаг)
Триггерящий элементЛК /profile/personal → блок «Контакты» → «Изменить телефон» / «Изменить email»; consumer identity-users-sync (Pin.Profile.Background)

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

  1. Пользователь в ЛК видит контакты — реплику из getMyProfile (ContactPhone, ContactEmail полей ProfileEntity; read-only, дом — ../domain/profile.md) — и нажимает «Изменить телефон» (email — аналогично, по отдельности).
  2. Фронт выполняет процедуру смены контакта в identity — полные алгоритмы (шаги request/confirm, отправка кода на НОВЫЙ контакт, отзыв сессий при смене телефона) документированы в UC-IDN-004 и здесь не дублируются:
    • POST /investors/request-phone-change { newPhoneNumber } → код на новый номер;
    • POST /investors/confirm-phone-change { newPhoneNumber, code } → смена номера учётки, отзыв прочих Active-сессий. Контекст profile в этих шагах не участвует.
  3. identity при подтверждении публикует UserUpsertedKafkaEvent (топик pin.identity.users.v1, снапшот учётки с новым PhoneNumber/Email и инкрементированным Version).
  4. Consumer identity-users-sync (Pin.Profile.Background, ../consumers/identity-users-sync.md) обрабатывает событие (бизнес-проекция; полный алгоритм — в доке консюмера):
    • actor = system; бизнес-идемпотентность — по event.Version против ReplicaVersion.
    • payload доверенный (внутренний контур); структурная проверка контракта; unknown enum UserType → DLQ (правило из ../domain/profile.md).
    • профиль ищется по Id = event.UserId; отсутствие допускается (первое событие → создание профиля, ветка A1 доки консюмера).
    • INV-PRF-1 MonotonicReplica: событие применяется только при event.Version > profile.ReplicaVersion, иначе идемпотентный пропуск (out-of-order/повтор); маппинг реплика-полей: ContactPhone ← event.PhoneNumber, ContactEmail ← event.Email, Name ← event.Name, UserType/CompanyId/ContactsManagedBy/Status ← event; ReplicaVersion = event.Version; LastSyncedAt = now. Собственные поля profile (Locale, Timezone, VerificationStatus, DefaultRequisiteTemplateId) не затрагиваются — consumer и self-операции меняют непересекающиеся наборы полей.
    • собственных Kafka-событий profile не публикует (контакты наружу транслирует их владелец identity../domain/profile.md, раздел событий).
    • offset коммитится после успешного сохранения (at-least-once + идемпотентность по ReplicaVersion); ошибка обработки → retry, затем DLQ (alert «DLQ identity-sync > 0»).
  5. Фронт после успешного подтверждения инвалидирует getCurrentInvestor (identity) и getMyProfile (profile); ЛК показывает новый контакт. Если consumer ещё не догнал, фронт показывает значение из ответа identity — реплика profile используется для последующих заходов.
  6. Смена email — тот же поток с requestEmailChange/confirmEmailChange (UC-IDN-004, поток A1): код письмом на новый адрес; сессии не отзываются.

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

A1. Смена email

  1. Шаги 1–6 с парой requestEmailChange/confirmEmailChange (UC-IDN-004 A1): код на новый email, смена email учётки; сессии не отзываются. Consumer-шаг идентичен (ContactEmail ← event.Email).

A2. Событие пришло не по порядку / повторно

  1. event.Version <= profile.ReplicaVersion → идемпотентный пропуск без мутаций (INV-PRF-1); offset коммитится. Более новое событие уже применено — реплика не откатывается.

A3. Менеджер застройщика

  1. Кнопки скрыты (canEditContacts = false из getMyProfile); прямой вызов identity отклоняется IDENTITY_CONTACTS_MANAGED_EXTERNALLY (UC-IDN-004 A3, UC-PRF-005). Актуализация контактов — только синк Talent. Сценарий завершается.

A4. Ошибки identity-flow (занятый контакт, неверный код, rate-limit)

  1. IDENTITY_USER_PHONE_TAKEN / IDENTITY_USER_EMAIL_TAKEN / IDENTITY_CONFIRMATION_INVALID/ EXPIRED/EXHAUSTED / IDENTITY_CONFIRMATION_RATE_LIMITED — полностью на стороне identity (UC-IDN-004, A2/A4). Контакты и реплика не меняются, событие не публикуется. Возврат к шагу 1.

A5. Consumer упал / отстаёт

  1. Реплика в profile временно показывает старый контакт; фактический вход/СМС уже работают по новому (source of truth — identity). Retry → DLQ → replay по runbook (README profile).
  2. Ни одна операция profile не принимает решений по значению контакта — только отображение, поэтому рассинхронизация не нарушает инварианты. После догона — A2/шаг 4.

A6. Быстрая последовательная смена телефона и email

  1. Два confirm-шага в identity порождают два UserUpsertedKafkaEvent с растущим Version; порядок сохраняется key = UserId (одна партиция per user — событие каждой смены применяется в порядке публикации).
  2. Consumer применяет события по возрастанию Version (INV-PRF-1); финальная реплика отражает оба изменения. Переставленное/задвоенное доставкой событие с Version <= ReplicaVersion идемпотентно пропускается — откат к промежуточному состоянию исключён (A2).

Постусловие

  • В identity: PhoneNumber/Email учётки изменены, подтверждены (постусловия UC-IDN-004).
  • В profile: реплика ContactPhone/ContactEmail обновлена; ReplicaVersion = event.Version, LastSyncedAt заполнен; собственные поля ЛК не изменились.
  • ЛК отображает новый контакт; персоны/документы/шаблоны/верификация не затронуты (контакты учётки и контакты-оверрайды шаблонов — независимые данные, UC-PRF-004).

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

КодHTTPУсловиеПоведение клиента
IDENTITY_CONTACTS_MANAGED_EXTERNALLY403TalentSync (identity INV-4; A3)Скрыть кнопки, плашка
IDENTITY_USER_PHONE_TAKEN / IDENTITY_USER_EMAIL_TAKEN409Контакт занят (identity INV-1/INV-3)«Уже используется»
IDENTITY_CONFIRMATION_INVALID/EXPIRED/EXHAUSTED400Код неверен/истёк/исчерпанКак в UC-IDN-001/004
IDENTITY_CONFIRMATION_RATE_LIMITED429Rate-limit кодовТаймер повтора
PROFILE_NOT_FOUND404Реплика ещё не создана (первый вход)Retry (UC-PRF-001 A1)
401 UNAUTHORIZED401Нет JWTAuth-модалка

Ошибки consumer-шага (десериализация, unknown enum, сбой БД) — не HTTP: retry → DLQ + alert (наблюдаемость — README profile; runbook «replay DLQ identity-sync»).

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

СобытиеКогда поднимаетсяSubscribers
UserContactChangedEvent (identity)Confirm-шаг identity — дом UC-IDN-004identity in-proc аудит
ProfileCreatedEvent (profile)Только если событие оказалось первым для UserId (ветка создания реплики)in-proc

Собственных доменных событий на обновление реплика-полей profile не поднимает (техническая синхронизация; аудит — ReplicaVersion/LastSyncedAt).

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

СобытиеTopicКогда публикуетсяConsumers
Consumed: UserUpsertedKafkaEvent.v1pin.identity.users.v1Успешный confirm-шаг смены телефона/email в identityprofile (identity-users-sync), а также crm, referral, messenger, notifications
Producedprofile в этом сценарии ничего не публикует (владелец контактов — identity)

Acceptance Criteria

#КритерийПроверка
AC1В profile нет endpoint-ов смены email/phone; ЛК ведёт на операции identityревью API-карты profile (README)
AC2После confirm-шага identity реплика profile обновляется событием без ручных действийсквозной интеграционный тест
AC3Out-of-order/повторное событие не откатывает реплику (INV-PRF-1)тест consumer-а с перестановкой Version
AC4Consumer не затирает собственные поля profile (Locale/Timezone/VerificationStatus/DefaultRequisiteTemplateId)тест непересекающихся полей
AC5Email и телефон меняются независимо (по отдельности)два раздельных теста
AC6Для TalentSync-пользователя кнопки скрыты и identity возвращает 403тест A3
AC7Сбой consumer-а не блокирует identity-flow; после replay DLQ реплика догоняеттест A5 + runbook

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

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

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

  • Identity (дом операций): UC-IDN-004; 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; событие — ../../identity/events/user-upserted.md.
  • Consumer: ../consumers/identity-users-sync.md (полный алгоритм консюмера — дом; здесь — проекция на сценарий).
  • Domain entity: ../domain/profile.md (реплика-поля, INV-PRF-1, граница «identity владеет контактами»).
  • Смежные операции identity (та же граница): пароль/2FA/пин — UC-IDN-002, UC-IDN-003.
  • Связанные UC: UC-PRF-001 (отображение профиля), UC-PRF-005 (запрет для менеджера застройщика).