Event Contract: pin.crm.deal-stage-changed (DealStageChangedEvent)
Версионируемый контракт интеграционного события «этап/статус сделки изменён». Consumer-документы ссылаются
сюда, не переописывая payload. Единое событие для двух триггеров одного агрегата
(deal.md): перемещение карточки на канбан-доске (move-step, drag-n-drop) и смена
статуса сделки. Порядок переходов per-deal гарантирован Kafka-ключом (AggregateId = DealId).
Назначение
Фиксирует переход сделки From → To по канбан-этапам и/или по бизнес-статусу. Публичное: даёт
downstream-контекстам факт смены стадии без синхронного запроса в crm — messenger рисует системную
карточку в чате сделки, notifications шлёт уведомление участникам. Внешние консюмеры оперируют системной
группой этапа (PipelineStepGroup), а не настраиваемым названием этапа застройщика: настраиваемые этапы —
внутренний справочник CRM (pipeline-step.md), поэтому в payload несётся
денормализованная группа.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.crm.deal-stage-changed |
| Topic | pin.crm.deal-stage-changed.v1 |
| Partitions / key | 3 (низкообъёмная сущность, ADR-0056) / key = AggregateId = DealId (per-deal порядок переходов) |
| ContractVersion | 1 |
| Producer | crm: перемещение по этапам (MoveToStep, drag-n-drop, INV-DEAL-5), вовлечение застройщика Draft → Application (engage-developer.md, INV-DEAL-11), смена статуса (StartSale/Complete/Cancel), авто-переход Application → Booking при создании брони, истечение брони |
| Статус | draft |
Событие публикуется атомарно с мутацией агрегата и записью таймлайна: пока переход не зафиксирован,
событие не отправляется; после фиксации — доставляется гарантированно (at-least-once), ключ = DealId.
Payload DealStageChangedEvent
Дом полей — entity: DealEntity (раздел «Доменные и интеграционные события») и
PipelineStepEntity / enum PipelineStepGroup; здесь перечислено только то,
что реально в событии. Все поля — скалярные или enum-строки; вложенных моделей нет.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
DealId | Guid | Да | DealEntity.Id | Immutable | Ключ агрегата (= Kafka key). |
TenantId | Guid | Да | DealEntity.TenantId | Immutable | DeveloperCompanyId застройщика (tenant boundary). |
FromStepId | Guid? | Нет | DealEntity.StepId (до перехода) | Additive only | Предыдущий настраиваемый этап; null, если этап не менялся (переход только по статусу). |
ToStepId | Guid | Да | DealEntity.StepId (после) | Additive only | Текущий этап канбана. Сделка всегда лежит на этапе — поле обязательно. |
FromStepGroup | string (enum PipelineStepGroup) | Нет | PipelineStepEntity.Group прежнего этапа | Additive only (новые значения) | Системная группа прежнего этапа; null, если группа не изменилась. |
ToStepGroup | string (enum PipelineStepGroup) | Да | DealEntity.StepGroup (денормализация) | Additive only (новые значения) | Системная группа текущего этапа — то, по чему ориентируются внешние консюмеры. Application/Booking/Contract/Payment/Registration/Completed/Cancelled. |
FromStatus | string (enum DealStatus) | Да | DealEntity.Status (до) | Additive only (новые значения) | Статус процесса до перехода. Может быть Draft (переход Draft → Application — вовлечение застройщика). |
ToStatus | string (enum DealStatus) | Да | DealEntity.Status (после) | Additive only (новые значения) | Статус процесса после перехода. Draft/Application/Booking/Sale/Completed/Cancelled. |
ChangedByUserId | Guid? | Нет | actor операции (UpdatedByUserId) | Immutable | Кто инициировал переход (агент, менеджер застройщика, Менеджер ПИН); null — системный авто-переход (истечение брони, авто-переход по брони). |
ChangedAt | DateTimeOffset | Да | момент перехода (DealEntity.UpdatedAt) | Immutable | Время смены этапа/статуса. |
Enum-значения не дублируются — дом DealStatus
(deal.md → «Enum DealStatus») и PipelineStepGroup
(pipeline-step.md → «SEED enum PipelineStepGroup»). Настраиваемое имя этапа
(PipelineStepEntity.Name), цвет и CancelReason в payload не входят: имя/цвет — внутренний справочник
CRM (внешним консюмерам нужна лишь группа), причина отказа несётся отдельным событием
deal-cancelled.md.
Когда публикуется
Публикуется при каждом изменении StepId и/или Status сделки в одном согласованном изменении с
мутацией агрегата и записью таймлайна (DealStatusChanged/DealMovedToStep).
| Условие | Переход состояния | Публикация |
|---|---|---|
Вовлечение застройщика (engage-developer, гейт INV-DEAL-11: подтверждённое намерение + одобренная фиксация + PaymentMethod) | Draft → Application (в состав добавлен SellManager; параллельно — deal-members-changed.md) | атомарно со сменой состояния агрегата |
MoveToStep (drag-n-drop) внутри одной группы | StepId: From → To (группа/статус не меняются) | атомарно со сменой состояния агрегата |
MoveToStep в этап другой группы | StepId + StepGroup + Status: From → To (INV-DEAL-5; для группы Cancelled требуется CancelReason) | та же |
| Создание брони (авто-переход) | Application → Booking | та же |
StartSale | Booking → Sale (guard INV-DEAL-6: ContractType/PaymentMethod заданы) | та же |
| Возврат менеджером после отмены/истечения брони | Booking → Application | та же |
Complete | Sale → Completed (актор — менеджер застройщика; INV-DEAL-7) | та же |
Cancel | * → Cancelled (INV-DEAL-8; параллельно — событие deal-cancelled.md) | та же |
Создание сделки публикуется отдельным событием deal-created.md, завершение — deal-completed.md; это событие несёт только сам переход, без полного snapshot сделки.
Потребители (consumers)
| Consumer-контекст | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
messenger | Системная карточка «этап сделки изменён» в чате сделки (по DealId); отображает ToStepGroup/ToStatus | MessageId + per-deal порядок по ключу | < 5 мин |
notifications | Уведомление участникам сделки о смене стадии (каналы получателя — своя реплика identity) | MessageId | < 5 мин |
Оба консюмера идемпотентны по MessageId; порядок переходов одной сделки гарантирован общим ключом DealId
(одна партиция). Отображаемые имена участников консюмеры берут из собственных реплик пользователей
(pin.identity.users.v1), не из этого события.
Доставка, порядок и обработка сбоев
- Гарантия публикации. Событие уходит только после фиксации перехода (
StepIdи/илиStatus) и записи таймлайна; при откате не публикуется. Доставка — at-least-once, ключDealId. - Порядок. Переходы одной сделки строго упорядочены ключом
DealId(одна партиция) — потребитель видит их в фактической последовательностиFrom → To. - Идемпотентность потребителя.
messenger/notificationsдедуплицируют поMessageId; повторная доставка не рисует вторую карточку/уведомление. - Согласованность с соседними топиками. Отмена (
* → Cancelled) параллельно публикуетdeal-cancelled; авто-переходApplication → Bookingсогласован сbooking-created; вовлечение застройщикаDraft → Applicationсогласовано сdeal-members-changed(добавлениеSellManager) — оба в одном изменении, но в разных топиках (взаимный порядок между топиками не гарантирован, потребители обрабатывают независимо). - Retry. Транзиентный сбой потребителя → повтор обработки с backoff; событие доступно до успешной обработки.
- DLQ. Неустранимый сбой после исчерпания попыток → dead-letter c алертом; проигрывание безопасно
(идемпотентно по
MessageId).
Переход Draft → Application — момент, с которого сделка становится видимой застройщику: до него сделка
живёт как черновик агент+инвестор без SellManager (developer-facing проекции фильтруют Status != Draft),
после — попадает на канбан застройщика.
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии контракта потребитель логирует предупреждение и продолжает обработку. Топик кодирует версию по конвенции суффиксом.v1.- Additive-only для minor: новые optional-поля с безопасным default — без bump. Новые значения enum
DealStatus/PipelineStepGroup— additive. - Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2+ новый топикpin.crm.deal-stage-changed.v2+ dual-publish на период миграции + deadline миграции консюмеров + sunset.v1.
Unknown enum (ToStepGroup/ToStatus/FromStepGroup/FromStatus) — сохранить строку как есть,
залогировать warning, не падать (карточка/уведомление с «нейтральной» формулировкой).
Безопасность
Событие несёт только идентификаторы, enum-значения и метку времени — прямого PII нет по построению:
ФИО/телефоны участников намеренно исключены (консюмеры резолвят их из своих реплик identity), поэтому
маскирование в самом payload не требуется.
| Поле | Класс данных | Правило |
|---|---|---|
DealId, TenantId, FromStepId, ToStepId | Internal | include |
FromStepGroup, ToStepGroup, FromStatus, ToStatus | Internal | include |
ChangedByUserId | Internal (id пользователя, не PII) | include; в логах — как id, без резолва ФИО |
ChangedAt | Internal | include |
Событие НЕ содержит secrets, tokens, raw credentials, ФИО/телефоны, номер эскроу-счёта
(EscrowAccountNumber — Sensitive, живёт только в агрегате deal.md), платёжные
реквизиты и unbounded-вложения. Доступ к топику — m2m ACL Kafka (messenger/notifications).
Наблюдаемость
- Логи (без PII): факт публикации
deal-stage-changedсDealId/FromStepGroup→ToStepGroup/FromStatus→ToStatus/ChangedByUserId/ChangedAt(идентификаторы и enum, не PII). - Метрики: число переходов по топику в разрезе
ToStepGroup/ToStatus; сквозная задержка доmessenger/notifications(< 5 мин); глубина DLQ (порог алерта > 0); доля повторных доставок. - Доступ: топик — m2m ACL Kafka (
messenger/notifications);EscrowAccountNumber, имя/цвет этапа иCancelReasonв событие не входят.
Acceptance Criteria
- Given сделка на этапе группы X, When
MoveToStepвнутри той же группы (drag-n-drop), Then ровно одноdeal-stage-changedс изменённымFromStepId/ToStepId;ToStepGroup/ToStatusне меняются. - Given сделка
Draft, гейт INV-DEAL-11 выполнен, When агент вовлекает застройщика (engage-developer), Thendeal-stage-changedсFromStatus = Draft→ToStatus = Application(ToStepGroup = Application); параллельно публикуетсяdeal-members-changedс добавленнымSellManager; с этого момента сделка видна застройщику. - Given перемещение в этап другой группы, When переход фиксируется (INV-DEAL-5), Then событие
несёт новые
ToStepGroupиToStatus; для группыCancelledтребуетсяCancelReason. - Given
StartSaleизBooking, When заданыContractType/PaymentMethod(guard INV-DEAL-6), Then событие сBooking → Sale. - Given отмена сделки, When
* → Cancelled, Then параллельно сdeal-stage-changedпубликуетсяdeal-cancelled(причина — там). - Given повторная/устаревшая доставка, When потребитель обрабатывает, Then карточка/уведомление
не задваиваются (идемпотентность по
MessageId, порядок по ключу). - Given неизвестное значение
ToStepGroup/ToStatus, When потребитель обрабатывает, Then нейтральная формулировка карточки/уведомления + warning (без падения). - Given breaking-изменение payload, When новый контракт, Then
pin.crm.deal-stage-changed.v2+ dual-publish + sunset.v1.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (23): GET /api/developer/crm/deals — Сделки застройщика (список/поиск), GET /api/developer/crm/deals/{dealId} — Карточка сделки (застройщик), GET /api/developer/crm/pipeline-steps — Этапы воронки застройщика, GET /api/investor/crm/deals/{dealId} — Карточка сделки (инвестор), GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), GET /api/pin-manager/crm/deals/{dealId} — Карточка любой сделки (Менеджер ПИН), POST /api/agent/crm/deals — Создать сделку из лида по объекту, POST /api/agent/crm/deals/{dealId}/bookings — Создать бронь (Резерв) от имени клиента, POST /api/agent/crm/deals/{dealId}/engage-developer — Вовлечь застройщика (Draft → Application), POST /api/developer/crm/deals/{dealId}/contracts-prepared — Отметить «Договоры подготовлены», POST /api/developer/crm/deals/{dealId}/rosreestr — Подтвердить регистрацию Росреестра, POST /api/developer/crm/pipeline-steps — Создать этап (+11)
- Use Cases (7): UC-BKG-002. Бронирование Резерв → Бронь, UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-003. Движение сделки по канбан-доске, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, UC-CRM-006. Вмешательство Менеджера ПИН, UC-CRM-017. Flow B: агент предлагает объект → Draft-сделка → подтверждение инвестором → вовлечение застройщика, UC-CRM-018. Несколько сделок из одной одобренной фиксации (по разным объектам застройщика)
- События (2): Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.crm.deal-members-changed (DealMembersChangedEvent)
- Консюмеры (1): Consumer: Deal Stage Card (MessengerDealStageChangedConsumer)
- scenarios (1): UC-OVR-006. Сделка: от брони до регистрации (инвестор-инициатор)
- registries (1): Реестр событий PIN
Связанные документы
- Сущности: deal.md (
StepId/StepGroup/Status,MoveToStep, инварианты INV-DEAL-5..9), pipeline-step.md (enumPipelineStepGroup, маппинг наDealStatus). - Соседние контракты топиков CRM: deal-created.md, deal-completed.md, deal-cancelled.md, deal-members-changed.md.
- Карта событий/консюмеров контекста: README.md (раздел «Events И Consumers»).