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

Event Contract: pin.crm.deal-completed (DealCompletedEvent)

Версионируемый контракт интеграционного события «сделка завершена». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей агрегата — entity: DealEntity (раздел «Доменные и интеграционные события», строка DealCompletedEvent); здесь перечислено только то, что реально в событии.

Назначение

Фиксирует терминальное завершение сделки: менеджер застройщика подтверждает завершение после регистрации в Росреестре (переход Sale → Completed). Публичное: даёт downstream-контекстам факт закрытой продажи без синхронного запроса в crminvestor-owner заводит объект в портфель инвестора, rewards создаёт комиссионное начисление (распределение агент/агентство/ПИН) и реферальное начисление по активной связи инвестора (crm-deal-completed.md), chessboard переводит объект недвижимости в статус «Продан». Событие несёт атрибуты объекта недвижимости и обе цены (базовую и конечную) — вход подбора правила вознаграждения и базы расчёта rewards (reward-rule.md).

Metadata

ПолеЗначение
Event typepin.crm.deal-completed
Topicpin.crm.deal-completed.v1
Partitions / key3 (низкообъёмная сущность, ADR-0056) / key = DealId (= AggregateId; порядок per-deal)
ContractVersion1
Producercrm: подтверждение завершения сделки менеджером застройщика (guard INV-DEAL-7 — Росреестр подтверждён)
Статусdraft

Событие публикуется атомарно с мутацией агрегата и записью таймлайна: пока завершение не зафиксировано, событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = DealId (порядок per-deal).

Payload DealCompletedEvent

Только IDs, атрибуты объекта недвижимости и финансовый snapshot терминального состояния — без ФИО/телефонов (downstream берут из своей реплики identity, см. user-upserted). Типы полей — как в deal.md; событие ничего не переопределяет.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
DealIdGuidДаDeal.IdImmutableКлюч агрегата (= Kafka key = AggregateId).
TenantIdGuidДаDeal.TenantIdImmutableDeveloperCompanyId застройщика (tenant boundary, ADR-0052).
PrimaryInvestorUserIdGuidДаDeal.PrimaryInvestorUserIdImmutableОсновной инвестор-участник → объект в его портфель (investor-owner); у сделки допускается несколько инвесторов, в портфель заводится основной.
CommissionRecipientUserIdGuidДаDeal.ExecutorAgentUserId (участник-исполнитель на момент завершения)Additive onlyПолучатель комиссии — активный участник-исполнитель (Executor) сделки на момент завершения → база начисления комиссии агенту (rewards).
OriginAgentUserIdGuidДаDeal.OriginAgentUserIdImmutableИммутабельный снапшот агента, изначально создавшего сделку (историческая атрибуция / воронка агентства / антифрод); не участвует в расчёте суммы комиссии.
OriginAgencyCompanyIdGuidДаDeal.OriginAgencyCompanyIdImmutableАгентство-основатель сделки (атрибуция; статистика агентства).
PropertyObjectIdGuidДаDeal.PropertyObjectIdImmutableОбъект сделки → перевод в «Продан» (chessboard).
ProjectIdGuidДаDeal.ProjectIdAdditive onlyПроект объекта (портфель/аналитика downstream).
PropertyTypestring (enum PropertyType)Дареплика объекта недвижимости chessboard на момент завершенияAdditive only (новые значения)Тип объекта (Apartment / Parking / Commercial; дом enum — reward-rule.md, значения — из chessboard). Вход предиката условий правила вознаграждения rewards.
LayoutIdGuid?Нет (парковка/коммерция без планировки)реплика объекта недвижимости chessboardAdditive onlyПланировка объекта — вход предиката условий правила rewards.
LayoutGroupIdGuid?Нетреплика объекта недвижимости chessboardAdditive onlyГруппа планировок — вход предиката условий правила rewards.
Roomsint?Нетреплика объекта недвижимости chessboard (0 = студия)Additive onlyКомнатность — вход предиката диапазона комнат правила rewards.
AreaTotalM2decimalДареплика объекта недвижимости chessboard (> 0, <= 10000, scale 2)Additive onlyОбщая площадь, м² — вход предиката диапазона площади правила rewards.
BasePriceMoneyДареплика объекта недвижимости chessboard на момент завершенияAdditive onlyБазовая цена объекта (до ремонта и до скидки; семантика базы — reward-rule.md). База расчёта комиссии при CalculationBase = BasePrice.
FinalPriceMoneyДаDeal.PriceAdditive onlyФинальная стоимость сделки — snapshot Deal.Price на момент завершения (Deal.Price обязателен, INV не допускает null). База расчёта комиссии при CalculationBase = FinalPrice и база реферального начисления rewards.
CurrencyCurrencySnapshotДасправочник валют handbook по FinalPrice.CurrencyIdAdditive onlyDisplay-снимок валюты сделки на момент публикации (snapshots.md); один на событие — валюта обеих цен едина.
ContractTypestring (enum DealContractType)ДаDeal.ContractTypeAdditive only (новые значения)Ddu / Dkp; на Completed всегда задан (обязателен с группы Sale, INV-DEAL-6).
CompletedAtDateTimeOffsetДаDeal.CompletedAtImmutableМомент подтверждения завершения застройщиком; момент asOf для подбора правила, условий платформы и реферальной ставки в rewards.

