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

Реплика Talent Identity: правила синка + Entity TalentSyncState

Дом правил репликации данных из talent.microservices.identity в контекст identity PIN (Talent Identity — источник данных о застройщиках и их пользователях через Kafka-события; в PIN создаётся CompanyEntity на tenant/developer и запись пользователя на основе UserEntity Talent) + дом полей служебной сущности TalentSyncState (чекпоинты синка).

Назначение

Определяет: какие события Talent потребляются, как маппятся Talent TenantEntity → CompanyEntity и Talent UserEntity → UserEntity(PIN), какие поля владеет синк, как обеспечиваются идемпотентность, порядок и устойчивость к out-of-order, что происходит при блокировке/удалении на стороне Talent. Поля целевых сущностей НЕ переописываются — см. company.md и user.md; здесь только маппинг и правила.

Классификация (TalentSyncState)

ПолеЗначение
Контекстidentity
KindEntity (служебная, anemic)
Source of truthidentity (мета о синке); данные — talent.microservices.identity
Таблицаidentity.talent_sync_states
Tenant isolationне tenanted (платформенная служебная таблица)

Потребляемые события (consumers)

Контракты внешние — фиксируются как upstream source of truth (schema registry Talent). Идемпотентность на уровне consumer-а — по идентификатору события (EventId); бизнес-идемпотентность — version-guard (см. правила ниже).

Topic (Talent)СобытиеConsumer PINЭффект
talent.identity.tenants.v1TenantUpsertedEvent (snapshot tenant-а: Id, Name, Type, Status, Logo, Requisites, Version)TalentTenantUpsertedConsumerUpsert CompanyEntity (создание Pending → Active, обновление реплика-полей)
talent.identity.tenants.v1TenantBlockedEvent / TenantUnblockedEventTalentTenantStatusConsumerCompany.Suspend(TalentBlocked) / Resume()
talent.identity.tenants.v1TenantDeletedEventTalentTenantStatusConsumerCompany.Archive() (soft)
talent.identity.users.v1UserUpsertedEvent (snapshot: Id, TenantId, FullName, Phone, Email, Status, Version)TalentUserUpsertedConsumerUpsert UserEntity типа DeveloperManager
talent.identity.users.v1UserBlockedEvent / UserUnblockedEventTalentUserStatusConsumerUser.Block() / Unblock() + отзыв Active RefreshSession (INV-S7)
talent.identity.users.v1UserDeletedEventTalentUserStatusConsumerUser.Archive() (soft) + отзыв назначений ролей
Детализация

