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

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-контекстам факт смены стадии без синхронного запроса в crmmessenger рисует системную карточку в чате сделки, notifications шлёт уведомление участникам. Внешние консюмеры оперируют системной группой этапа (PipelineStepGroup), а не настраиваемым названием этапа застройщика: настраиваемые этапы — внутренний справочник CRM (pipeline-step.md), поэтому в payload несётся денормализованная группа.

Metadata

ПолеЗначение
Event typepin.crm.deal-stage-changed
Topicpin.crm.deal-stage-changed.v1
Partitions / key3 (низкообъёмная сущность, ADR-0056) / key = AggregateId = DealId (per-deal порядок переходов)
ContractVersion1
Producercrm: перемещение по этапам (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Описание
DealIdGuidДаDealEntity.IdImmutableКлюч агрегата (= Kafka key).
TenantIdGuidДаDealEntity.TenantIdImmutableDeveloperCompanyId застройщика (tenant boundary).
FromStepIdGuid?НетDealEntity.StepId (до перехода)Additive onlyПредыдущий настраиваемый этап; null, если этап не менялся (переход только по статусу).
ToStepIdGuidДаDealEntity.StepId (после)Additive onlyТекущий этап канбана. Сделка всегда лежит на этапе — поле обязательно.
FromStepGroupstring (enum PipelineStepGroup)НетPipelineStepEntity.Group прежнего этапаAdditive only (новые значения)Системная группа прежнего этапа; null, если группа не изменилась.
ToStepGroupstring (enum PipelineStepGroup)ДаDealEntity.StepGroup (денормализация)Additive only (новые значения)Системная группа текущего этапа — то, по чему ориентируются внешние консюмеры. Application/Booking/Contract/Payment/Registration/Completed/Cancelled.
FromStatusstring (enum DealStatus)ДаDealEntity.Status (до)Additive only (новые значения)Статус процесса до перехода. Может быть Draft (переход Draft → Application — вовлечение застройщика).
ToStatusstring (enum DealStatus)ДаDealEntity.Status (после)Additive only (новые значения)Статус процесса после перехода. Draft/Application/Booking/Sale/Completed/Cancelled.
ChangedByUserIdGuid?Нетactor операции (UpdatedByUserId)ImmutableКто инициировал переход (агент, менеджер застройщика, Менеджер ПИН); null — системный авто-переход (истечение брони, авто-переход по брони).
ChangedAtDateTimeOffsetДамомент перехода (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та же
StartSaleBooking → Sale (guard INV-DEAL-6: ContractType/PaymentMethod заданы)та же
Возврат менеджером после отмены/истечения брониBooking → Applicationта же
CompleteSale → 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/ToStatusMessageId + 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.
Fallback для неизвестного enum

Unknown enum (ToStepGroup/ToStatus/FromStepGroup/FromStatus) — сохранить строку как есть, залогировать warning, не падать (карточка/уведомление с «нейтральной» формулировкой).

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

PII

Событие несёт только идентификаторы, enum-значения и метку времени — прямого PII нет по построению: ФИО/телефоны участников намеренно исключены (консюмеры резолвят их из своих реплик identity), поэтому маскирование в самом payload не требуется.

ПолеКласс данныхПравило
DealId, TenantId, FromStepId, ToStepIdInternalinclude
FromStepGroup, ToStepGroup, FromStatus, ToStatusInternalinclude
ChangedByUserIdInternal (id пользователя, не PII)include; в логах — как id, без резолва ФИО
ChangedAtInternalinclude

Событие НЕ содержит secrets, tokens, raw credentials, ФИО/телефоны, номер эскроу-счёта (EscrowAccountNumber — Sensitive, живёт только в агрегате deal.md), платёжные реквизиты и unbounded-вложения. Доступ к топику — m2m ACL Kafka (messenger/notifications).

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

  • Логи (без PII): факт публикации deal-stage-changed с DealId/FromStepGroupToStepGroup/ FromStatusToStatus/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), Then deal-stage-changed с FromStatus = DraftToStatus = 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.

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

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

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