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

Event Contract: pin.crm.deal-cancelled (DealCancelledEvent)

Версионируемый контракт интеграционного события «сделка отменена / отказ». Consumer-документы ссылаются сюда, не переописывая payload.

Назначение

Фиксирует прекращение сделки DealEntity — переход в терминальный DealStatus.Cancelled (US-INV-03 «отказ»). Публикуется при отмене сделки (инвариант INV-DEAL-8) из любого нетерминального статуса. Публичное: даёт downstream-контекстам факт отказа без синхронного запроса в crmchessboard освобождает занятый объект недвижимости, notifications уведомляет участников.

Metadata

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

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

Payload DealCancelledEvent

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

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
DealIdGuidДаDealEntity.IdImmutableКлюч агрегата (= Kafka key, AggregateId).
TenantIdGuidДаDealEntity.TenantIdImmutableDeveloperCompanyId застройщика — tenant boundary.
PropertyObjectIdGuidДаDealEntity.PropertyObjectIdImmutableОбъект недвижимости сделки (логическая ссылка на chessboard); consumer chessboard освобождает его.
CancelledByUserIdGuid?Нетactor отмены (= DealEntity.UpdatedByUserId)Additive onlyКто отменил сделку; null — системная/каскадная отмена (worker/consumer).
CancelReasonstringДаDealEntity.CancelReasonAdditive onlyПричина отказа (обязательна при Status = Cancelled — INV-DEAL-8, check ck_deals_cancel_reason; varchar(1000)). Free text — правило PII ниже.
CancelledAtDateTimeOffsetДаDealEntity.CancelledAtImmutableМомент отказа.
Что не входит в payload

Price, ContractType, EscrowAccountNumber, состав участников и документы в payload не входят: consumer-ам отмены они не нужны (эскроу — Sensitive, наружу не покидает домен по правилу deal.md).

Расхождение с исходным ТЗ-наброском payload

Набросок помечал причину как Reason? (optional) — но домен (deal.md, INV-DEAL-8 + check ck_deals_cancel_reason) делает CancelReason обязательной в состоянии Cancelled, поэтому в событии поле присутствует всегда. Дом поля — deal.md.

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

УсловиеПереход состоянияПубликация
Отмена/отказ по сделке из нетерминального статуса; активная бронь отменяется тем же изменением (объект недвижимости освобождается)Application / Booking / Sale → Cancelledатомарно со сменой состояния агрегата, отменой активной брони и записью таймлайна

Повторная отмена терминальной сделки запрещена (INV-DEAL-9 → CRM_DEAL_ALREADY_TERMINAL), поэтому событие на один DealId эмитится не более одного раза. Завершение сделки (Sale → Completed) публикуется отдельным событием deal-completed, а не этим.

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

Consumer-контекстSide effectsИдемпотентностьЗадержка (SLA)
chessboardОсвободить объект недвижимости PropertyObjectId (перевод в «Свободен»), если он был занят этой сделкой; идемпотентно с booking-cancelled (property-object-state guard)MessageId + текущее состояние объекта недвижимости< 1 мин
rewardsОтмена начислений по сделке (CancelReason = DealCancelled): комиссионного и реферального (crm-deal-cancelled); после гейта получения денег — не отменяются (warning + ручная компенсация). Начисления существуют только у завершённых сделок, поэтому штатно доминирует идемпотентный skip — ветка отмены защитнаяMessageId + DealId (терминальный Cancelled)< 5 мин
notificationsУведомление участникам сделки об отказе (детали CancelReason — по политике каналов, не сторонним)MessageId< 5 мин

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

  • Гарантия публикации. Событие уходит только после фиксации перехода → Cancelled (с отменой активной брони и записью таймлайна тем же изменением, INV-DEAL-8); при откате не публикуется. Доставка — at-least-once, ключ DealId; на один DealId эмитится не более одного раза (INV-DEAL-9).
  • Порядок. События одной сделки упорядочены ключом DealId. Относительно booking-cancelled того же объекта кросс-топиковый порядок не гарантируется — chessboard освобождает объект идемпотентно (целевой статус «Свободен» для обоих).
  • Идемпотентность потребителя. chessboard — по текущему состоянию объекта (повторное «Свободен» — no-op); notifications — по MessageId.
  • Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки, освобождение объекта / уведомление не теряется.
  • DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом оператору; проигрывание из DLQ безопасно (обработка идемпотентна).

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

  • ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик версию кодирует по конвенции — суффикс .v1.
  • Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
  • Breaking (удаление/переименование/смена типа/смысла) — ContractVersion = 2 + топик pin.crm.deal-cancelled.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Порядок per-deal гарантирован key-ом (DealId); consumer chessboard идемпотентен и устойчив к переупорядочению относительно booking-cancelled (fail-safe: объект недвижимости освобождается ровно один раз).

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

ПолеКласс данныхПравило
DealId, TenantId, PropertyObjectId, CancelledAtInternalinclude
CancelledByUserIdInternalinclude
CancelReasonPII (free text — может содержать ФИО/телефон)include (нужен notifications/аудиту); в логах producer/consumer — маскирование (ФИО, +7***1234); инвестору/сторонним детали причины не раскрываются по политике каналов
Чего в событии нет

Событие НЕ содержит secrets, tokens, платёжных реквизитов, EscrowAccountNumber, полных PII-профилей, unbounded вложений.

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

  • Логи: факт публикации deal-cancelled с DealId/TenantId/PropertyObjectId/CancelledByUserId/ CancelledAt; CancelReason (free-text) в логах не печатается целиком — маскирование (может содержать ФИО/телефон, введённые вручную).
  • Метрики: число отмен по топику; сквозная задержка до chessboard (< 1 мин — освобождение объекта) и notifications (< 5 мин); глубина DLQ (порог алерта > 0); доля повторных доставок.
  • Доступ: топик — m2m ACL Kafka (chessboard/notifications); детали CancelReason инвестору/сторонним не раскрываются (политика каналов).

Acceptance Criteria

  • Given сделка в нетерминальном статусе (Application/Booking/Sale), When актор отменяет её с указанием причины, Then Status → Cancelled, активная бронь отменяется тем же изменением, ровно одно deal-cancelled публикуется с непустым CancelReason и CancelledAt.
  • Given сделка уже терминальна (Completed/Cancelled), When повторная попытка отмены, Then отказ CRM_DEAL_ALREADY_TERMINAL, событие не публикуется.
  • Given отмена без причины, When валидация, Then операция отклоняется (INV-DEAL-8, check ck_deals_cancel_reason) — событие с пустой причиной невозможно.
  • Given повторная доставка в chessboard, When объект уже освобождён (гонка с booking-cancelled), Then переход — no-op (объект освобождён ровно один раз).
  • Given breaking-изменение payload, When новый контракт, Then pin.crm.deal-cancelled.v2 + dual-publish + sunset .v1.

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

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

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