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 type | pin.identity.user-upserted |
| Topic | pin.identity.users.v1 — общий с user-blocked; общий key (UserId) обеспечивает порядок per-user |
| Partitions / key | 6 (до 1 млн пользователей, ADR-0056) / key = UserId |
| ContractVersion | 1 |
| Producer | identity: регистрация (по СМС, lazy-регистрация), обновление данных инвестора, подтверждение смены email/телефона, admin API, talent-identity-sync |
| Статус | draft |
Событие публикуется атомарно со сменой состояния агрегата: до фиксации изменения событие не отправляется, после — доставляется гарантированно (at-least-once).
Payload UserUpsertedKafkaEvent
Дом полей — entity: UserEntity (раздел «Доменные и интеграционные события»);
здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
UserId | Guid | Да | UserEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
Type | string (enum UserType) | Да | UserEntity.Type | Additive only (новые значения) | Investor / DeveloperManager / Agent / PartnerManager / PinEmployee. |
CompanyId | Guid? | Нет | UserEntity.CompanyId | Additive only | Родительская компания; null — инвестор (ADR-0052). |
Status | string (enum UserStatus) | Да | UserEntity.Status | Additive only | Invited / Active / Blocked / Archived. |
Name | FullNameSnapshot | Да | UserEntity.Name | Additive only | ФИО (LastName может быть пустой строкой при lazy-регистрации). |
PhoneNumber | string | Да | UserEntity.PhoneNumber | Additive only | E.164. Только для доверенных consumer-ов (crm/referral/messenger — ACL топика); правило PII ниже. |
RegisteredVia | string (enum RegistrationSource) | Да | UserEntity.RegisteredVia | Immutable | SmsSignUp / LazyRegistration / TalentSync / AdminApi. Иммутабельно с регистрации. |
ReferrerUrl | string? | Нет | UserEntity.ReferrerUrl | Immutable | Slug реферальной ссылки, зафиксированный при регистрации (≤ 64); null — регистрация без реферера. Правила привязки — контекст referral. |
Version | long | Да | UserEntity.Version | Immutable semantics | Доменная версия; монотонный guard у потребителей. Version = 1 ⇔ регистрация. |
Вложенная модель FullNameSnapshot
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
FirstName | string | Да | ≤ 128, min 1 | Имя. |
LastName | string | Да (пустая строка допустима до заполнения) | ≤ 128 | Фамилия. |
MiddleName | string? | Нет | ≤ 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, Version | Internal | include |
Name.* | PII (ФИО) | include (нужен crm/messenger/notifications); в логах producer/consumer — маскирование |
PhoneNumber | PII | include только для доверенных consumer-ов — доступ к топику ограничен m2m ACL Kafka (referral/crm/profile/messenger/user-activity/notifications); в логах маскируется +7***1234 |
ReferrerUrl | Internal | include |
Событие НЕ содержит secrets (PasswordHash/PinCodeHash), email, документов, платёжных данных.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (9): Правила реферальной привязки (attach-rules) — дом доменных правил, Entity (Replica): MessengerUserEntity, Entity (Replica): UserDisplayReplicaEntity, Entity / Aggregate: AuthorReplica + CompanyReplica, Entity / Aggregate: ProfileEntity, Entity / Aggregate: RecipientContactEntity, Entity / Aggregate: ReferralRegistrationEntity, Entity: CallParticipantEntity (owned-child ConferenceSessionEntity), Entity: ParticipantEntity (owned-child ChatEntity)
- API (7): DELETE /api/admin/v1/users/{userId} — Удаление пользователя: архив vs полное удаление (admin), Identity Admin API — реестр методов и общие правила области, Identity Agent API — реестр методов и общие правила области, Identity Company-User API — реестр методов и общие правила области, POST /api/admin/v1/users/{userId}/block — Блокировка пользователя (admin), POST /api/v1/me/become-agent — Инвестор становится агентом, POST /api/v1/me/become-investor — Агент возвращается к роли инвестора
- Use Cases (3): UC-IDN-011. Инвестор становится агентом, UC-PRF-006. Смена email/phone из ЛК (делегирование в identity + обновление реплики), UC-REF-003. Регистрация по реферальной ссылке → связь + лид (правило R1)
- События (15): Событие «Запрошена карточка лида»: pin.referral.lead-attach-requested, События сервиса messenger: реестр топиков, События сервиса news: карта топиков и отклонённые события/консюмеры, События сервиса support: карта топиков и решения об интеграциях, Event Contract: pin.crm.booking-expired (BookingExpiredEvent), Event Contract: pin.crm.booking-paid (BookingPaidEvent), Event Contract: pin.crm.deal-completed (DealCompletedEvent), Event Contract: pin.crm.deal-members-changed (DealMembersChangedEvent), Event Contract: pin.crm.document-requested (DocumentRequestedEvent), Event Contract: pin.crm.lead-booking-intent-changed (LeadBookingIntentChangedEvent), Event Contract: pin.crm.lead-participants-changed (LeadParticipantsChangedEvent), Event Contract: pin.identity.agent-membership-changed (AgentMembershipChangedKafkaEvent) (+3)
- Консюмеры (11): Consumer: контроль смены телефона связанного инвестора (pin-investor-owner-identity-users), Consumer: Calendar Identity User Sync (calendar-identity-users), Consumer: CRM Identity User Sync (CrmUserUpsertedConsumer), Consumer: Identity Events → Notifications (notifications-identity-events), Consumer: identity-users-sync (ProfileUserUpsertedConsumer, ProfileUserBlockedConsumer), Consumer: identity.users.v1 → AuthorReplica (news-identity-users), Consumer: Messenger User Replica Sync (MessengerUserUpsertedConsumer), Consumer: ReferralUserUpsertedConsumer (pin.identity.users.v1), Consumer: Support Identity User Sync, Consumer: Talent Identity Sync (pin-identity-talent-sync), Consumer: Wallet Identity Display Sync (wallet-identity-users)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Identity)
- Поиск (2): MessengerContactSearchDocument, UserSearchDocument
- index.md (4): Calendar Service, Identity Service (Pin.Identity), Messenger Service, Wallet Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущность: user.md; соседний контракт топика: user-blocked.md.
- Источники: операции регистрации и обновления учётки (identity API), talent-identity-sync, delete-user.
- Правила реферальной привязки —
docs/06-services/referral/.