Event Contract: pin.investor-owner.owner-linked (InvestorOwnerLinkedKafkaEvent)
Версионируемый контракт интеграционного события «собственник Talent Owners связан с инвестором PIN». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей агрегата — owner-mapping.md (раздел «Доменные и интеграционные события»).
Назначение
Фиксирует успешный переход OwnerMappingEntity в статус Linked — телефон собственника из Talent Owners
сопоставлен с пользователем-инвестором PIN, InvestorUserId заполнен, LinkKind зафиксирован. Публичное:
даёт downstream-контекстам факт установленной связи без синхронного запроса в investor-owner —
notifications отправляет приветственное уведомление новому инвестору-собственнику, profile создаёт
профиль при LinkKind = InvestorCreated (лениво зарегистрированный собственник).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.investor-owner.owner-linked |
| Имя события | InvestorOwnerLinkedKafkaEvent |
| Topic | pin.investor-owner.owner-linked.v1 |
| Partitions / key | 3 партиции (низкий поток связей: разовый импорт + точечные сделки, ADR-0056) / key = Id связи (= OwnerId) |
| ContractVersion | 1 |
| Producer | investor-owner: consumer синхронизации Talent Owners talent-owners-sync (автолинковка); admin-операция resolve-owner-mapping-conflict (ручное разрешение, Conflict → Linked) |
| Статус | draft |
Событие несёт метаданные конверта: MessageId (Guid v7), CreatedAt, версия контракта,
AggregateId = Id, AggregateType = "OwnerMapping". Публикуется через транзакционный outbox атомарно
со сменой состояния связи.
Payload InvestorOwnerLinkedKafkaEvent
Дом полей — entity: OwnerMappingEntity; здесь перечислено только то, что
реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
OwnerId | Guid | Да | OwnerMappingEntity.Id | Immutable | Идентификатор связи = id собственника в Talent Owners (= Kafka key). |
InvestorUserId | Guid | Да | OwnerMappingEntity.InvestorUserId | Immutable | Пользователь-инвестор PIN (identity.users.id); адресат приветствия и владелец создаваемого профиля. При LinkKind = InvestorCreated равен OwnerId (INV-OWN-001). |
LinkKind | string (enum OwnerLinkKind) | Да | OwnerMappingEntity.LinkKind | Additive only (новые значения) | InvestorCreated / ExistingInvestorMatched. Позволяет consumer-ам различать нового ленивого инвестора и сопоставление существующего аккаунта. |
LinkedAt | DateTimeOffset | Да | OwnerMappingEntity.LinkedAt | Additive only | Момент установки связи. |
Version | long | Да | OwnerMappingEntity.Version | Immutable semantics | Доменная версия связи на момент публикации; для порядка/дедупликации. |
Значения LinkKind (enum OwnerLinkKind)
| Значение | Смысл для consumer-а |
|---|---|
InvestorCreated | Инвестора с телефоном не было; identity создал пользователя с Id = OwnerId (INV-OWN-001, RegisteredVia = LazyRegistration). profile создаёт профиль, notifications шлёт онбординг-приветствие. |
ExistingInvestorMatched | Инвестор уже существовал; связь установлена на его Id. profile профиль не пересоздаёт (уже есть), приветствие — по правилам notifications (может подавляться). |
Минимальность payload (обоснование, ADR-0056)
Контракт сознательно минимален — только идентификаторы и факт связи:
- PII отсутствует:
PhoneNumberиOwnerFullNameв payload не передаются (правило дома owner-mapping.md); потребителям достаточноInvestorUserId, контактные данныеnotifications/profileберут из собственных данных получателя. ConflictReason/ConflictDetailsне входят: событие фиксирует именно успешную связь; разбор конфликтов — внутренняя admin-очередь, наружу не публикуется.- Расширение — additive-only: понадобится, например,
ResolvedByUserId(ручное разрешение) — добавляется как optional без bumpContractVersion; стартовать с широкого контракта «на вырост» запрещено ADR-0056.
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
Consumer talent-owners-sync: инвестор найден по PhoneNumber (сопоставлен существующий) | PendingLink → Linked | связь и событие — в одном согласованном изменении |
Consumer talent-owners-sync: инвестора не было, identity создал c Id = OwnerId | PendingLink → Linked | та же |
Admin-операция resolve-owner-mapping-conflict (Служба заботы, permission investor-owner.mappings.manage) — ручное указание InvestorUserId | Conflict → Linked | та же |
Upsert снапшота ФИО/телефона по уже связанному собственнику (Linked → Linked, INV-6) событие НЕ
публикует — оно фиксирует именно установление связи, а не последующие обновления реплики.
Потребители (consumers)
| Consumer-контекст | Consumer | Эффект | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
profile | consumer investor-owner-linked | При LinkKind = InvestorCreated — создание профиля инвестора (idempotent upsert по InvestorUserId); при ExistingInvestorMatched — no-op (профиль есть) | MessageId + business key InvestorUserId | < 5 мин |
notifications | consumer investor-owner-linked | Приветственное уведомление новому инвестору-собственнику (push/СМС по правилам каналов); при массовом первичном импорте — подавление по правилу на стороне notifications | MessageId | < 5 мин |
Порядок доставки и согласованность
- Доставка — at-least-once: потребители обязаны дедуплицировать по
MessageId; повторная доставка не создаёт дублей (профиль — идемпотентный upsert поInvestorUserId, приветствие — дедуп поMessageId). - Порядок per-mapping гарантирован ключом партиционирования
Id; между топиками (owner-linked↔property-added/property-sold) порядок не гарантирован. - Окно eventual-consistency — SLA < 5 мин на потребителя. Для лениво созданного инвестора
(
InvestorCreated) события объектов портфеля могут прийти вuser-activity/notificationsраньше, чемprofileсоздаст профиль поowner-linked: это допустимо — адресат всех событийInvestorUserId, профиль не требуется для доставки уведомления или записи ленты;profileдовосстановит профиль по своему событию идемпотентно.
Версионирование и совместимость
- Версия — один
ContractVersion = 1; consumer при несовпадении версии события логирует warning и продолжает (warn-and-proceed). Суффикс.v1в имени топика — конвенция. - Additive-only для minor: новые поля — optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2+ новый топикpin.investor-owner.owner-linked.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum
LinkKind— fallback: сохранить строку как есть + warning;profileпри неизвестном виде связи профиль не пересоздаёт (fail-safe),notificationsиспользует нейтральный шаблон. - Порядок per-mapping гарантирован key =
Id; повторная доставка — идемпотентна поInvestorUserId(upsert профиля, дедуп уведомления поMessageId).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
OwnerId, LinkKind, LinkedAt, Version | Internal | include |
InvestorUserId | Internal (ссылка на пользователя, не PII) | include |
Событие НЕ содержит PhoneNumber, OwnerFullName (PII собственника остаётся в investor-owner),
secrets, tokens, платёжных реквизитов и unbounded-вложений. Доступ к топику ограничен m2m ACL Kafka
(profile, notifications).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: OwnerMappingEntity
- События (1): Event Contract: pin.investor-owner.property-sold (InvestorPropertySoldKafkaEvent)
- Консюмеры (2): Consumer: синхронизация из Talent Owners (pin-investor-owner-talent-owners-sync), Consumer: Investor-Owner Events → Notifications (notifications-investor-owner-events)
- scenarios (1): UC-OVR-004. От реферальной ссылки до объекта у собственника
- index.md (1): Сервис Инвестор-Собственник (investor-owner)
- registries (1): Реестр событий PIN
Связанные документы
- Сущность (дом полей и полный список событий агрегата): owner-mapping.md; соседние контракты объектов — property-added.md, property-sold.md.
- Источники публикации: talent-owners-sync.md, admin resolve-owner-mapping-conflict.md.
- Потребители:
docs/06-services/profile/,docs/06-services/notifications/. - Создание инвестора: internal-API
identity../../identity/api/provision-investor.md. - Решения: ADR-0052 (инвестор вне tenant —
TenantIdв payload отсутствует), ADR-0056 (минимальность контракта).