UC-IDN-007. Синхронизация компании и её пользователей из Talent Identity (system)
Назначение
Системный сценарий репликации: внешняя система talent.microservices.identity публикует события
застройщиков (tenant-ов) и их пользователей; identity создаёт/обновляет CompanyEntity и UserEntity
типа DeveloperManager, назначает дефолтную роль и переопубликует собственные события pin.identity.* для
остальных контекстов PIN. Инициатор — система (фоновые consumers), пользовательского ввода нет. Связан с
контекстом identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-007 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | System (фоновые consumers, system-actor; не пользовательский контекст) |
| Permissions / scope | permissions не применяются: m2m ACL Kafka на топики talent.identity.*; мутации — под system-actor (actor-stamping CreatedByUserId = system) |
| Tenant | consumer работает кросс-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/пользователя) |
Основной Поток
- Talent публикует
TenantUpsertedEvent(snapshot:Id,Name,Type,Status,LogoFileId,Requisites,Version) вtalent.identity.tenants.v1. - Обработка события компании:
- Идемпотентность. Повтор события по
EventIdподтверждается без эффекта (R1). - Проверка. Смаппить
TypeTalent →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). - Публикация. Переопубликовать
CompanyUpsertedKafkaEvent→pin.identity.companies.v1(downstream-контексты PIN не подписываются на топики Talent — service-boundaries). Компания,TalentSyncStateи публикация — в одном согласованном изменении (R8); затем ack offset-а.
- Идемпотентность. Повтор события по
- Talent публикует
UserUpsertedEvent(snapshot:Id,TenantId,FullName,Phone,Email,Status,Version) вtalent.identity.users.v1. - Обработка события пользователя:
- Идемпотентность. Повтор по
EventId— ack без эффекта. - Проверка. Найти компанию по
ExternalTalentTenantId = payload.TenantId; отсутствие → retriableIDENTITY_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) НЕ реплицируются. - Публикация. Переопубликовать
UserUpsertedKafkaEvent→pin.identity.users.v1(+UserAccessChangedKafkaEvent→pin.identity.access.v1при создании дефолтного назначения); обновитьTalentSyncState. Всё — в одном согласованном изменении (R8); затем ack.
- Идемпотентность. Повтор по
- Менеджер застройщика входит в PIN обычным СМС-flow по реплицированному номеру (UC-IDN-001);
первый вход переводит
Invited → Active. - Статус-события:
TenantBlockedEvent→ компанияSuspend(TalentBlocked);TenantUnblockedEvent→Resume();TenantDeletedEvent→Archive()(soft, R7).UserBlockedEvent→ пользовательBlock()+ отзыв всех ActiveRefreshSession(INV-S7);UserDeletedEvent→Archive()+ отзыв назначений. - Каждый статус-переход переопубликуется:
CompanySuspendedKafkaEvent/UserBlockedKafkaEvent. - Мониторинг:
talent_sync_lag_seconds(now − maxLastEventAt), алерт при лаге > 5 мин / DLQ > 0.
Альтернативные Потоки
A1. Событие с устаревшей версией (out-of-order)
payload.Version <= ReplicaVersion→ идемпотентный skip, метрикаtalent_sync_stale_events_total, ack. Состояние не меняется (INV-6 company / INV-10 user).
A2. Конфликт телефона (R6)
- Реплицируемый номер уже занят PIN-пользователем (например, инвестором) — unique index
ux_users_phone_numberловит вставку. - Событие → DLQ;
TalentSyncState.Status = Conflict,LastError = IDENTITY_SYNC_PHONE_CONFLICT(без PII); тикет заботы. Авто-merge запрещён (риск отдать чужой аккаунт). - Resolution — вручную через admin API (
identity.users.manage, platform-scope): merge/смена номера → replay события из DLQ →Status = Synced.
A3. Неизвестный enum (Type/Status Talent)
- Маппинг не находит значения → событие в DLQ + алерт (unknown enum handling, compatibility-требование). Синк остальных агрегатов продолжается.
A4. Начальная загрузка (bootstrap, R9)
- Первичное наполнение: чтение compacted-топиков с offset 0 либо разовый импорт по API Talent
миграционным job-ом (
identity/jobs/talent-initial-import.md); дальше — только события.
A5. Блокировка менеджера в Talent
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-кодов нет — сценарий полностью событийный; «клиент» — retry/DLQ-политика consumer-а.
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
CompanyCreatedEvent / CompanyStatusChangedEvent | Создание/переходы статуса реплики | in-proc |
UserRegisteredEvent | Создание DeveloperManager | in-proc |
RoleAssignmentGrantedEvent | Дефолтное назначение при создании | in-proc аудит |
SessionRevokedEvent | Блокировка менеджера (A5, INV-S7) | in-proc |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
CompanyUpsertedKafkaEvent.v1 | pin.identity.companies.v1 | Каждый применённый tenant-snapshot | catalog, crm, rewards, news |
CompanySuspendedKafkaEvent.v1 | pin.identity.companies.v1 | TenantBlockedEvent (причина TalentBlocked) | те же |
UserUpsertedKafkaEvent.v1 | pin.identity.users.v1 | Каждый применённый user-snapshot | referral, crm, profile, messenger, user-activity, notifications |
UserBlockedKafkaEvent.v1 | pin.identity.users.v1 | UserBlockedEvent Talent | те же |
UserAccessChangedKafkaEvent.v1 | pin.identity.access.v1 | Дефолтное назначение / отзыв при архиве | crm, catalog, chessboard, gateway |
От 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) | тест публикации |
| AC6 | Stale-версия скипается без мутаций (A1) | тест out-of-order |
| AC7 | Блокировка в Talent отзывает сессии PIN (A5) | тест: активная сессия менеджера → refresh 401 |
| AC8 | Лаг синка виден метрикой talent_sync_lag_seconds; алерт при > 5 мин | проверка метрик |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-CHS-005. Синхронизация реплики из Talent Chessboard (system), UC-PRF-005. Запрет редактирования профиля для менеджера застройщика (TalentSync)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Identity)
- index.md (1): Identity Service (Pin.Identity)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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), EntityCompanyEntity(INV-5/6), EntityUserEntity(INV-10). - Bootstrap-job:
identity/jobs/talent-initial-import.md(R9). - Связанные UC: UC-IDN-001 (вход менеджера), UC-IDN-004 A3 (запрет self-service контактов).