Event Contract: pin.investor-owner.property-added (InvestorPropertyAddedKafkaEvent)
Версионируемый контракт интеграционного события «объект появился в портфеле инвестора». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей агрегата — investor-property.md (раздел «Доменные и интеграционные события»).
Назначение
Фиксирует создание записи InvestorPropertyEntity — купленный объект недвижимости появился в портфеле
инвестора («Мои объекты недвижимости», /profile/my-lots), независимо от источника: завершение сделки в CRM
или синхронизация владений из Talent Owners. Публичное: даёт downstream-контекстам факт появления объекта
без синхронного запроса в investor-owner — notifications отправляет уведомление инвестору,
user-activity пишет запись в ленту событий пользователя (/profile/events).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.investor-owner.property-added |
| Имя события | InvestorPropertyAddedKafkaEvent |
| Topic | pin.investor-owner.property-added.v1 |
| Partitions / key | 3 партиции (единицы-десятки объектов на инвестора, низкий поток, ADR-0056) / key = Id записи портфеля |
| ContractVersion | 1 |
| Producer | investor-owner: consumers crm-deal-completed, talent-owners-sync; admin-операция create-investor-property (компенсирующее исправление) |
| Статус | draft |
Событие несёт метаданные конверта: MessageId (Guid v7), CreatedAt, версия контракта,
AggregateId = Id, AggregateType = "InvestorProperty". Публикуется через транзакционный outbox
атомарно с созданием записи.
Payload InvestorPropertyAddedKafkaEvent
Дом полей — entity: InvestorPropertyEntity; здесь перечислено только
то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | InvestorPropertyEntity.Id | Immutable | Ключ записи (= Kafka key). |
InvestorUserId | Guid | Да | InvestorPropertyEntity.InvestorUserId | Immutable | Инвестор-владелец (identity.users.id); адресат уведомления и ленты. |
PropertyObjectId | Guid | Да | InvestorPropertyEntity.PropertyObjectId | Immutable | Объект недвижимости (chessboard); при Source = TalentOwnersSync без сопоставления с шахматкой — стабильный id объекта Talent (правило PropertyObjectId consumer-а синхронизации). |
ProjectId | Guid? | Нет | InvestorPropertyEntity.ProjectId | Additive only | Проект (ЖК/отель); null, если объект не сопоставлен с шахматкой PIN. |
Source | string (enum PropertySource) | Да | InvestorPropertyEntity.Source | Additive only (новые значения) | PinCrmDeal / TalentOwnersSync / AdminCorrection. Позволяет consumer-ам различать «поздравляем с покупкой» (сделка PIN) и «импорт существующего владения» (синхронизация). |
PurchasePrice | MoneySnapshot | Да | InvestorPropertyEntity.PurchasePrice | Additive only | За сколько купили (итоговая цена сделки / цена из Talent Owners). |
Currency | CurrencySnapshot | Да | справочник валют handbook на момент публикации | Additive only | Display-снапшот валюты PurchasePrice.CurrencyId — поля модели см. snapshots.md (не переописываются). |
PurchasedAt | DateTimeOffset | Да | InvestorPropertyEntity.PurchasedAt | Additive only | Дата покупки (завершение сделки). |
Version | long | Да | InvestorPropertyEntity.Version | Immutable semantics | Доменная версия; Version = 1 ⇔ создание записи. |
Вложенная модель MoneySnapshot
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
Amount | decimal | Да | > 0 (цена покупки, INV-4 дома полей) | Сумма. |
CurrencyId | short | Да | ссылка на справочник валют handbook (RUB-only, ADR-0048) | Валюта — типизированная ссылка; display-код — только внутри CurrencySnapshot события. |
Событие с денежной суммой несёт CurrencyId + CurrencySnapshot (правило snapshot-моделей handbook —
snapshots.md); снапшот фиксируется на момент публикации и не
«догоняется» при последующих изменениях справочника.
Минимальность payload (обоснование, ADR-0056)
Контракт сознательно минимален — только идентификаторы и факт покупки:
PropertyObjectSnapshot(наименование/номер/площадь/этаж) в payload НЕ дублируется:notificationsформирует текст по шаблону с deeplink на/profile/my-lots(данные объекта недвижимости фронт рендерит изcatalogпоPropertyObjectId),user-activityхранит в ленте только ссылочную записьPropertyObjectId. Дублирование снапшота в событии создало бы вторую копию display-данных с собственным жизненным циклом (снапшот живёт в агрегате, актуальные данные — вcatalog/chessboard).- PII отсутствует: телефон/ФИО собственника не передаются (правило дома
owner-mapping.md); контакты для доставки уведомления
notificationsберёт из собственных данных получателя поInvestorUserId. - Финансовые поля ограничены покупкой:
SalePrice/RealizedProfitне входят — продажа публикуется отдельным событиемpin.investor-owner.property-sold.v1, смешивание семантик в одном контракте потребовало бы optional-полей со сложными правилами заполнения. - Расширение — additive-only: если потребителю понадобится, например,
BuildingIdилиDealId, поле добавляется как optional без bumpContractVersion— стартовать с широкого контракта «на вырост» запрещено ADR-0056.
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
Consumer crm-deal-completed: создание записи после pin.crm.deal-completed.v1 | [*] → Active | запись и событие — в одном согласованном изменении |
| Consumer talent-owners-sync: создание записи из синхронизации Talent Owners | [*] → Active; при владении, пришедшем уже проданным, — [*] → Sold (тогда атомарно публикуется и property-sold) | та же |
Admin-операция create-investor-property (компенсирующее исправление Службой заботы, permission investor-owner.portfolio.manage) | [*] → Active | та же |
Upsert существующей записи синхронизацией (INV-7) событие НЕ публикует — оно фиксирует именно появление
объекта в портфеле; переход Active → Sold — отдельное событие property-sold.
Потребители (consumers)
| Consumer-контекст | Consumer | Эффект | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
notifications | consumer investor-property-added | Уведомление инвестору (push/СМС по правилам каналов notifications): при Source = PinCrmDeal — «объект появился в вашем портфеле» с deeplink /profile/my-lots; при Source = TalentOwnersSync — сдержанный шаблон импорта (или подавление при массовом первичном импорте) | MessageId | < 5 мин |
user-activity | consumer investor-property-added | Запись ленты событий пользователя (/profile/events): тип «покупка объекта», PropertyObjectId, PurchasedAt | MessageId + business key Id | < 5 мин |
Порядок доставки и согласованность
- Доставка — at-least-once:
notificationsиuser-activityдедуплицируют поMessageId(плюс business keyIdуuser-activity); повтор доставки не создаёт дублей уведомления/записи ленты. - Промежуточное состояние для инвестора: запись портфеля видна в «Мои объекты недвижимости» сразу после
коммита агрегата (страница читает агрегат напрямую), а это событие доставляется downstream асинхронно
(SLA < 5 мин) — инвестор может увидеть объект в списке раньше, чем получит уведомление о нём. Между
топиками (
owner-linked,property-sold) порядок не гарантирован (см. «Версионирование»).
Версионирование и совместимость
- Версия — один
ContractVersion = 1; consumer при несовпадении версии события логирует warning и продолжает (warn-and-proceed). Суффикс.v1в имени топика — конвенция. - Additive-only для minor: новые поля — optional с безопасным default, без bump.
- Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2+ новый топикpin.investor-owner.property-added.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Unknown enum
Source— fallback: сохранить строку как есть + warning;notificationsпри неизвестном источнике использует нейтральный шаблон (fail-safe, уведомление не теряется). - Порядок per-property гарантирован key =
Id(событияproperty-added→property-soldодного объекта публикуются в разные топики; consumer-ыproperty-soldне должны требовать предварительной обработкиproperty-added— идемпотентный upsert поId).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
Id, PropertyObjectId, ProjectId, Source, PurchasedAt, Version, Currency (снапшот валюты) | Internal | include |
InvestorUserId | Internal (ссылка на пользователя, не PII) | include |
PurchasePrice.* | Sensitive (финансовые данные инвестора) | include только для доверенных consumer-ов — доступ к топику ограничен m2m ACL Kafka (notifications, user-activity); в логах producer/consumer суммы не пишутся открытым текстом |
Событие НЕ содержит secrets, tokens, телефонов/ФИО (PII собственника остаётся в investor-owner),
платёжных реквизитов и unbounded-вложений.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: InvestorPropertyEntity, Entity / Aggregate: NotificationEntity
- API (1): POST /api/v1/admin/portfolio/properties — Создание записи портфеля вручную (admin, компенсирующее)
- События (2): Event Contract: pin.investor-owner.owner-linked (InvestorOwnerLinkedKafkaEvent), Event Contract: pin.investor-owner.property-sold (InvestorPropertySoldKafkaEvent)
- Консюмеры (3): Consumer: завершённая сделка CRM → объект в портфеле инвестора (pin-investor-owner-crm-deal-completed), 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
Связанные документы
- Сущность (дом полей и полный список событий агрегата):
investor-property.md; соседний контракт —
property-sold.md; snapshot-модели справочника (
CurrencySnapshot) — ../../handbook/domain/snapshots.md. - Источники публикации: crm-deal-completed.md, talent-owners-sync.md, create-investor-property.md.
- Потребители:
docs/06-services/notifications/,docs/06-services/user-activity/. - Решения: ADR-0052 (инвестор вне tenant —
TenantIdв payload отсутствует), ADR-0056 (минимальность контракта).