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

Event Contract: pin.identity.user-upserted (UserUpsertedKafkaEvent)

Версионируемый контракт интеграционного события «пользователь зарегистрирован/обновлён». Consumer-документы ссылаются сюда, не переописывая payload. Покрывает оба минимальных события — user-registered и user-updated: регистрация — это первое событие агрегата (Version = 1; иммутабельные поля RegisteredVia и ReferrerUrl присутствуют в каждом событии, поэтому referral может выполнить привязку и на повторной доставке — first-seen идемпотентность по UserId).

Назначение

Фиксирует создание учётки UserEntity (СМС-регистрация, lazy-регистрация, синк Talent, admin API) и каждое изменение её публичных полей (Name, Type, CompanyId, Status, PhoneNumber). Публичное: даёт downstream-контекстам snapshot пользователя без синхронного запроса в identity — реферальная привязка при регистрации (ReferrerUrl — slug реферальной ссылки), лиды и lookup инвесторов в CRM, создание профиля, контакты мессенджера, связывание anonymous-сессии, каналы уведомлений.

Metadata

ПолеЗначение
Event typepin.identity.user-upserted
Topicpin.identity.users.v1 — общий с user-blocked; общий key (UserId) обеспечивает порядок per-user
Partitions / key6 (до 1 млн пользователей, ADR-0056) / key = UserId
ContractVersion1
Produceridentity: регистрация (по СМС, lazy-регистрация), обновление данных инвестора, подтверждение смены email/телефона, admin API, talent-identity-sync
Статусdraft
Атомарность публикации

Событие публикуется атомарно со сменой состояния агрегата: до фиксации изменения событие не отправляется, после — доставляется гарантированно (at-least-once).

Payload UserUpsertedKafkaEvent