Каждый consumer документируется отдельным файлом identity/consumers/*.md; здесь — реестр и правила маппинга.

Маппинг Talent tenant → CompanyEntity

Поле Talent (TenantEntity)Поле PIN (CompanyEntity)Правило
IdId и ExternalTalentTenantIdId PIN = Id Talent (company.md, поле Id): прямая адресация без lookup.
Type (Developer — основной поток)TypeМаппинг enum-в-enum; неизвестный тип Talent → событие в DLQ + алерт (unknown enum handling). Только при создании (INV-4 ImmutableType).
NameNameПерезапись при каждом upsert (поле владеет синк — INV-5 SyncOwnedFields).
LogoFileIdLogoFileIdПерезапись.
Requisites {LegalName, Inn, Kpp, Ogrn, LegalAddress}Requisites (owned VO)Перезапись поэлементно.
Status (Active/Blocked/Deleted)StatusActive → Active; Blocked → Suspended; Deleted → Archived. Переходы — методами агрегата (guard-ы stateDiagram company.md).
VersionReplicaVersionМонотонный guard (см. правила).
SyncSourceКонстанта TalentIdentity при создании consumer-ом.
Slug, Description, WorkspaceSettingsЛокальные поля PIN, синк не трогает: Slug генерируется при создании из Name, дальше управляется PIN.
LastSyncedAt= envelope.OccurredAt применённого события.

Маппинг Talent user → UserEntity (PIN)

Поле Talent (UserEntity)Поле PIN (UserEntity)Правило
IdId и ExternalTalentUserIdId PIN = Id Talent.
TenantIdCompanyIdКомпания обязана уже существовать (порядок: consumer при отсутствии — retry с backoff, событие пользователя не теряется; см. правило R4).
FullName {FirstName, LastName, MiddleName}Name (owned FullName)Перезапись (владеет синк).
PhoneNumberPhoneNumberПерезапись после нормализации E.164; конфликт уникальности — правило R6.
EmailEmailПерезапись; EmailConfirmedAt = envelope.OccurredAt (Talent считается подтвердившим).
Status (Active/Blocked/Deleted)Statusпри создании — Invited (пользователь ещё не входил в PIN), перевод в Active при первом успешном входе; при обновлении Active → Active; Blocked → Blocked; Deleted → Archived.
VersionReplicaVersionМонотонный guard.
TypeКонстанта DeveloperManager (пользователи Talent — менеджеры застройщиков).
ContactsManagedByКонстанта TalentSync (self-service профиля запрещён).
RegisteredVia / SyncSourceTalentSync / TalentIdentity.
PasswordHash, PinCodeHash, IsTwoFactorEnabledНе реплицируются: секреты входа PIN локальны; менеджер входит в PIN тем же СМС-flow по реплицированному номеру.
РолиСинк НЕ назначает ролей автоматически, кроме первичного RoleAssignment(role = developer-manager, scope = Organization(CompanyId)) при создании пользователя (дефолт; сужение до Project — вручную руководителем).

Правила синка

#ПравилоРеализация
R1 ИдемпотентностьПовторная доставка события не меняет состояниеидемпотентность по идентификатору события (EventId) + бизнес-guard R2
R2 Монотонность версийСобытие применяется только если payload.Version > entity.ReplicaVersion; иначе — skip + метрика talent_sync_stale_eventsшаг валидации consumer-а до мутации (INV-6 company / INV-10 user)
R3 Snapshot-семантикаUpsert-события несут полный snapshot; consumer перезаписывает ВСЕ синк-владеемые поля, не диффыметод агрегата ApplyTalentSnapshot(snapshot, version, occurredAt)
R4 Порядок tenant→userUserUpsertedEvent при отсутствующей компании → retriable-ошибка (retry с exponential backoff, затем DLQ + алерт); компания появится своим событиемесли компании ещё нет → ошибка IDENTITY_SYNC_COMPANY_NOT_FOUND → retry policy
R5 Локальные поля неприкосновенныСинк никогда не пишет в поля с владельцем PIN (Slug, WorkspaceSettings, секреты, роли кроме дефолтной)раздельные методы агрегата (ApplyTalentSnapshot vs UpdateLocal) — конфликт невозможен по построению
R6 Конфликт телефонаTalent-номер уже занят PIN-пользователем (инвестором): событие в DLQ + тикет заботы (ручной merge учёток; авто-merge запрещён — риск отдать чужой аккаунт)уникальный индекс → IDENTITY_SYNC_PHONE_CONFLICT
R7 Удаление = архив*DeletedEvent → soft-delete (Archived); физическое удаление PII — только регламент администратора платформы (ADR-0053)методы Archive()
R8 ТранзакционностьМутация сущности + TalentSyncState + собственные исходящие события PIN (pin.identity.*) — в одном согласованном изменении (outbox)consumer
R9 Начальная загрузкаBootstrap — чтение топиков с offset 0 (compacted) либо разовый импорт по API Talent миграционным job-ом; дальнейшее — только события../background/talent-initial-import.md

Поля TalentSyncState

Чекпоинт «одна строка на внешний агрегат»: диагностика отставания, ручной replay, DLQ-расследования.

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор записи.
AggregateTypeTalentAggregateTypeДаvarchar(32): Tenant / User; часть uniqueconsumerТип внешнего агрегата.
ExternalIdGuidДаunique ux_talent_sync_states_type_external (aggregate_type, external_id)consumerId агрегата в Talent.
LocalIdGuidДаindex ix_talent_sync_states_local_idconsumerId созданной сущности PIN (= ExternalId по конвенции, хранится явно на случай смены конвенции).
LastAppliedVersionlongДа>= 0consumerПоследняя применённая версия Talent (дублирует ReplicaVersion сущности — денормализация для мониторинга без чтения агрегатов).
LastEventIdGuidДаenvelopeEventId последнего применённого события.
LastEventAtDateTimeOffsetДаindex ix_talent_sync_states_last_event_atenvelopeOccurredAt последнего события (лаг синка = now − max).
StatusTalentSyncRecordStatusДаvarchar(32): Synced / Conflict / DeadLetteredconsumerConflict — R6; DeadLettered — событие в DLQ, требует ручного действия.
LastErrorstring?Нетvarchar(1024)consumerТекст последней ошибки (без PII).
CreatedAt / UpdatedAtDateTimeOffsetДаconsumerШтампы.
Инварианты

Одна строка на (AggregateType, ExternalId) — unique-индекс; Status = Conflict/DeadLettered требует ручного resolution через admin-API (identity.users.manage платформенного scope) с переводом обратно в Synced. Диаграмма переходов тривиальна (Synced ↔ Conflict/DeadLettered), отдельно не приводится.

Производимые события PIN (re-publish)

Синк не является «тихим»: каждое применённое изменение публикует собственные события PIN (CompanyUpsertedKafkaEventpin.identity.companies.v1, UserUpsertedKafkaEvent/UserBlockedKafkaEventpin.identity.users.v1 — дома: company.md, user.md), чтобы downstream-контексты PIN не подписывались на топики Talent напрямую (правило границ service-boundaries.md).

Входной контракт (что синк ожидает от Talent). Строгий порядок per-key (TenantId/UserId) в партиции топика; полный snapshot в upsert-событиях (R3); монотонно растущий Version на агрегат (R2). Нарушения обрабатываются правилами: неизвестный Type/Status → DLQ (R2/маппинг), не растущий Version → idem-skip, смена Type компании → DLQ (INV-4). Кросс-агрегатный порядок tenant→user не гарантируется источником — компенсируется R4 (retry + backoff).

Исходящий контракт (что downstream может предполагать). Для каждого re-publish-события PIN: key = Id целевой сущности (порядок per-aggregate), ContractVersion = 1, монотонная доменная Version PIN-агрегата (не ReplicaVersion Talent), доставка at-least-once после фиксации изменения (outbox, R8). Так как компания применяется раньше своего пользователя (R4), downstream, зависящий от компании, получает pin.identity.companies.v1 не позже связанного pin.identity.users.v1. При изменении эффективных прав (создание дефолтного назначения, отзыв при архиве) дополнительно уходит UserAccessChangedKafkaEvent (pin.identity.access.v1, key = UserId).

Наблюдаемость, безопасность

АспектЗначение
Метрикиtalent_sync_lag_seconds (now − LastEventAt по типам), talent_sync_stale_events_total, talent_sync_dlq_total, talent_sync_conflicts_total
Логипо событию: EventId, ExternalId, Version, действие (created/updated/skipped/conflict); PII (телефон/ФИО) — маскируется
ТрейсингCorrelationId из envelope пробрасывается в исходящие pin-события
Алертылаг > 5 мин; DLQ > 0; Conflict > 0
Runbookreplay DLQ: исправить данные (merge учёток при R6) → повторная подача события; полная пересинхронизация — R9 job
Безопасностьconsumers работают под system-actor (не пользовательский контекст); секреты входа не реплицируются; topic-доступ — m2m ACL Kafka

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

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

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

  • company.md, user.md — целевые дома полей (INV-5/6, INV-4/10).
  • Consumers детально: talent-identity-sync.md.
  • ADR-0052, ADR-0053; service-boundaries.md («Компании/пользователи Talent → identity»).
  • Точные контракты топиков Talent — schema registry talent.microservices.identity (upstream source of truth).
Допущение

Имена топиков talent.identity.tenants.v1/talent.identity.users.v1 — зафиксированное предположение, подтверждается интеграционным контрактом при реализации.