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 type | pin.profile.person-upserted |
| C# type | PersonUpsertedKafkaEvent |
| Topic | pin.profile.persons.v1 |
| Partitions / key | 3 партиции (масштаб 1–10k активных пользователей, ADR-0056) / key = Id (PersonId) — порядок per-person |
| ContractVersion | static int ContractVersion = 1 |
| Producer | profile: handlers createPerson/updatePerson/archivePerson/restorePerson, документ-операции через root (addPersonDocument/updatePersonDocument/archivePersonDocument), consumer identity-users-sync (автосоздание Self) |
| Owning team | PIN 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 | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PersonEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
OwnerUserId | Guid | Да | PersonEntity.OwnerUserId | Immutable | Владелец аккаунта (identity.users.id) — маршрутизация к реплике пользователя у потребителя. |
Relation | string (enum PersonRelationType) | Да | PersonEntity.Relation | Additive only (новые значения) | Self / Spouse / Child / Parent / Other — семантика в person.md. |
Status | string (enum PersonStatus) | Да | PersonEntity.Status | Additive only | Active / Archived. Archived — сигнал «персона скрыта из выбора» (исторические снапшоты у потребителей остаются валидными). |
DocumentsCount | int | Да | PersonEntity.DocumentsCount | Additive only | Число активных документов персоны (денормализация root, >= 0) — потребитель видит «документы добавлены/удалены» без деталей. |
Version | long | Да | PersonEntity.Version | Immutable 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 | та же |
Восстановление: restorePerson | Archived → Active (INV-PRS-1 для Self, INV-PRS-7) | та же |
Потребители (consumers)
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
crm | CrmPersonUpsertedConsumer | Пометка «реквизиты могли устареть» на незавершённых бронях/сделках, чей снапшот реквизитов ссылается на 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. Неизвестный enumRelation/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, Version | Internal | include |
Событие НЕ содержит secrets, tokens, ФИО/дату рождения/номера документов (PII — только шифротекстом в БД
profile), ссылок на сканы storage, unbounded-вложений. Логи producer/consumer — только идентификаторы.
Доступ к топику — m2m ACL Kafka (consumer-контексты платформы); untrusted-потребителей нет.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: PersonEntity, Entity / Aggregate: RecipientContactEntity, Entity / Aggregate: RequisiteTemplateEntity
- API (5): DELETE /api/v1/profile/persons/{personId}/documents/{documentId}/scans/{fileId} — Удаление скана документа (removeDocumentScan), POST /api/v1/profile/persons — Создание персоны (createPerson), POST /api/v1/profile/persons/{personId}/documents — Добавление документа персоны (addPersonDocument), POST /api/v1/profile/persons/{personId}/documents/{documentId}/scans — Прикрепление скана документа (attachDocumentScan), POST /api/v1/profile/persons/{personId}/restore — Восстановление персоны (restorePerson)
- Use Cases (2): UC-PRF-001. Заполнение профиля и персоны «Сам» (первый вход инвестора в ЛК), UC-PRF-002. Добавление персоны «Супруг(а)» с паспортом и сканами
- Консюмеры (2): Consumer: identity-users-sync (ProfileUserUpsertedConsumer, ProfileUserBlockedConsumer), Consumer: Profile Events → Notifications (notifications-profile-events)
- registries (1): Реестр событий PIN
- index.md (1): Profile Service (Pin.Profile)
Связанные документы
- Сущность (дом полей и инварианты): person.md; документы персоны: person-document.md; шаблоны: requisite-template.md.
- Источники (операции): API персон и документов — карта в README; автосоздание
Self— identity-users-sync. - Синхронная выдача реквизитов CRM: get-requisite-snapshot (будущий документ по конвенции).
- Решения: ADR-0052 (инвестор вне tenant), ADR-0056 (simplicity-first).