Вложенные модели Money и CurrencySnapshot

Дом модели Moneydeal.md; таблицу полей не дублируем. В событии: Amount decimal (numeric(18,2), >= 0), CurrencyId short — типизированная ссылка на справочник валют handbook (RUB-only, ADR-0048). BasePrice и FinalPrice несут один и тот же CurrencyId (валюта сделки едина — расхождение потребитель трактует как нарушение контракта). Строкового кода валюты Money не несёт — display-код (RUB) потребители берут из снапшота Currency; CurrencySnapshot — вложенная snapshot-модель handbook, её поля не переописываются (snapshots.md).

Что не входит в payload

EscrowAccountNumber, BankId, документы, участники и ФИО/телефоны в payload не входят (не нужны consumer-ам завершения; PII/Sensitive не покидают домен без нужды — см. раздел «Безопасность»).

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

УсловиеПереход состоянияПубликация
Менеджер застройщика (TenantId) подтверждает завершение после Росреестра (guard INV-DEAL-7: порядок договоры → эскроу(Ddu) → Росреестр → завершение)Sale → Completedатомарно со сменой состояния агрегата и записью таймлайна

Публикуется ровно один раз на сделку: Completed терминален и не редактируется (INV-DEAL-9), повторное завершение отклоняется с ошибкой CRM_DEAL_ALREADY_TERMINAL и события не даёт. Прекращение сделки — отдельное событие deal-cancelled.

Снимок получателя комиссии

CommissionRecipientUserId фиксируется снимком Deal.ExecutorAgentUserId (текущий участник-исполнитель) на момент завершения: если исполнитель менялся в ходе сделки, комиссию получает Executor на момент Completed, тогда как OriginAgentUserId/OriginAgencyCompanyId несут иммутабельную атрибуцию основателя.

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

Сверено с deal.md («Доменные и интеграционные события») и README (Events и Consumers).

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
investor-ownerFirst-seen по DealId — завести объект PropertyObjectId в портфель инвестора PrimaryInvestorUserId (цена = FinalPrice)MessageId + first-seen по DealId< 5 мин
rewardsКомиссионное начисление получателю CommissionRecipientUserId (Executor на момент завершения) / агентству OriginAgencyCompanyId (база — BasePrice/FinalPrice по правилу застройщика) и реферальное начисление партнёру при активной реферальной связи PrimaryInvestorUserIdcrm-deal-completed.mdMessageId + DealId (обе ветки)< 5 мин
chessboardПеревод объекта недвижимости PropertyObjectId в статус «Продан» (владелец статусов объекта недвижимости — chessboard, решение 5 README)MessageId + DealId< 1 мин
notificationsУведомления сторонам сделки о завершенииMessageId + DealId< 5 мин

Все consumer-ы идемпотентны по MessageId; бизнес-идемпотентность — first-seen по DealId (терминальное событие, повторная доставка не должна дублировать объект в портфеле / начисление комиссии / смену статуса).