Дом полей — entity: UserEntity (раздел «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
UserIdGuidДаUserEntity.IdImmutableКлюч агрегата (= Kafka key).
Typestring (enum UserType)ДаUserEntity.TypeAdditive only (новые значения)Investor / DeveloperManager / Agent / PartnerManager / PinEmployee.
CompanyIdGuid?НетUserEntity.CompanyIdAdditive onlyРодительская компания; null — инвестор (ADR-0052).
Statusstring (enum UserStatus)ДаUserEntity.StatusAdditive onlyInvited / Active / Blocked / Archived.
NameFullNameSnapshotДаUserEntity.NameAdditive onlyФИО (LastName может быть пустой строкой при lazy-регистрации).
PhoneNumberstringДаUserEntity.PhoneNumberAdditive onlyE.164. Только для доверенных consumer-ов (crm/referral/messenger — ACL топика); правило PII ниже.
RegisteredViastring (enum RegistrationSource)ДаUserEntity.RegisteredViaImmutableSmsSignUp / LazyRegistration / TalentSync / AdminApi. Иммутабельно с регистрации.
ReferrerUrlstring?НетUserEntity.ReferrerUrlImmutableSlug реферальной ссылки, зафиксированный при регистрации (≤ 64); null — регистрация без реферера. Правила привязки — контекст referral.
VersionlongДаUserEntity.VersionImmutable semanticsДоменная версия; монотонный guard у потребителей. Version = 1 ⇔ регистрация.

Вложенная модель FullNameSnapshot

ПолеТипОбязательностьОграниченияОписание
FirstNamestringДа≤ 128, min 1Имя.
LastNamestringДа (пустая строка допустима до заполнения)≤ 128Фамилия.
MiddleNamestring?Нет≤ 128Отчество.
Исключённые поля

Email, PasswordHash/PinCodeHash/IsTwoFactorEnabled, LockoutUntil в payload не входят: email потребителям не нужен (контакты — profile/messenger собственными данными), секреты не покидают домен (правило user.md).

Когда публикуется

УсловиеПереход состоянияПубликация
Регистрация: по СМС, lazy-регистрация из заявки, admin API, синхронизация из Talent[*] → Active (инвестор) / [*] → Invited (пользователь компании/синк)атомарно с созданием агрегата
Изменение публичных полей: обновление данных инвестора (ФИО), подтверждение смены телефона, смена Status (разблокировка, архивация, первый вход Invited → Active), снапшот синка Talentсоответствующий переход stateDiagram user.md или без переходата же
Полное удаление PII (admin purge, delete-user)Archived (анонимизированный snapshot — потребители перезаписывают свои реплики)та же

Переход Active → Blocked публикуется отдельным событием user-blocked (тот же топик), а не upsert-ом.

Потребители (consumers)

Сверено с service-boundaries.md и user.md («Потребители pin.identity.users.v1»).

Consumer-контекстЭффектИдемпотентностьЗадержка (SLA)
referralПри first-seen UserId и ReferrerUrl != null — создание ReferralRegistration/реферальной связи по правилам привязки (Имя пустое / регистрация < 4 ч / владелец-агент)first-seen по UserId + MessageId< 1 мин (СМС-нотификация рефереру)
crmРеплика пользователя для лидов/lookup инвесторов (ФИО+телефон, FTS-дубли фиксаций)MessageId + Version-guard< 1 мин
profileСоздание PersonalProfile при first-seen; обновление снапшота ФИОто же< 5 мин
messengerСправочник контактов (отображаемое имя участника чатов)то же< 5 мин
user-activityУчётка для связывания anonymous_id ↔ UserIdто же< 5 мин
notificationsДолгоживущая реплика контактов получателя (RecipientContactEntity: ФИО/телефон/статус) для адреса при enqueue без синхронного lookup; welcome-цепочка при регистрациито же< 5 мин

Порядок и согласованность

  • Полный snapshot публичных полей — замещение. Потребитель замещает публичные поля своей реплики целиком, а не мёржит дельты; отсутствие optional-поля (ReferrerUrl = null) означает «нет реферера». Иммутабельные RegisteredVia/ReferrerUrl присутствуют в каждом событии — referral выполняет привязку идемпотентно даже на повторной доставке (first-seen по UserId).
  • First-seen = создание. Upsert для неизвестного UserId создаёт реплику (referral/profile/ user-activity), для известного — обновляет; поведение детерминировано UserId.
  • Порядок per-user и разные топики. Ключ UserId в общем с user-blocked топике даёт строгий порядок upsert/blocked между собой (Version-guard). С фактом входа (user-signed-in, топик pin.identity.sign-ins.v1) порядок не гарантирован — вход и реплика согласуются eventual.
  • Анонимизация. Полное удаление PII (delete-user) публикуется как upsert с анонимизированным снапшотом и наибольшей Version — потребители перезаписывают реплики последним значением (монотонность гарантирует, что анонимизацию не затрёт старый снапшот).

Версионирование и совместимость

  • ContractVersion = 1; при несоответствии версии события consumer логирует warning и продолжает (без upcasting).
  • Additive-only: новые optional-поля с default — без bump. Breaking — ContractVersion = 2 + топик pin.identity.users.v2 + dual-publish + deadline миграции + sunset .v1.
  • Unknown enum (Type/Status/RegisteredVia) — fallback: сохранить строку как есть, warning; для Status вести себя как Blocked (fail closed).
  • Порядок per-user гарантирован key-ом; потребители обязаны держать Version-guard (события upsert и blocked одного топика упорядочены между собой).

Безопасность

ПолеКласс данныхПравило
UserId, Type, CompanyId, Status, RegisteredVia, VersionInternalinclude
Name.*PII (ФИО)include (нужен crm/messenger/notifications); в логах producer/consumer — маскирование
PhoneNumberPIIinclude только для доверенных consumer-ов — доступ к топику ограничен m2m ACL Kafka (referral/crm/profile/messenger/user-activity/notifications); в логах маскируется +7***1234
ReferrerUrlInternalinclude

Событие НЕ содержит secrets (PasswordHash/PinCodeHash), email, документов, платёжных данных.

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

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

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

  • Сущность: user.md; соседний контракт топика: user-blocked.md.
  • Источники: операции регистрации и обновления учётки (identity API), talent-identity-sync, delete-user.
  • Правила реферальной привязки — docs/06-services/referral/.