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

UC-IDN-007. Синхронизация компании и её пользователей из Talent Identity (system)

Назначение

Системный сценарий репликации: внешняя система talent.microservices.identity публикует события застройщиков (tenant-ов) и их пользователей; identity создаёт/обновляет CompanyEntity и UserEntity типа DeveloperManager, назначает дефолтную роль и переопубликует собственные события pin.identity.* для остальных контекстов PIN. Инициатор — система (фоновые consumers), пользовательского ввода нет. Связан с контекстом identity.

Metadata

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

Актор

ПолеЗначение
РольSystem (фоновые consumers, system-actor; не пользовательский контекст)
Permissions / scopepermissions не применяются: m2m ACL Kafka на топики talent.identity.*; мутации — под system-actor (actor-stamping CreatedByUserId = system)
Tenantconsumer работает кросс-tenant: создаёт сами tenant-ы (CompanyId = TenantId реплики) — ADR-0052
Record-levelне применяется

Предусловие

  • Kafka доступна; consumer-группы identity подписаны на talent.identity.tenants.v1 и talent.identity.users.v1 (compacted-топики).
  • SEED применён (системная роль developer-manager существует).
  • Для событий пользователя: компания уже реплицирована (иначе retry — R4).
Допущение по именам топиков

Имена топиков talent.identity.tenants.v1 / talent.identity.users.v1 — зафиксированное assumption до подтверждения schema registry Talent.

Триггер

ПолеЗначение
ИсточникKafka event
Триггерящий элементСобытия Talent в топиках talent.identity.tenants.v1 / talent.identity.users.v1 (upsert-события и статус-события tenant/пользователя)

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

  1. Talent публикует TenantUpsertedEvent (snapshot: Id, Name, Type, Status, LogoFileId, Requisites, Version) в talent.identity.tenants.v1.
  2. Обработка события компании:
    • Идемпотентность. Повтор события по EventId подтверждается без эффекта (R1).
    • Проверка. Смаппить Type Talent → CompanyType; неизвестное значение enum → DLQ + алерт (A3). Version-guard R2: применять только если payload.Version > company.ReplicaVersion, иначе skip + метрика talent_sync_stale_events (A1).
    • Правила. Компании нет → создать CompanyEntity (Id = payload.Id, Type, SyncSource = TalentIdentity, Status: Pending → Active после первого snapshot, Slug из Name с разрешением коллизий). Компания есть → применить snapshot: перезаписать ТОЛЬКО синк-владеемые поля Name, LogoFileId, Requisites (INV-5; локальные Slug, Description, WorkspaceSettings неприкосновенны — R5); ReplicaVersion = payload.Version, LastSyncedAt = occurredAt.
    • Состояние синка. Обновить TalentSyncState (AggregateType = Tenant, ExternalId, LocalId, LastAppliedVersion, LastEventId, LastEventAt, Status = Synced).
    • Публикация. Переопубликовать CompanyUpsertedKafkaEventpin.identity.companies.v1 (downstream-контексты PIN не подписываются на топики Talent — service-boundaries). Компания, TalentSyncState и публикация — в одном согласованном изменении (R8); затем ack offset-а.
  3. Talent публикует UserUpsertedEvent (snapshot: Id, TenantId, FullName, Phone, Email, Status, Version) в talent.identity.users.v1.
  4. Обработка события пользователя:
    • Идемпотентность. Повтор по EventId — ack без эффекта.
    • Проверка. Найти компанию по ExternalTalentTenantId = payload.TenantId; отсутствие → retriable IDENTITY_SYNC_COMPANY_NOT_FOUND (retry с exponential backoff → DLQ; событие не теряется — R4). Version-guard R2 по user.ReplicaVersion; нормализовать телефон E.164.
    • Правила. Пользователя нет → создать UserEntity (Id = payload.Id, Type = DeveloperManager, CompanyId = company.Id, Status = Invited, ContactsManagedBy = TalentSync, RegisteredVia = TalentSync, SyncSource = TalentIdentity) + дефолтное RoleAssignment (developer-manager, Organization(CompanyId)) (сужение до Project — вручную, UC-IDN-005). Пользователь есть → применить snapshot: перезаписать Name, PhoneNumber, Email (EmailConfirmedAt = occurredAt); секреты (PasswordHash/PinCodeHash/2FA) НЕ реплицируются.
    • Публикация. Переопубликовать UserUpsertedKafkaEventpin.identity.users.v1 (+ UserAccessChangedKafkaEventpin.identity.access.v1 при создании дефолтного назначения); обновить TalentSyncState. Всё — в одном согласованном изменении (R8); затем ack.
  5. Менеджер застройщика входит в PIN обычным СМС-flow по реплицированному номеру (UC-IDN-001); первый вход переводит Invited → Active.
  6. Статус-события: TenantBlockedEvent → компания Suspend(TalentBlocked); TenantUnblockedEventResume(); TenantDeletedEventArchive() (soft, R7). UserBlockedEvent → пользователь Block() + отзыв всех Active RefreshSession (INV-S7); UserDeletedEventArchive() + отзыв назначений.
  7. Каждый статус-переход переопубликуется: CompanySuspendedKafkaEvent / UserBlockedKafkaEvent.
  8. Мониторинг: talent_sync_lag_seconds (now − max LastEventAt), алерт при лаге > 5 мин / DLQ > 0.

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