Доставка, порядок и обработка сбоев

  • Гарантия публикации. Событие уходит только после фиксации перехода Sale → Completed и записи таймлайна; при откате не публикуется. Доставка — at-least-once, ключ DealId; на сделку эмитится ровно один раз (INV-DEAL-9, Completed терминален).
  • Порядок. События одной сделки упорядочены ключом DealId.
  • Идемпотентность потребителя. Все потребители — first-seen по DealId (+ MessageId): повторная доставка не задваивает объект в портфеле (investor-owner), начисление комиссии (rewards), смену статуса «Продан» (chessboard) и уведомление (notifications).
  • Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки — терминальный факт завершения не теряется.
  • DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом; проигрывание безопасно (first-seen). Потеря события означает необновлённый портфель/комиссию/статус объекта — глубина DLQ этого топика мониторится строго.

Версионирование и совместимость

  • ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции — суффикс .v1.
  • Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
  • Breaking (удаление/переименование/смена типа/смысла) — новый ContractVersion = 2 + новый топик pin.crm.deal-completed.v2 + dual-publish на период миграции + deadline миграции consumer-ов + sunset .v1.
  • Unknown enum ContractType — fallback: сохранить строку как есть, warning (без падения консюмера).
  • Unknown enum PropertyType (новый тип объекта в chessboard) — warning без падения; rewards трактует fail-closed для правил с непустым условием по типу (reward-rule.md).

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

ПолеКласс данныхПравило
DealId, TenantId, PrimaryInvestorUserId, CommissionRecipientUserId, OriginAgentUserId, OriginAgencyCompanyId, PropertyObjectId, ProjectId, ContractType, CompletedAtInternalinclude
PropertyType, LayoutId, LayoutGroupId, Rooms, AreaTotalM2Internal (атрибуты объекта)include (вход предикатов правил rewards)
BasePrice, FinalPrice (Amount/CurrencyId), Currency (CurrencySnapshot)Sensitive (финансовое)include (нужно rewards для начислений, investor-owner для портфеля); в логах producer/consumer суммы не печатать
Чего в событии нет

Событие НЕ содержит ФИО/телефонов, EscrowAccountNumber, банковских реквизитов, документов, secrets, tokens. ФИО/телефоны сторон consumer-ы берут из собственной реплики identity (user-upserted); правило маскирования при логировании этих реплик — ФИО не печатать полностью, телефон +7***1234 (единое правило PII платформы). Доступ к топику — m2m ACL Kafka (только перечисленные consumer-контексты).

Наблюдаемость

  • Логи (без суммы/PII): факт публикации deal-completed с идентификаторами (DealId/ PrimaryInvestorUserId/CommissionRecipientUserId/OriginAgentUserId/OriginAgencyCompanyId/ PropertyObjectId/ProjectId/ContractType/CompletedAt); суммы BasePrice/FinalPrice в логах не печатаются (Sensitive).
  • Метрики: число завершений по топику; сквозная задержка до chessboard (< 1 мин — «Продан»), до investor-owner/rewards/notifications (< 5 мин); глубина DLQ (порог алерта > 0 — критично для портфеля/комиссии); доля повторных доставок.
  • Доступ: топик — m2m ACL Kafka (только перечисленные потребители).

Acceptance Criteria

  • Given сделка в Sale с подтверждённой регистрацией Росреестра (guard INV-DEAL-7: договоры → эскроу(Ddu) → Росреестр → завершение), When менеджер застройщика подтверждает завершение, Then Sale → Completed, ровно одно deal-completed публикуется с атрибутами объекта (PropertyType/ AreaTotalM2/LayoutId?/LayoutGroupId?/Rooms?), обеими ценами (BasePrice/FinalPrice, единый CurrencyId + Currency-снапшот), ContractType, получателем комиссии CommissionRecipientUserId (= Deal.ExecutorAgentUserId на момент завершения) и атрибуцией OriginAgentUserId/OriginAgencyCompanyId.
  • Given сделка уже терминальна, When повторное завершение, Then отказ CRM_DEAL_ALREADY_TERMINAL, события нет.
  • Given повторная доставка события, When investor-owner обрабатывает, Then объект заводится в портфель инвестора один раз (first-seen по DealId).
  • Given повторная доставка, When rewards обрабатывает, Then комиссионное начисление получателю CommissionRecipientUserId и реферальное начисление фиксируются по одному разу (first-seen по DealId в обеих ветках).
  • Given неизвестное значение ContractType, When потребитель обрабатывает, Then строка сохраняется как есть + warning (без падения консюмера).
  • Given breaking-изменение payload, When новый контракт, Then pin.crm.deal-completed.v2 + dual-publish + sunset .v1.

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

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

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