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