A1. Событие с устаревшей версией (out-of-order)

  1. payload.Version <= ReplicaVersion → идемпотентный skip, метрика talent_sync_stale_events_total, ack. Состояние не меняется (INV-6 company / INV-10 user).

A2. Конфликт телефона (R6)

  1. Реплицируемый номер уже занят PIN-пользователем (например, инвестором) — unique index ux_users_phone_number ловит вставку.
  2. Событие → DLQ; TalentSyncState.Status = Conflict, LastError = IDENTITY_SYNC_PHONE_CONFLICT (без PII); тикет заботы. Авто-merge запрещён (риск отдать чужой аккаунт).
  3. Resolution — вручную через admin API (identity.users.manage, platform-scope): merge/смена номера → replay события из DLQ → Status = Synced.

A3. Неизвестный enum (Type/Status Talent)

  1. Маппинг не находит значения → событие в DLQ + алерт (unknown enum handling, compatibility-требование). Синк остальных агрегатов продолжается.

A4. Начальная загрузка (bootstrap, R9)

  1. Первичное наполнение: чтение compacted-топиков с offset 0 либо разовый импорт по API Talent миграционным job-ом (identity/jobs/talent-initial-import.md); дальше — только события.

A5. Блокировка менеджера в Talent

  1. UserBlockedEvent → пользователь Block() (Active → Blocked), батч-отзыв RefreshSession (INV-S7), переопубликация UserBlockedKafkaEvent. Следующий запрос менеджера с access-JWT отклоняется после истечения короткого TTL токена; refresh невозможен.

Постусловие

  • CompanyEntity (SyncSource = TalentIdentity, Id = TenantId Talent) актуальна snapshot-у Talent; локальные поля PIN нетронуты.
  • UserEntity (Type = DeveloperManager, ContactsManagedBy = TalentSync) актуальна; дефолтное назначение developer-manager/Organization существует.
  • TalentSyncState содержит LastAppliedVersion/LastEventAt (мониторинг лага без чтения агрегатов).
  • Downstream-контексты получили pin.identity.companies.v1 / pin.identity.users.v1 / access.v1.

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

КодHTTPУсловиеПоведение клиента
IDENTITY_SYNC_COMPANY_NOT_FOUND— (retriable)Событие пользователя раньше события компании (R4)consumer: retry с backoff → DLQ + алерт
IDENTITY_SYNC_PHONE_CONFLICT— (DLQ)Unique-конфликт телефона (R6)DLQ + TalentSyncState.Conflict + тикет заботы
IDENTITY_SYNC_UNKNOWN_ENUM— (DLQ)Неизвестный Type/Status Talent (A3)DLQ + алерт; исправляется маппингом в релизе
IDENTITY_COMPANY_INVALID_STATUS_TRANSITION— (DLQ)Статус-событие в недопустимом состоянии (INV-8 company)DLQ; ручной разбор runbook-ом
Без HTTP

