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

Event Contract: pin.investor-owner.property-sold (InvestorPropertySoldKafkaEvent)

Версионируемый контракт интеграционного события «объект инвестора продан, доход зафиксирован». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей агрегата — investor-property.md (раздел «Доменные и интеграционные события»).

Назначение

Фиксирует переход InvestorPropertyEntity Active → Sold — объект недвижимости продан, атомарно зафиксированы SalePrice, SoldAt и вычисленный агрегатом RealizedProfit («за сколько продали, сколько и когда заработали»). Публичное: даёт downstream-контекстам факт продажи и реализованного дохода без синхронного запроса в investor-ownernotifications уведомляет инвестора, user-activity пишет запись «продажа объекта» в ленту событий пользователя (/profile/events).

Metadata

ПолеЗначение
Event typepin.investor-owner.property-sold
Имя событияInvestorPropertySoldKafkaEvent
Topicpin.investor-owner.property-sold.v1
Partitions / key3 партиции (единицы-десятки объектов на инвестора, низкий поток, ADR-0056) / key = Id записи портфеля
ContractVersion1
Producerinvestor-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Описание
IdGuidДаInvestorPropertyEntity.IdImmutableКлюч записи портфеля (= Kafka key).
InvestorUserIdGuidДаInvestorPropertyEntity.InvestorUserIdImmutableИнвестор-владелец (identity.users.id); адресат уведомления и ленты.
PropertyObjectIdGuidДаInvestorPropertyEntity.PropertyObjectIdImmutableПроданный объект недвижимости (chessboard); при Source = TalentOwnersSync без сопоставления с шахматкой — стабильный id объекта Talent.
SalePriceMoneySnapshotДаInvestorPropertyEntity.SalePriceAdditive onlyЗа сколько продали.
SoldAtDateTimeOffsetДаInvestorPropertyEntity.SoldAtAdditive onlyДата продажи (она же «когда заработали», EarnedAt).
RealizedProfitMoneySnapshotДаInvestorPropertyEntity.RealizedProfitAdditive onlyРеализованный доход SalePrice.Amount − PurchasePrice.Amount (валюта общая); может быть отрицательным (убыток).
CurrencyCurrencySnapshotДасправочник валют handbook на момент публикацииAdditive onlyDisplay-снапшот единой валюты записи (SalePrice.CurrencyId = RealizedProfit.CurrencyId, INV-5) — поля модели см. snapshots.md (не переописываются).
VersionlongДаInvestorPropertyEntity.VersionImmutable semanticsДоменная версия записи на момент продажи; для порядка/дедупликации.

Вложенная модель MoneySnapshot

ПолеТипОбязательностьОграниченияОписание
AmountdecimalДаSalePrice > 0 (INV-4); RealizedProfit допускает < 0 (убыток)Сумма.
CurrencyIdshortДассылка на справочник валют 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 без bump ContractVersion; стартовать с широкого контракта запрещено ADR-0056.

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

УсловиеПереход состоянияСогласованность
Consumer talent-owners-sync: фиксация продажи по владению, помеченному проданным в Talent OwnersActive → 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)
notificationsconsumer investor-property-soldУведомление инвестору «продажа зафиксирована» с итогом дохода и deeplink /profile/my-lots; при массовом первичном импорте проданных владений — подавление по правилу на стороне notificationsMessageId< 5 мин
user-activityconsumer investor-property-soldЗапись ленты событий пользователя (/profile/events): тип «продажа объекта», PropertyObjectId, SoldAt, RealizedProfitMessageId + business key Id< 5 мин

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

  • Доставка — at-least-once: notifications и user-activity дедуплицируют по MessageId (плюс business key Id у 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-addedproperty-sold одного объекта публикуются в разные топики; consumer-ы property-sold не должны требовать предварительной обработки property-added — идемпотентный upsert по Id; при владении, пришедшем уже проданным, оба события публикуются одной транзакцией и упорядочены по Version).

Безопасность

ПолеКласс данныхПравило
Id, PropertyObjectId, SoldAt, Version, Currency (снапшот валюты)Internalinclude
InvestorUserIdInternal (ссылка на пользователя, не PII)include
SalePrice.*, RealizedProfit.*Sensitive (финансовые данные инвестора)include только для доверенных consumer-ов — доступ к топику ограничен m2m ACL Kafka (notifications, user-activity); в логах producer/consumer суммы не пишутся открытым текстом

Событие НЕ содержит secrets, tokens, телефонов/ФИО (PII собственника остаётся в investor-owner), платёжных реквизитов и unbounded-вложений.

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

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

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

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