Consumer: Talent Identity Sync (pin-identity-talent-sync)
Консолидированный документ Kafka-синка из talent.microservices.identity в контекст identity PIN — один
consumer group обрабатывает четыре потока событий (tenant-upsert, tenant-status, user-upsert, user-status).
Правила маппинга полей и владения полями — дом talent-replica.md; целевые сущности — company.md, user.md. Retry/DLQ/replay-политика группы описана здесь единожды на все четыре потока (см. разделы «Идемпотентность и replay» и «Retry и DLQ» ниже).
Назначение
Consumer group pin-identity-talent-sync потребляет события Talent Identity о tenant-ах
(компаниях-застройщиках) и их пользователях (менеджерах застройщиков) и поддерживает в PIN реплики:
CompanyEntity (SyncSource = TalentIdentity) и UserEntity типа DeveloperManager. Каждое применённое
изменение re-publish-ится собственными событиями PIN (pin.identity.companies.v1, pin.identity.users.v1,
pin.identity.access.v1), чтобы downstream-контексты PIN не подписывались на топики Talent напрямую (правило
границ — service-boundaries.md).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| Consumer group | pin-identity-talent-sync |
| Producer | talent.microservices.identity (внешняя система; контракты — upstream source of truth, schema registry Talent; имена топиков — зафиксированное assumption README identity) |
| Топик 1 | talent.identity.tenants.v1 — события TenantUpsertedEvent, TenantBlockedEvent, TenantUnblockedEvent, TenantDeletedEvent |
| Топик 2 | talent.identity.users.v1 — события UserUpsertedEvent, UserBlockedEvent, UserUnblockedEvent, UserDeletedEvent |
| ContractVersion | ожидается 1 для всех событий Talent; при несовпадении версии — warn-and-proceed (метрика talent_sync_contract_mismatch_total) |
| Порядок | key события = TenantId/UserId Talent → все события одного агрегата в одной партиции, строгий порядок per-aggregate; кросс-агрегатный порядок tenant→user НЕ гарантирован (обрабатывается правилом R4 talent-replica.md) |
Маппинг «событие → эффект» (бизнес-термины):
| Событие(я) | Эффект |
|---|---|
TenantUpsertedEvent | Создание/обновление CompanyEntity-реплики |
TenantBlockedEvent / TenantUnblockedEvent / TenantDeletedEvent | Company.Suspend(TalentBlocked) / Resume() / Archive() |
UserUpsertedEvent | Создание/обновление UserEntity (DeveloperManager) + дефолтный RoleAssignment при создании |
UserBlockedEvent / UserUnblockedEvent / UserDeletedEvent | User.Block()/Unblock()/Archive() + отзыв активных сессий/назначений |
Контекст события
- Из метаданных события доступны:
MessageId(Guid v7, для дедупликации),CreatedAt(=OccurredAtсобытия Talent),Version(= ContractVersion producer-а), идентификатор и тип агрегата-источника (TenantId/UserIdTalent,Tenant/User). - Актор — система синка (не пользовательский контекст): создатель/изменивший мутируемых сущностей штампуется
служебным идентификатором; синк пишет в platform-таблицу
companiesи кросс-tenant вusers(tenant-фильтр не применяется). - Корреляция сквозного трейса восстанавливается из метаданных и пробрасывается в исходящие
pin.identity.*. - Ключ идемпотентности:
talent-sync:{EventType}:{MessageId}; бизнес-идемпотентность — version-guard R2 (payload.Version > entity.ReplicaVersion, INV-6 company / INV-10 user).
Входные данные / payload событий
Контракты внешние (schema registry Talent — upstream source of truth); ниже — поля, которые consumers реально читают, и их маппинг. Полная таблица правил маппинга — дом talent-replica.md (разделы «Маппинг Talent tenant → CompanyEntity» и «Маппинг Talent user → UserEntity»); здесь — только связка «поле payload → шаг алгоритма».
TenantUpsertedEvent (snapshot tenant-а)
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
Id | Guid | Да | Ключ поиска компании по внешнему id Talent; при создании → CompanyEntity.Id и ExternalTalentTenantId (Id PIN = Id Talent) |
Type | string (enum Talent) | Да | Маппинг → CompanyType (Developer — основной поток); только при создании (INV-4 ImmutableType); неизвестное значение → DLQ |
Name | string | Да | Перезапись CompanyEntity.Name (синк-владеемое поле, INV-5) |
LogoFileId | Guid? | Нет | Перезапись CompanyEntity.LogoFileId |
Requisites.LegalName | string? | Нет | Перезапись Requisites.LegalName (owned VO CompanyRequisites) |
Requisites.Inn | string? | Нет | Перезапись Requisites.Inn |
Requisites.Kpp | string? | Нет | Перезапись Requisites.Kpp |
Requisites.Ogrn | string? | Нет | Перезапись Requisites.Ogrn |
Requisites.LegalAddress | string? | Нет | Перезапись Requisites.LegalAddress |
Status | string: Active/Blocked/Deleted | Да | Маппинг статуса: Active → Active, Blocked → Suspended, Deleted → Archived — методами агрегата с guard-ами переходов company.md |
Version | long | Да | Монотонный guard R2 → ReplicaVersion; денормализуется в TalentSyncState.LastAppliedVersion |
TenantBlockedEvent / TenantUnblockedEvent / TenantDeletedEvent
| Поле payload | Тип | Обязательность | Как используется |
|---|---|---|---|
Id | Guid | Да | поиск компании по внешнему id Talent |
Version | long | Да | Монотонный guard R2 |
UserUpsertedEvent (snapshot пользователя Talent)
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
Id | Guid | Да | поиск пользователя по внешнему id Talent; при создании → UserEntity.Id и ExternalTalentUserId |
TenantId | Guid | Да | резолв UserEntity.CompanyId по внешнему id tenant-а; компании нет → retriable-ошибка R4 |
FullName.FirstName | string | Да | Перезапись Name.FirstName (owned FullName) |
FullName.LastName | string | Да | Перезапись Name.LastName |
FullName.MiddleName | string? | Нет | Перезапись Name.MiddleName |
PhoneNumber | string | Да | Нормализация E.164 → перезапись PhoneNumber; конфликт уникальности → R6 (DLQ + Conflict) |
Email | string? | Нет | Перезапись Email; при непустом — EmailConfirmedAt = момент события (Talent считается подтвердившим) |
Status | string: Active/Blocked/Deleted | Да | При создании — Invited (в PIN ещё не входил); при обновлении: Active → Active, Blocked → Blocked, Deleted → Archived |
Version | long | Да | Монотонный guard R2 → ReplicaVersion |
UserBlockedEvent / UserUnblockedEvent / UserDeletedEvent
| Поле payload | Тип | Обязательность | Как используется |
|---|---|---|---|
Id | Guid | Да | поиск пользователя по внешнему id Talent |
Version | long | Да | Монотонный guard R2 |
Алгоритм
Отдельный проход для каждого из четырёх потоков событий.
A. Tenant upsert (TenantUpsertedEvent)
- Контекст события. Восстановить метаданные (
MessageId,CreatedAt,Id); сверитьVersionс ожидаемымContractVersion = 1— при несовпадении warn-and-proceed; идемпотентность по ключуtalent-sync:tenant-upserted:{MessageId}— дубль → логskipped-duplicate, ack. - Проверка входа.
Idзадан;Name— непустая, ≤ 256 (при превышении — truncate + warn, событие не теряется);Version >= 0; маппингType→CompanyType— неизвестное значение → отказIDENTITY_SYNC_UNKNOWN_COMPANY_TYPE(non-retriable → DLQ + алерт); маппингStatus→CompanyStatus— неизвестное значение →IDENTITY_SYNC_UNKNOWN_STATUS(та же DLQ-ветка). Существование сущности не проверяется — операция upsert, отсутствие допустимо. - Что читаем. Компания по внешнему id Talent (может отсутствовать — первое событие tenant-а); запись синк-статуса
TalentSyncStateэтого tenant-а (может отсутствовать). - Правила и переходы.
- Version-guard (R2/INV-6): если компания уже есть и
payload.Version <= ReplicaVersion— событие устаревшее/дубль: метрикаtalent_sync_stale_events_total, логskipped-stale, к шагу 7 (только отметка идемпотентности), ack. - Type-guard (INV-4): если компания есть и
map(payload.Type) ≠ Type— отказIDENTITY_SYNC_TYPE_CHANGED(non-retriable → DLQ,TalentSyncState.Status = DeadLettered): смена типа компании в Talent — аномалия, требует ручного разбора. - Создание (компании нет): создать
CompanyEntityиз снапшота Talent —Id = payload.Id,ExternalTalentTenantId = payload.Id,SyncSource = TalentIdentity,Status = Pending; сгенерироватьSlugизName(транслитерация + lower-kebab + разрешение коллизии суффиксом-2,-3… с проверкой уникальности, INV-1); дефолтныйWorkspaceSettingsпо типу компании; затем активировать (Pending → Active) — либо сразуSuspend(TalentBlocked)/Archive(), еслиpayload.Status=Blocked/Deleted. Поднимаются доменныеCompanyCreatedEventиCompanyStatusChangedEvent. - Обновление (компания есть): применить снапшот (R3 snapshot-семантика) — перезаписать ВСЕ синк-владеемые
поля:
Name,LogoFileId,Requisitesпоэлементно;ReplicaVersion = payload.Version;LastSyncedAt = момент события; локальные поля PIN (Slug,Description,WorkspaceSettings) не трогаются (R5, INV-5). Сверка статуса:map(payload.Status) ≠ Status→ переход методом агрегата (Suspend(TalentBlocked)/Resume()/Archive()); совпадает — no-op. - Маппинг поле-в-поле — по таблице «Маппинг Talent tenant → CompanyEntity» talent-replica.md.
- Version-guard (R2/INV-6): если компания уже есть и
- Что меняется. Компания (создание/обновление; concurrency синка — version-guard R2, а не версия строки). Запись
TalentSyncState: при отсутствии создаётся (AggregateType = Tenant,ExternalId = payload.Id,LocalId = company.Id), затемLastAppliedVersion = payload.Version,LastEventId = MessageId,LastEventAt = момент события,Status = Synced,LastError = null. - События.
CompanyUpsertedKafkaEvent→ топикpin.identity.companies.v1, key =company.Id,AggregateType = Company,ContractVersion = 1— полный snapshot (контракт); при переходеActive → SuspendedдополнительноCompanySuspendedKafkaEvent(Reason = TalentBlocked, контракт). - Согласованность. Компания +
TalentSyncState+ отметка идемпотентности + исходящие события — атомарно (R8). Offset подтверждается только после успешной фиксации. - Результат. Успех → ack. Retriable-код → retry по политике (таблица ниже); non-retriable → DLQ +
TalentSyncState.Status = DeadLettered+ алерт.
B. Tenant status (TenantBlocked/TenantUnblocked/TenantDeleted)
- Контекст. Метаданные соответствующего события; ключ идемпотентности
talent-sync:tenant-{blocked|unblocked|deleted}:{MessageId}; дубль → skip. - Проверка.
Idзадан,Version >= 0. Существование: компания по внешнему id Talent; отсутствует → отказIDENTITY_SYNC_COMPANY_NOT_FOUND— retriable (out-of-order:TenantUpsertedEventещё не применён; key-партиционирование Talent делает это маловероятным, но политика R4 действует). Допустимость состояния — guard-ы переходов в методах агрегата (шаг 4). - Что читаем. Компания (шаг 2) + её
TalentSyncState. - Правила и переходы. Version-guard R2 (устаревшее → skip). По типу события:
TenantBlockedEvent→Suspend(TalentBlocked)(guard INV-8: только изActive; ужеSuspended— идемпотентный no-op с обновлениемReplicaVersion);TenantUnblockedEvent→Resume()(Suspended → Active; изArchived— no-op + warn: Talent разблокировал архивированную в PIN компанию — метрикаtalent_sync_conflicts_total);TenantDeletedEvent→Archive()(R7: soft-delete,Status = Archived+DeletedAt, INV-7). Каждый переход поднимаетCompanyStatusChangedEvent. - Что меняется. Компания (обновление);
TalentSyncState(Version/EventId/EventAt/Synced — как в A5). - События.
TenantBlockedEvent→CompanySuspendedKafkaEvent(Reason = TalentBlocked);TenantUnblockedEvent/TenantDeletedEvent→CompanyUpsertedKafkaEventс актуальным snapshot (потребители видятStatus = Active/Archived). - Согласованность. Всё — атомарно.
- Результат. Как A8.
C. User upsert (UserUpsertedEvent)
- Контекст. Метаданные события; ключ идемпотентности
talent-sync:user-upserted:{MessageId}; дубль → skip. - Проверка входа.
Idзадан;TenantIdзадан;FullName.FirstNameнепустое (≤ 128, truncate + warn);PhoneNumberнепустой;Version >= 0;Emailпри непустом — формат email (невалидный → warn +Email = null, событие не теряется);- нормализация
PhoneNumberв E.164 — невалидный номер → отказIDENTITY_SYNC_INVALID_PHONE(non-retriable → DLQ: без телефона менеджер не сможет войти СМС-flow); - существование компании (R4): компания по внешнему id tenant-а; отсутствует → отказ
IDENTITY_SYNC_COMPANY_NOT_FOUND— retriable (exponential backoff → DLQ); компания появится своимTenantUpsertedEvent, событие пользователя не теряется; - допустимость состояния компании:
Status = Archived→ warn + продолжение (реплика пользователя создаётся; вход всё равно заблокирован INV-9 user.md).
- Что читаем. Пользователь по внешнему id Talent (может отсутствовать); его
TalentSyncState; при создании дополнительно — владелец того же телефона среди активных пользователей PIN (проверка конфликта R6); для дефолтного назначения — системная SEED-рольdeveloper-manager(CompanyId = null; отсутствие — фатальная ошибка конфигурации → retriable fail + алерт). - Правила и переходы.
- Version-guard R2/INV-10:
payload.Version <= ReplicaVersion→ skip-stale (метрика, отметка идемпотентности, ack). - Конфликт телефона (R6): при создании, если телефон занят другим PIN-пользователем (обычно инвестором) —
отказ
IDENTITY_SYNC_PHONE_CONFLICT(non-retriable):TalentSyncState.Status = Conflict,LastError(без PII), событие в DLQ, алерт → тикет заботы, ручной merge учёток; авто-merge запрещён (риск отдать чужой аккаунт). При обновлении с изменившимся номером — та же проверка перед перезаписью. - Создание (пользователя нет): создать
UserEntityиз снапшота —Type = DeveloperManager,Status = Invited(вActiveпереведёт первый успешный вход),ContactsManagedBy = TalentSync(self-service запрещён),RegisteredVia = TalentSync,SyncSource = TalentIdentity,ExternalTalentUserId = payload.Id; секреты (PasswordHash/PinCodeHash/2FA) НЕ реплицируются — вход в PIN тем же СМС-flow по реплицированному номеру. Еслиpayload.Status = Blocked— сразуBlock();Deleted—Archive(). ПоднимаетсяUserRegisteredEvent. Дефолтное назначение роли:RoleAssignment(role = developer-manager, scope = Organization(CompanyId), grantedBy = система)— единственная роль, которую назначает синк (сужение доProject— вручную руководителем); инварианты INV-R2/R4/R6 проверяются при создании. ПоднимаетсяRoleAssignmentGrantedEvent. - Обновление (пользователь есть): применить снапшот — перезапись
Name(ownedFullName),PhoneNumber(E.164),Email(+EmailConfirmedAtпри непустом);ReplicaVersion,LastSyncedAt; локальные поля (секреты, роли,LastSignInAt) не трогаются (R5). Сверка статуса:Blocked→Block()+ отзыв активныхRefreshSession(INV-S7, шаг 5);ActiveприStatus = Blocked→Unblock();Deleted→Archive()(+ отзыв назначений, шаг 5).Invited/ActivePIN-статусы приpayload.Status = Activeне меняются (переходInvited → Active— только первым входом).
- Version-guard R2/INV-10:
- Что меняется. Пользователь (создание/обновление) + при создании — дефолтный
RoleAssignment. При блокировке — отзыв всех активныхRefreshSessionпользователя (Status = Revoked, причинаAdminBlock); при архиве — тот же отзыв сессий + отзыв всех активныхRoleAssignment.TalentSyncState— как A5. - События. Всегда —
UserUpsertedKafkaEvent→pin.identity.users.v1, key =user.Id(контракт); при создании назначения —UserAccessChangedKafkaEvent→pin.identity.access.v1(контракт); приBlock()—UserBlockedKafkaEvent(Reason = TalentBlocked, контракт); приArchive()—UserUpsertedKafkaEventсоStatus = Archived+UserAccessChangedKafkaEvent(назначения отозваны). - Согласованность. Пользователь + назначение + сессии +
TalentSyncState+ отметка идемпотентности + исходящие события — атомарно. - Результат. Как A8.
D. User status (UserBlocked/UserUnblocked/UserDeleted)
- Контекст. Ключ идемпотентности
talent-sync:user-{blocked|unblocked|deleted}:{MessageId}. - Проверка.
Idзадан,Version >= 0; пользователь по внешнему id Talent; отсутствует → отказIDENTITY_SYNC_USER_NOT_FOUND— retriable (out-of-order противUserUpsertedEvent). - Что читаем. Пользователь (шаг 2) + его
TalentSyncState. - Правила и переходы. Version-guard R2.
UserBlockedEvent→Block()(guard user.md:Active|Invited → Blocked; ужеBlocked— no-op + version bump);UserUnblockedEvent→Unblock()(Blocked → Active;Archived— no-op + warn +talent_sync_conflicts_total);UserDeletedEvent→Archive()(R7 soft-delete). - Что меняется. Пользователь (обновление); при Block/Archive — отзыв активных
RefreshSession(INV-S7); при Archive — отзыв активныхRoleAssignment;TalentSyncState. - События. Block →
UserBlockedKafkaEvent(pin.identity.users.v1); Unblock →UserUpsertedKafkaEvent(snapshotStatus = Active); Archive →UserUpsertedKafkaEvent(Status = Archived) +UserAccessChangedKafkaEvent(pin.identity.access.v1, отзыв назначений). Доменные —SessionRevokedEventна каждый отзыв,RoleAssignmentRevokedEvent. - Согласованность. Всё — атомарно.
- Результат. Как A8.
Диаграмма
Идемпотентность и replay
- По событию: дедупликация по
MessageId(ключиtalent-sync:{eventType}:{MessageId}) — повторная доставка after-commit-crash не меняет состояние. - По бизнес-правилу: version-guard R2 — событие с
Version <= ReplicaVersionпропускается (защита от out-of-order и от replay целого топика). - Replay: да. Полный re-read топиков с offset 0 безопасен (upsert + version-guard); runbook — «полная
пересинхронизация» talent-replica.md (R9, job
talent-initial-import). Точечный replay DLQ — после ручного resolution (merge учёток при R6) через операцию resolution конфликтов (переводTalentSyncState.Status → Synced- повторная подача события).
Retry и DLQ
DLQ-топик группы: pin.identity.talent-sync-dlq.v1 (исходное событие + заголовки x-dlq-reason,
x-dlq-error-code, x-source-topic, x-original-offset).
| Сбой | Код | Retry | DLQ | Ручное действие |
|---|---|---|---|---|
| Инфраструктурный (PostgreSQL/шина недоступны, timeout) | — | Бесконечный exponential backoff 1s→60s с паузой партиции (событие не теряется) | Нет | Runbook identity-overview: восстановить зависимость |
| Компания не найдена (out-of-order tenant→user, R4) | IDENTITY_SYNC_COMPANY_NOT_FOUND | 6 попыток, backoff 30s → 1m → 2m → 4m → 8m → 16m | Да, после исчерпания | Проверить, пришёл ли TenantUpsertedEvent; replay из DLQ |
| Пользователь не найден (status-событие раньше upsert) | IDENTITY_SYNC_USER_NOT_FOUND | та же политика | Да | То же |
Неизвестный enum (Type/Status Talent) | IDENTITY_SYNC_UNKNOWN_COMPANY_TYPE / IDENTITY_SYNC_UNKNOWN_STATUS | Нет (non-retriable) | Да + алерт | Обновить маппинг (релиз) → replay |
| Смена типа компании (INV-4) | IDENTITY_SYNC_TYPE_CHANGED | Нет | Да + алерт | Ручной разбор с Talent; решение по данным |
| Конфликт телефона (R6) | IDENTITY_SYNC_PHONE_CONFLICT | Нет | Да + TalentSyncState.Status = Conflict + тикет заботы | Ручной merge учёток → resolution → replay |
| Невалидный телефон | IDENTITY_SYNC_INVALID_PHONE | Нет | Да + алерт | Исправление данных в Talent → повторное событие |
| Конфликт версий при гонке с admin-операцией | — | 3 попытки немедленно (перечитать агрегат) | После исчерпания | Расследование гонки |
Наблюдаемость и безопасность
- Logs (structured, по событию):
consumed/skipped-duplicate/skipped-stale/created/updated/status-changed/conflict/dead-letteredc полямиEventId,ExternalId,payload.Version,entity.ReplicaVersion; PII (телефон/ФИО/email) — маскируется (+7***1234,d***@mail). - Metrics:
talent_sync_lag_seconds(now −TalentSyncState.LastEventAt, поAggregateType),talent_sync_consume_duration_ms,talent_sync_stale_events_total,talent_sync_retries_total,talent_sync_dlq_total,talent_sync_conflicts_total,talent_sync_contract_mismatch_total. - Traces: consume-span → span обработки → spans хранилища; correlation из метаданных пробрасывается в исходящие
pin.identity.*. - Alerts: лаг > 5 мин (SLO README identity); DLQ > 0; Conflict > 0; ошибки конфигурации (SEED-роль
developer-managerне найдена). - Dashboard / Runbook: identity-overview; runbooks «replay DLQ синка», «merge учёток (R6)», «полная пересинхронизация (R9)».
- Безопасность: consumers работают под system-actor (не пользовательский контекст); секреты входа не
реплицируются; доступ к топикам Talent — m2m ACL Kafka; tenant mismatch невозможен по построению (адресация по
ExternalTalent*Id), несоответствиеpayload.TenantIdкомпании существующего пользователя → fail closed (IDENTITY_SYNC_TENANT_MISMATCH, DLQ).
Пред/постусловия
- До обработки события. Реплика может содержать или не содержать целевую компанию/пользователя (операция
upsert). Порядок per-aggregate гарантирован ключом Talent (
TenantId/UserId); кросс-агрегатный порядок tenant→user источником не гарантируется — компенсируется R4 (retry + backoff для user до появления компании). - После обработки применённого события. Целевая сущность отражает последний snapshot
(
ReplicaVersion = payload.Version, синк-владеемые поля перезаписаны, локальные — нетронуты, R5);TalentSyncState=Synced(либоConflict/DeadLetteredпри R6/аномалии); ушли соответствующиеpin.identity.*(companies/users[+accessпри изменении прав/блокировке/архиве]); offset ack только после фиксации. Устаревшее (Version <= ReplicaVersion) или дубль (MessageId) — состояние не изменено (skip), ack. Downstream-контексты PIN зависят только отpin.identity.*, на топики Talent не подписаны (service-boundaries.md).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Реплика Talent Identity: правила синка + Entity TalentSyncState
- События (5): Event Contract: pin.identity.company-suspended (CompanySuspendedKafkaEvent), Event Contract: pin.identity.company-upserted (CompanyUpsertedKafkaEvent), Event Contract: pin.identity.user-access-changed (UserAccessChangedKafkaEvent), Event Contract: pin.identity.user-blocked (UserBlockedKafkaEvent), Event Contract: pin.identity.user-upserted (UserUpsertedKafkaEvent)
- Консюмеры (4): Consumer: Chessboard Project Deleted (pin-identity-chessboard-projects), Consumer: Identity Companies (pin-chessboard-identity-companies), Consumer: identity-users-sync (ProfileUserUpsertedConsumer, ProfileUserBlockedConsumer), Consumer: Talent Handbook Sync (pin-handbook-talent-sync)
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Identity)
- adr (1): ADR-0063 — справочники pin.handbook (репликация из Talent Handbook)
- index.md (2): Handbook Service, Identity Service (Pin.Identity)
- registries (1): Реестр событий PIN
Связанные документы
- Правила маппинга и владения полями: talent-replica.md (R1–R9).
- Целевые сущности: company.md, user.md, role.md (дефолтное назначение), user-session.md (INV-S7).
- Производимые события: company-upserted, company-suspended, user-upserted, user-blocked, user-access-changed.
- Решения: ADR-0052, ADR-0053.