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

Consumer: Deal Stage Card (MessengerDealStageChangedConsumer)

Назначение

Consumer слушает событие pin.crm.deal-stage-changed.v1 и рисует в чат сделки информационную карточку «этап изменён» (StageChanged, Category = Info) при каждом переходе From → To, а для отдельных групп этапа — целевые/специальные карточки (например SentToRosreestr для группы Registration). На терминальном ToStatus ∈ {Completed, Cancelled} дополнительно закрывает чат (Status → Closed, INV-CHAT-8) — новые пользовательские сообщения запрещены, системные карточки допустимы. Порядок переходов per-deal гарантирован ключом DealId.

Owning-сервис и триггер

ПолеЗначение
Сервис/контекстmessenger
ConsumerMessengerDealStageChangedConsumer
Обрабатываемое событиеDealStageChangedEvent
Topicpin.crm.deal-stage-changed.v1 (key = DealId, 3 партиции)
ContractVersionожидается 1; при несовпадении версии — warn-and-proceed (messenger_deal_stage_contract_mismatch_total)
Consumer groupmessenger-deal-stage-changed
Producercrm: переход этапа сделки / старт продажи / завершение / отмена / авто-переход брони
Эффект (событие → что меняется)В ленту чата добавляются карточки этапа; на терминале сделки чат закрывается

Контекст события

  • Из метаданных события доступны: идентификатор сообщения (для дедупликации), время события, версия контракта, идентификатор и тип агрегата-источника (DealId / Deal).
  • Актор — системный; чат не tenant-фильтруется, доступ по членству (ADR-0052).
  • Корреляция сквозного трейса восстанавливается из метаданных события.
  • Ключ бизнес-идемпотентности: messenger-deal-stage:{MessageId}; на уровне данных — уникальность карточки по (ChatId, CardType, SourceEventId = MessageId) (INV-MSG-8); закрытие чата идемпотентно (повторное закрытие уже Closed — no-op).

Входные данные / payload события

Контракт — дом deal-stage-changed.md. Ниже — читаемые поля.

Поле payloadТипОбязательностьКак используется (маппинг)
DealIdGuidДаПоиск чата сделки по DealId; CardStageChangedPayload.DealId
ToStepGroupstring (PipelineStepGroup)ДаВетвление спец-карточек (Registration → SentToRosreestr)
FromStatusstring (DealStatus)ДаCardStageChangedPayload.FromStatus
ToStatusstring (DealStatus)ДаCardStageChangedPayload.ToStatus; терминал {Completed, Cancelled} → закрытие чата
ChangedAtDateTimeOffsetДамомент отправки карточки / ChatEvent.OccurredAt

ToStepName карточки = null: настраиваемое имя этапа — внутренний справочник CRM, в событие не входит (несётся только группа). FromStepId/ToStepId/FromStepGroup/ChangedByUserId consumer не читает (на карточку не влияют). Неизвестный ToStatus/ToStepGroup — нейтральная формулировка + warning (fallback).

Алгоритм

  1. Контекст события. Восстановить метаданные события; сверить версию контракта с 1 (при несовпадении — warn-and-proceed). Проверить бизнес-идемпотентность по messenger-deal-stage:{MessageId}: событие уже обработано → пропустить как дубль (skipped-duplicate, ack).
  2. Проверка входа. DealId задан; ToStatus/ToStepGroup непустые. Структурно невалидный payload → MSG_STAGE_PAYLOAD_INVALID (DLQ + алерт).
  3. Что читаем. Найти чат сделки по DealId. Если чата ещё нет — он не провизионирован (событие пришло раньше deal-created): отказ MSG_CHAT_NOT_PROVISIONED с ретраем (backoff, догонит по DealId).
  4. Бизнес-правила и маппинг. Определить набор карточек:
    • всегда — StageChanged (Category = Info, ActionStatus = None, Payload = CardStageChangedPayload { DealId, FromStatus, ToStatus, ToStepName = null });
    • если ToStepGroup == Registration — дополнительно SentToRosreestr (Category = Info, Payload = CardDealRefPayload { DealId });
    • если ToStatus ∈ {Completed, Cancelled} — пометить чат к закрытию (reason = ToStatus, INV-CHAT-8). Каждой карточке SourceEventId = MessageId, текст — по CardType/ToStatus. Видимость — все участники (INV-MSG-5).
  5. Что меняется. В ленту чата добавляется каждая карточка (Seq присваивается монотонно, денормализуется last-message, IsSystem = true, INV-MSG-4). При терминале чат переводится в Closed.
  6. Что фиксируется. По закрытию в системную ленту материализуется факт ChatClosed (chat-event.md); участникам готовится realtime-доставка MessageReceived/ChatUpdated (persist-then-push). Интеграционных Kafka-событий нет: offline-push шлёт notifications напрямую из deal-stage-changed (без двойного пуша).
  7. Согласованность. Карточки, смена статуса чата, факт ленты и отметка идемпотентности фиксируются в одном согласованном изменении; offset подтверждается только после фиксации.
  8. Результат. Успех → ack (SLA < 5 мин). MSG_CHAT_NOT_PROVISIONED/инфраструктурный сбой → ретрай (backoff); структурная ошибка → DLQ + алерт.

Диаграмма

Предусловия и постусловия

До обработки (истинно на входе):

  • Событие deal-stage-changed опубликовано crm при переходе этапа сделки From → To (или терминале Completed/Cancelled); messenger переход не переисчисляет, а отражает.
  • Deal-чат по DealId, как правило, уже провизионирован (deal-created); если ещё нет — допустимая гонка, разрешается ретраем (шаг 3).
  • Порядок переходов per-deal сохранён ключом партиции DealId — карточки лягут в ленту в порядке этапов.