HTTP-кодов нет — сценарий полностью событийный; «клиент» — retry/DLQ-политика consumer-а.

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

СобытиеКогда поднимаетсяSubscribers
CompanyCreatedEvent / CompanyStatusChangedEventСоздание/переходы статуса репликиin-proc
UserRegisteredEventСоздание DeveloperManagerin-proc
RoleAssignmentGrantedEventДефолтное назначение при созданииin-proc аудит
SessionRevokedEventБлокировка менеджера (A5, INV-S7)in-proc

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

СобытиеTopicКогда публикуетсяConsumers
CompanyUpsertedKafkaEvent.v1pin.identity.companies.v1Каждый применённый tenant-snapshotcatalog, crm, rewards, news
CompanySuspendedKafkaEvent.v1pin.identity.companies.v1TenantBlockedEvent (причина TalentBlocked)те же
UserUpsertedKafkaEvent.v1pin.identity.users.v1Каждый применённый user-snapshotreferral, crm, profile, messenger, user-activity, notifications
UserBlockedKafkaEvent.v1pin.identity.users.v1UserBlockedEvent Talentте же
UserAccessChangedKafkaEvent.v1pin.identity.access.v1Дефолтное назначение / отзыв при архивеcrm, catalog, chessboard, gateway
Контракт с Talent и downstream

От Talent ожидается снапшот-семантика с монотонной Version на агрегат и стабильными идентификаторами (Id tenant/user) — на них опираются идемпотентность (R1) и version-guard (R2); при неизвестном enum или конфликте — событие в DLQ, а не молчаливая потеря. Downstream-контекстам синк предоставляет переопубликованные события pin.identity.* с ключом партиционирования UserId (user/access) и CompanyId (companies) и переносит Version из снапшота Talent — потребители применяют их идемпотентно и сохраняют относительный порядок изменений на агрегат, не подписываясь на топики Talent напрямую (service-boundaries). Гарантия «не откатывать более свежее состояние» переносится по цепочке: version-guard identity (R2) + version-guard потребителя; окно рассинхронизации downstream видно через talent_sync_lag_seconds плюс лаг собственной шины.

Acceptance Criteria

#КритерийПроверка
AC1Повторная доставка события не меняет состояние (R1+R2)тест двойной доставки
AC2Событие пользователя до компании → retry, после появления компании применяется (R4)тест порядка
AC3Синк не перетирает локальные поля (Slug, WorkspaceSettings, секреты) — R5тест: локальное изменение + snapshot
AC4Конфликт телефона → DLQ + Conflict, авто-merge отсутствует (R6)тест конфликта с инвестором
AC5Каждое применение переопубликует pin.identity.* в том же согласованном изменении (R8)тест публикации
AC6Stale-версия скипается без мутаций (A1)тест out-of-order
AC7Блокировка в Talent отзывает сессии PIN (A5)тест: активная сессия менеджера → refresh 401
AC8Лаг синка виден метрикой talent_sync_lag_seconds; алерт при > 5 минпроверка метрик

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

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

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

  • Consumers детально: identity/consumers/talent-tenant-upserted.md, talent-user-upserted.md, talent-tenant-status.md, talent-user-status.md.
  • Domain entity: Реплика Talent Identity (маппинги, R1–R9, TalentSyncState), Entity CompanyEntity (INV-5/6), Entity UserEntity (INV-10).
  • Bootstrap-job: identity/jobs/talent-initial-import.md (R9).
  • Связанные UC: UC-IDN-001 (вход менеджера), UC-IDN-004 A3 (запрет self-service контактов).