Event Contract: pin.crm.deal-completed (DealCompletedEvent)
Версионируемый контракт интеграционного события «сделка завершена». Consumer-документы ссылаются сюда, не
переописывая payload. Дом полей агрегата — entity: DealEntity (раздел «Доменные и
интеграционные события», строка DealCompletedEvent); здесь перечислено только то, что реально в событии.
Назначение
Фиксирует терминальное завершение сделки: менеджер застройщика подтверждает завершение после
регистрации в Росреестре (переход Sale → Completed). Публичное: даёт downstream-контекстам факт закрытой
продажи без синхронного запроса в crm — investor-owner заводит объект в портфель инвестора, rewards
создаёт комиссионное начисление (распределение агент/агентство/ПИН) и реферальное начисление по активной
связи инвестора (crm-deal-completed.md), chessboard
переводит объект недвижимости в статус «Продан». Событие несёт атрибуты объекта недвижимости и обе цены
(базовую и конечную) — вход подбора правила вознаграждения и базы расчёта rewards
(reward-rule.md).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.crm.deal-completed |
| Topic | pin.crm.deal-completed.v1 |
| Partitions / key | 3 (низкообъёмная сущность, ADR-0056) / key = DealId (= AggregateId; порядок per-deal) |
| ContractVersion | 1 |
| Producer | crm: подтверждение завершения сделки менеджером застройщика (guard INV-DEAL-7 — Росреестр подтверждён) |
| Статус | draft |
Событие публикуется атомарно с мутацией агрегата и записью таймлайна: пока завершение не зафиксировано,
событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = DealId
(порядок per-deal).
Payload DealCompletedEvent
Только IDs, атрибуты объекта недвижимости и финансовый snapshot терминального состояния — без
ФИО/телефонов (downstream берут из своей реплики identity, см.
user-upserted). Типы полей — как в
deal.md; событие ничего не переопределяет.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
DealId | Guid | Да | Deal.Id | Immutable | Ключ агрегата (= Kafka key = AggregateId). |
TenantId | Guid | Да | Deal.TenantId | Immutable | DeveloperCompanyId застройщика (tenant boundary, ADR-0052). |
PrimaryInvestorUserId | Guid | Да | Deal.PrimaryInvestorUserId | Immutable | Основной инвестор-участник → объект в его портфель (investor-owner); у сделки допускается несколько инвесторов, в портфель заводится основной. |
CommissionRecipientUserId | Guid | Да | Deal.ExecutorAgentUserId (участник-исполнитель на момент завершения) | Additive only | Получатель комиссии — активный участник-исполнитель (Executor) сделки на момент завершения → база начисления комиссии агенту (rewards). |
OriginAgentUserId | Guid | Да | Deal.OriginAgentUserId | Immutable | Иммутабельный снапшот агента, изначально создавшего сделку (историческая атрибуция / воронка агентства / антифрод); не участвует в расчёте суммы комиссии. |
OriginAgencyCompanyId | Guid | Да | Deal.OriginAgencyCompanyId | Immutable | Агентство-основатель сделки (атрибуция; статистика агентства). |
PropertyObjectId | Guid | Да | Deal.PropertyObjectId | Immutable | Объект сделки → перевод в «Продан» (chessboard). |
ProjectId | Guid | Да | Deal.ProjectId | Additive only | Проект объекта (портфель/аналитика downstream). |
PropertyType | string (enum PropertyType) | Да | реплика объекта недвижимости chessboard на момент завершения | Additive only (новые значения) | Тип объекта (Apartment / Parking / Commercial; дом enum — reward-rule.md, значения — из chessboard). Вход предиката условий правила вознаграждения rewards. |
LayoutId | Guid? | Нет (парковка/коммерция без планировки) | реплика объекта недвижимости chessboard | Additive only | Планировка объекта — вход предиката условий правила rewards. |
LayoutGroupId | Guid? | Нет | реплика объекта недвижимости chessboard | Additive only | Группа планировок — вход предиката условий правила rewards. |
Rooms | int? | Нет | реплика объекта недвижимости chessboard (0 = студия) | Additive only | Комнатность — вход предиката диапазона комнат правила rewards. |
AreaTotalM2 | decimal | Да | реплика объекта недвижимости chessboard (> 0, <= 10000, scale 2) | Additive only | Общая площадь, м² — вход предиката диапазона площади правила rewards. |
BasePrice | Money | Да | реплика объекта недвижимости chessboard на момент завершения | Additive only | Базовая цена объекта (до ремонта и до скидки; семантика базы — reward-rule.md). База расчёта комиссии при CalculationBase = BasePrice. |
FinalPrice | Money | Да | Deal.Price | Additive only | Финальная стоимость сделки — snapshot Deal.Price на момент завершения (Deal.Price обязателен, INV не допускает null). База расчёта комиссии при CalculationBase = FinalPrice и база реферального начисления rewards. |
Currency | CurrencySnapshot | Да | справочник валют handbook по FinalPrice.CurrencyId | Additive only | Display-снимок валюты сделки на момент публикации (snapshots.md); один на событие — валюта обеих цен едина. |
ContractType | string (enum DealContractType) | Да | Deal.ContractType | Additive only (новые значения) | Ddu / Dkp; на Completed всегда задан (обязателен с группы Sale, INV-DEAL-6). |
CompletedAt | DateTimeOffset | Да | Deal.CompletedAt | Immutable | Момент подтверждения завершения застройщиком; момент asOf для подбора правила, условий платформы и реферальной ставки в rewards. |
Вложенные модели Money и CurrencySnapshot
Дом модели Money — deal.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).
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-owner | First-seen по DealId — завести объект PropertyObjectId в портфель инвестора PrimaryInvestorUserId (цена = FinalPrice) | MessageId + first-seen по DealId | < 5 мин |
rewards | Комиссионное начисление получателю CommissionRecipientUserId (Executor на момент завершения) / агентству OriginAgencyCompanyId (база — BasePrice/FinalPrice по правилу застройщика) и реферальное начисление партнёру при активной реферальной связи PrimaryInvestorUserId — crm-deal-completed.md | MessageId + 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, CompletedAt | Internal | include |
PropertyType, LayoutId, LayoutGroupId, Rooms, AreaTotalM2 | Internal (атрибуты объекта) | 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 менеджер застройщика подтверждает завершение, ThenSale → 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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ReferralAccrualEntity
- API (11): GET /api/agent/crm/statistics — Статистика агентства/агента, GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/developer/crm/statistics — Статистика ЛК застройщика, GET /api/investor/crm/deals — Мои сделки (инвестор), GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), POST /api/developer/crm/deals/{dealId}/complete — Завершить сделку, POST /api/developer/crm/deals/{dealId}/contracts-prepared — Отметить «Договоры подготовлены», POST /api/investor/crm/deals/{dealId}/acknowledge-terms — Отметить «Ознакомлен», POST /api/investor/crm/deals/{dealId}/escrow — Указать эскроу-счёт, POST /api/pin-manager/crm/deals/{dealId}/override-status — Ручная смена статуса сделки, PUT /api/agent/crm/{deals|leads}/{id}/executor — Передать ведение (сменить исполнителя)
- Use Cases (10): UC-CAL-007. Дедлайны брони CRM в календаре агента, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, UC-CRM-009. Передача ведения сделки (смена исполнителя), UC-CRM-015. Явное назначение исполнителя для разрешения спора о комиссии, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика), UC-OWN-002. Завершение сделки в CRM → купленный объект в «Мои объекты недвижимости» (system), UC-RWD-003. Сделка завершена — начисление комиссии (system), UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата), UC-RWD-016. Застройщик подтверждает начисление после смены исполнителя, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- События (5): Event Contract: pin.crm.booking-paid (BookingPaidEvent), Event Contract: pin.crm.deal-cancelled (DealCancelledEvent), Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.crm.deal-members-changed (DealMembersChangedEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent)
- Консюмеры (5): Consumer: завершённая сделка CRM → объект в портфеле инвестора (pin-investor-owner-crm-deal-completed), Consumer: Calendar CRM Deal Sync (calendar-crm-deal-events), Consumer: Chessboard Dimensions Sync (pin-rewards-chessboard-dimensions), Consumer: CRM Deal Completed, Consumer: DevStatsFunnelDealCompletedConsumer
- Саги (2): Saga «Commission Settlement» (финансовый контур вознаграждений), Saga «Deal Completion» (fan-out завершения сделки)
- scenarios (3): UC-OVR-004. От реферальной ссылки до объекта у собственника, UC-OVR-006. Сделка: от брони до регистрации (инвестор-инициатор), UC-OVR-010. От завершённой сделки до выплаты комиссии и реферального вознаграждения
- index.md (2): Calendar Service, Rewards Service
- registries (1): Реестр событий PIN
Связанные документы
- Источник (агрегат/операция): deal.md (переход
Sale → Completed, INV-DEAL-7/9); соседние контракты: deal-created.md, deal-cancelled.md; карта событий — README. - Consumer-документы: investor-owner/crm-deal-completed.md (портфель), rewards/crm-deal-completed.md (комиссионное + реферальное начисления), chessboard/crm-property-object-status.md («Продан»), notifications/crm-events.md.
- Дом атрибутов объекта и семантики баз цен для
rewards: reward-rule.md (enumPropertyType,CommissionCalculationBase); snapshot-модели — snapshots.md.