После успешной обработки (гарантируется):

  • В ленте чата ровно одна карточка StageChanged для данного MessageId; при ToStepGroup = Registration — дополнительно ровно одна SentToRosreestr; повтор доставки новых карточек не создаёт (INV-MSG-8).
  • last-message чата денормализован на последнюю добавленную карточку; Seq монотонно возрос.
  • При ToStatus ∈ {Completed, Cancelled} чат в статусе Closed (INV-CHAT-8); повторное закрытие — no-op; факт ChatClosed материализован в системную ленту (chat-event.md).
  • Онлайн-участникам ушла realtime-доставка MessageReceived/ChatUpdated; оффлайн — при подключении (persist-then-push).
  • Наружу не ушло ни одного Kafka-события; offset подтверждён только после фиксации.

Кросс-сервисные контракты

Ожидается от crm (вход pin.crm.deal-stage-changed.v1): at-least-once доставка; ключ партиционирования DealId (порядок этапов per-deal); обязательные поля DealId, FromStatus, ToStatus, ToStepGroup, ChangedAt, MessageId (дедуп), ContractVersion. Гарантия продьюсера: DealId указывает на реальную сделку; ToStatus/ToStepGroup — enum-значения (неизвестные допустимы — нейтральный текст, не отказ); настраиваемое имя этапа (ToStepName) в событие не входит.

Ожидается от deal-created: чат по DealId провизионирован (eventual; окно — обычно секунды, при лаге до нескольких минут; пока нет — MSG_CHAT_NOT_PROVISIONED + backoff-ретрай).

Предоставляет наружу: ничего — исходящих Kafka-событий нет. Offline-push шлёт notifications напрямую из deal-stage-changed; освобождение объекта и прочие эффекты терминала слушают события crm напрямую, не транзитом через messenger (нет двойного пуша). Единственный эффект вовне контекста — realtime-доставка карточек участникам внутри messenger.

Особый случай — событие на уже Closed-чат (поздний/повторный терминал, поздний рядовой переход): системные карточки на Closed-чат допустимы (INV-CHAT-8) — карточка постится, закрытие — no-op.

Критерии приёмки (Given/When/Then)

  • Happy (промежуточный этап): Given провизионированный Deal-чат и deal-stage-changed с не терминальным ToStatus; When consumer обрабатывает событие; Then в ленте карточка StageChanged (Info, ActionStatus = None) с текстом перехода, участникам MessageReceived, чат остаётся Active, Kafka молчит.
  • Регистрация: Given ToStepGroup = Registration; When обработка; Then помимо StageChanged добавляется SentToRosreestr; обе карточки де-дуплятся по SourceEventId.
  • Терминал: Given ToStatus ∈ {Completed, Cancelled}; When обработка; Then карточка StageChanged добавлена, чат переведён в Closed, факт ChatClosed в системной ленте, участникам ChatUpdated.
  • Дубль: Given уже обработанный MessageId; When повторная доставка; Then новых карточек нет (skipped-duplicate), закрытие no-op, ack.
  • Чат не готов: Given чат по DealId не провизионирован; When обработка; Then MSG_CHAT_NOT_PROVISIONED, ретрай с backoff, после лимита — DLQ; карточки не постятся.
  • Событие на Closed-чат: Given чат уже Closed; When приходит следующий stage-эвент; Then системная карточка добавляется (INV-CHAT-8), закрытие no-op.
  • Неизвестный статус/группа: Given ToStatus/ToStepGroup вне enum; When обработка; Then нейтральная карточка StageChanged + warning, спец-карточка не добавляется, не отказ.
  • Битый payload: Given структурно невалидное событие; When обработка; Then MSG_STAGE_PAYLOAD_INVALID, DLQ + алерт, без ретрая.
  • Иная версия контракта: Given ContractVersion ≠ 1; When обработка; Then warn-and-proceed (метрика mismatch), обработка продолжается.

Идемпотентность и replay

  • По сообщению: ключ messenger-deal-stage:{MessageId}.
  • По данным: уникальность (ChatId, CardType, SourceEventId) де-дуплит каждую карточку (INV-MSG-8); закрытие чата идемпотентно (повтор на Closed — no-op).
  • Replay: да. Повторный re-read безопасен: карточки не дублируются, закрытие no-op. Порядок переходов per-deal гарантирован ключом DealId.

Retry и DLQ

СбойКодRetryDLQРучное действие
Чат ещё не провизионированMSG_CHAT_NOT_PROVISIONEDДа, backoff (догоняет deal-created)После лимитаПроверить лаг messenger-deal-created
Инфраструктурный (PostgreSQL/Kafka)Бесконечный backoff 1s→60sНетRunbook: восстановить зависимость
Структурная ошибка payloadMSG_STAGE_PAYLOAD_INVALIDНетДа + алертПроверить продьюсер crm
ContractVersion mismatchwarn-and-proceedНетПроверить версию контракта

Наблюдаемость и безопасность

  • Logs: consumed / skipped-duplicate / card-posted(CardType) / chat-closed / retry-not-provisioned / failed c MessageId, DealId, ChatId, ToStatus. PII нет.
  • Metrics: messenger_deal_stage_consume_duration_ms, messenger_stage_cards_posted_total{card_type}, messenger_deal_chats_closed_total, messenger_deal_stage_dlq_total, messenger_deal_stage_lag_seconds.
  • Traces: приём события → обработка → обращения к хранилищу; корреляция из метаданных события.
  • Alerts: лаг > 5 мин; всплеск MSG_CHAT_NOT_PROVISIONED (рассинхрон с deal-created); DLQ > 0.
  • Безопасность: системный актор, доступ по членству; событие — только id/enum/timestamp (PII нет).

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

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

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