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

Event Contract: pin.profile.person-upserted (PersonUpsertedKafkaEvent)

Версионируемый контракт интеграционного события «персона создана/изменена/архивирована». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей — entity: PersonEntity (раздел «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии.

Назначение

Сигнальное событие любого изменения агрегата «Персона» (person.md): создание (в т.ч. автосоздание персоны Self консюмером identity-users-sync), правка персональных данных (Rename/SetBirthDate/SetCitizenship), изменение состава документов через root (AddDocument/UpdateDocument/ArchiveDocument — меняется DocumentsCount и Version), архив/восстановление.

Событие не содержит PII — только идентификаторы, статус и счётчики. Downstream-контексты используют его как сигнал «данные персоны изменились» без синхронного запроса в profile: главный потребитель — crm, которому нужно знать, что реквизиты, ранее скопированные в незавершённую бронь/сделку из шаблона с этой персоной, могли устареть (сами снапшоты в сделках иммутабельны и не перезаписываются — CRM решает, предлагать ли повторный запрос снапшота через internal API get-requisite-snapshot). Расшифрованные значения реквизитов передаются только синхронным internal API (simplicity-first, ADR-0056; PII в Kafka не публикуется — правило README, раздел «Events И Consumers»).

Metadata

ПолеЗначение
Event typepin.profile.person-upserted
C# typePersonUpsertedKafkaEvent
Topicpin.profile.persons.v1
Partitions / key3 партиции (масштаб 1–10k активных пользователей, ADR-0056) / key = Id (PersonId) — порядок per-person
ContractVersionstatic int ContractVersion = 1
Producerprofile: handlers createPerson/updatePerson/archivePerson/restorePerson, документ-операции через root (addPersonDocument/updatePersonDocument/archivePersonDocument), consumer identity-users-sync (автосоздание Self)
Owning teamPIN Platform Core
Статусdraft

Метаданные события — идентификатор сообщения, время события CreatedAt, версия (= ContractVersion), payload, AggregateId = Id (PersonId), AggregateType = "Person". Корреляция trace хранится на outbox-записи. Событие публикуется через транзакционный outbox атомарно со сменой состояния агрегата.

TenantId в payload отсутствует: персоны принадлежат инвестору и не tenanted (ADR-0052 — данные инвестора вне tenant; изоляция record-level по OwnerUserId).

Payload PersonUpsertedKafkaEvent

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
IdGuidДаPersonEntity.IdImmutableКлюч агрегата (= Kafka key).
OwnerUserIdGuidДаPersonEntity.OwnerUserIdImmutableВладелец аккаунта (identity.users.id) — маршрутизация к реплике пользователя у потребителя.
Relationstring (enum PersonRelationType)ДаPersonEntity.RelationAdditive only (новые значения)Self / Spouse / Child / Parent / Other — семантика в person.md.
Statusstring (enum PersonStatus)ДаPersonEntity.StatusAdditive onlyActive / Archived. Archived — сигнал «персона скрыта из выбора» (исторические снапшоты у потребителей остаются валидными).
DocumentsCountintДаPersonEntity.DocumentsCountAdditive onlyЧисло активных документов персоны (денормализация root, >= 0) — потребитель видит «документы добавлены/удалены» без деталей.
VersionlongДаPersonEntity.VersionImmutable semanticsДоменная версия после мутации (>= 1); монотонный guard у потребителей (защита от out-of-order/replay).

Вложенных моделей нет. ФИО, дата рождения, гражданство, значения и маски документов в payload не входят — PII из контекста profile в Kafka не публикуется никогда (правило person.md, раздел «Доменные и интеграционные события»).

Когда публикуется

УсловиеПереход состоянияТранзакция
Создание персоны: createPerson, автосоздание Self в identity-users-sync[*] → Active (инварианты INV-PRS-1, INV-PRS-2, INV-PRS-7)агрегат + событие в одном согласованном изменении
Правка данных: Rename / SetBirthDate / SetCitizenship (guard INV-PRS-3)Active → Active (Version++)та же
Изменение документов через root: AddDocument / UpdateDocument / ArchiveDocument (пересчёт DocumentsCount)Active → Active (Version++)та же (root + child + событие)
Архив: archivePerson (INV-PRS-4..6)Active → Archivedта же
Восстановление: restorePersonArchived → Active (INV-PRS-1 для Self, INV-PRS-7)та же

Потребители (consumers)

Consumer-контекстConsumerSide effectsИдемпотентностьЗадержка (SLA)
crmCrmPersonUpsertedConsumerПометка «реквизиты могли устареть» на незавершённых бронях/сделках, чей снапшот реквизитов ссылается на PersonId; при следующем шаге сделки CRM предлагает повторный запрос снапшота (internal API). Завершённые сделки и уже выданные снапшоты не трогаются.MessageId + Version-guard per Id< 5 мин

Consumer-документ CRM — будущий путь docs/06-services/crm/consumers/profile-person-upserted.md (контекст crm — по будущему ТЗ; конвенция ссылок). Других потребителей на текущем объёме нет; подписка новых контекстов — additive (событие публичное, без PII).

Доставка и восстановление

  • At-least-once через транзакционный outbox. Событие фиксируется в одном согласованном изменении с мутацией агрегата и досылается отдельно; недоступность брокера не откатывает бизнес-операцию (персона сохранена), публикация повторяется ретраем outbox. Следствие для потребителя — возможны дубли доставки и повторная публикация после ре-плея; обработчик обязан быть идемпотентным.
  • Порядок строгий per-person по key = Id; между разными персонами порядок не гарантирован. Потребитель держит Version-guard: событие с Version <= уже применённого идемпотентно пропускается (out-of-order/replay), реплика не откатывается.
  • Идемпотентность потребителя — дедуп по MessageId; бизнес-эффект (пометка «реквизиты могли устареть») повторно не применяется.
  • Poison-message. Событие, которое consumer не может применить после конечного числа ретраев (сбой его БД, нарушение его инвариантов), уходит в DLQ контекста-потребителя с alert; данные profile при этом уже консистентны (событие — сигнал), восстановление — ре-плей из DLQ по runbook. Неизвестный enum Relation/Status в DLQ не отправляется — обрабатывается fallback-ом (см. ниже).

Версионирование и совместимость (реальность SDK)

  • Версия — один static int ContractVersion = 1; consumer при envelope.Version != ContractVersion логирует warning и продолжает (upcasting/schema-migration в SDK нет).
  • Additive-only для minor: новые optional-поля с безопасным default — без bump.
  • Breaking (удаление/переименование/смена типа/смысла) — ContractVersion = 2 + новый топик pin.profile.persons.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum (Relation/Status): fallback — сохранить строку без изменений, warning; для Status вести себя как Archived (fail closed: неизвестное состояние не участвует в выборе реквизитов).
  • Порядок per-person гарантирован key = Id; потребители держат Version-guard.

Безопасность

ПолеКласс данныхПравило
Id, OwnerUserId, Relation, Status, DocumentsCount, VersionInternalinclude

Событие НЕ содержит secrets, tokens, ФИО/дату рождения/номера документов (PII — только шифротекстом в БД profile), ссылок на сканы storage, unbounded-вложений. Логи producer/consumer — только идентификаторы. Доступ к топику — m2m ACL Kafka (consumer-контексты платформы); untrusted-потребителей нет.

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

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

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

  • Сущность (дом полей и инварианты): person.md; документы персоны: person-document.md; шаблоны: requisite-template.md.
  • Источники (операции): API персон и документов — карта в README; автосоздание Selfidentity-users-sync.
  • Синхронная выдача реквизитов CRM: get-requisite-snapshot (будущий документ по конвенции).
  • Решения: ADR-0052 (инвестор вне tenant), ADR-0056 (simplicity-first).