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

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-ownernotifications отправляет приветственное уведомление новому инвестору-собственнику, profile создаёт профиль при LinkKind = InvestorCreated (лениво зарегистрированный собственник).

Metadata

ПолеЗначение
Event typepin.investor-owner.owner-linked
Имя событияInvestorOwnerLinkedKafkaEvent
Topicpin.investor-owner.owner-linked.v1
Partitions / key3 партиции (низкий поток связей: разовый импорт + точечные сделки, ADR-0056) / key = Id связи (= OwnerId)
ContractVersion1
Producerinvestor-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Описание
OwnerIdGuidДаOwnerMappingEntity.IdImmutableИдентификатор связи = id собственника в Talent Owners (= Kafka key).
InvestorUserIdGuidДаOwnerMappingEntity.InvestorUserIdImmutableПользователь-инвестор PIN (identity.users.id); адресат приветствия и владелец создаваемого профиля. При LinkKind = InvestorCreated равен OwnerId (INV-OWN-001).
LinkKindstring (enum OwnerLinkKind)ДаOwnerMappingEntity.LinkKindAdditive only (новые значения)InvestorCreated / ExistingInvestorMatched. Позволяет consumer-ам различать нового ленивого инвестора и сопоставление существующего аккаунта.
LinkedAtDateTimeOffsetДаOwnerMappingEntity.LinkedAtAdditive onlyМомент установки связи.
VersionlongДаOwnerMappingEntity.VersionImmutable 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 без bump ContractVersion; стартовать с широкого контракта «на вырост» запрещено ADR-0056.

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

УсловиеПереход состоянияСогласованность
Consumer talent-owners-sync: инвестор найден по PhoneNumber (сопоставлен существующий)PendingLink → Linkedсвязь и событие — в одном согласованном изменении
Consumer talent-owners-sync: инвестора не было, identity создал c Id = OwnerIdPendingLink → Linkedта же
Admin-операция resolve-owner-mapping-conflict (Служба заботы, permission investor-owner.mappings.manage) — ручное указание InvestorUserIdConflict → Linkedта же

Upsert снапшота ФИО/телефона по уже связанному собственнику (Linked → Linked, INV-6) событие НЕ публикует — оно фиксирует именно установление связи, а не последующие обновления реплики.

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

Consumer-контекстConsumerЭффектИдемпотентностьЗадержка (SLA)
profileconsumer investor-owner-linkedПри LinkKind = InvestorCreated — создание профиля инвестора (idempotent upsert по InvestorUserId); при ExistingInvestorMatched — no-op (профиль есть)MessageId + business key InvestorUserId< 5 мин
notificationsconsumer investor-owner-linkedПриветственное уведомление новому инвестору-собственнику (push/СМС по правилам каналов); при массовом первичном импорте — подавление по правилу на стороне notificationsMessageId< 5 мин

Порядок доставки и согласованность

  • Доставка — at-least-once: потребители обязаны дедуплицировать по MessageId; повторная доставка не создаёт дублей (профиль — идемпотентный upsert по InvestorUserId, приветствие — дедуп по MessageId).
  • Порядок per-mapping гарантирован ключом партиционирования Id; между топиками (owner-linkedproperty-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, VersionInternalinclude
InvestorUserIdInternal (ссылка на пользователя, не PII)include

Событие НЕ содержит PhoneNumber, OwnerFullName (PII собственника остаётся в investor-owner), secrets, tokens, платёжных реквизитов и unbounded-вложений. Доступ к топику ограничен m2m ACL Kafka (profile, notifications).

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

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

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

  • Сущность (дом полей и полный список событий агрегата): 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 (минимальность контракта).