Event Contract: pin.crm.deal-cancelled (DealCancelledEvent)
Версионируемый контракт интеграционного события «сделка отменена / отказ». Consumer-документы ссылаются сюда, не переописывая payload.
Назначение
Фиксирует прекращение сделки DealEntity — переход в терминальный DealStatus.Cancelled (US-INV-03
«отказ»). Публикуется при отмене сделки (инвариант INV-DEAL-8) из любого нетерминального статуса.
Публичное: даёт downstream-контекстам факт отказа без синхронного запроса в crm — chessboard освобождает
занятый объект недвижимости, notifications уведомляет участников.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.crm.deal-cancelled |
| Topic | pin.crm.deal-cancelled.v1 |
| Partitions / key | 3 (ADR-0056; низкообъёмная сущность) / key = AggregateId = DealId (порядок per-deal) |
| ContractVersion | 1 |
| Producer | crm: отмена сделки (команда отказа агента/менеджера/инвестора, INV-DEAL-8) |
| Статус | draft |
Событие публикуется атомарно с мутацией агрегата (и отменой активной брони тем же изменением —
INV-DEAL-8): пока отказ не зафиксирован, событие не отправляется; после фиксации — доставляется
гарантированно (at-least-once), ключ = DealId.
Payload DealCancelledEvent
Дом полей — entity: DealEntity (раздел «Доменные и интеграционные события»);
здесь перечислено только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
DealId | Guid | Да | DealEntity.Id | Immutable | Ключ агрегата (= Kafka key, AggregateId). |
TenantId | Guid | Да | DealEntity.TenantId | Immutable | DeveloperCompanyId застройщика — tenant boundary. |
PropertyObjectId | Guid | Да | DealEntity.PropertyObjectId | Immutable | Объект недвижимости сделки (логическая ссылка на chessboard); consumer chessboard освобождает его. |
CancelledByUserId | Guid? | Нет | actor отмены (= DealEntity.UpdatedByUserId) | Additive only | Кто отменил сделку; null — системная/каскадная отмена (worker/consumer). |
CancelReason | string | Да | DealEntity.CancelReason | Additive only | Причина отказа (обязательна при Status = Cancelled — INV-DEAL-8, check ck_deals_cancel_reason; varchar(1000)). Free text — правило PII ниже. |
CancelledAt | DateTimeOffset | Да | DealEntity.CancelledAt | Immutable | Момент отказа. |
Price, ContractType, EscrowAccountNumber, состав участников и документы в payload не входят:
consumer-ам отмены они не нужны (эскроу — Sensitive, наружу не покидает домен по правилу deal.md).
Набросок помечал причину как 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); consumerchessboardидемпотентен и устойчив к переупорядочению относительноbooking-cancelled(fail-safe: объект недвижимости освобождается ровно один раз).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
DealId, TenantId, PropertyObjectId, CancelledAt | Internal | include |
CancelledByUserId | Internal | include |
CancelReason | PII (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 актор отменяет её с указанием причины, ThenStatus → 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.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (5): GET /api/agent/crm/statistics — Статистика агентства/агента, GET /api/investor/crm/deals — Мои сделки (инвестор), POST /api/agent/crm/bookings/{bookingId}/cancel — Отменить бронь, POST /api/pin-manager/crm/deals/{dealId}/override-status — Ручная смена статуса сделки, PUT /api/agent/crm/deals/{dealId}/step — Переместить сделку по этапу (канбан)
- Use Cases (6): UC-CAL-007. Дедлайны брони CRM в календаре агента, UC-CRM-003. Движение сделки по канбан-доске, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, UC-CRM-006. Вмешательство Менеджера ПИН, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика), UC-RWD-003. Сделка завершена — начисление комиссии (system)
- События (5): Event Contract: pin.crm.booking-cancelled (BookingCancelledEvent), Event Contract: pin.crm.deal-completed (DealCompletedEvent), Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.crm.deal-members-changed (DealMembersChangedEvent), Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent)
- Консюмеры (2): Consumer: Calendar CRM Deal Sync (calendar-crm-deal-events), Consumer: CRM Deal Cancelled
- Саги (3): Saga «Booking» (Резерв → Бронь → передача в продажу), Saga «Commission Settlement» (финансовый контур вознаграждений), Saga «Deal Completion» (fan-out завершения сделки)
- index.md (1): Calendar Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущность: deal.md (INV-DEAL-8/9, enum
DealStatus, поляCancelReason/CancelledAt); соседние контракты: deal-completed.md, deal-created.md, booking-cancelled.md.