Consumer: Deal Chat Provisioning (MessengerDealCreatedConsumer)
Назначение
Consumer слушает событие pin.crm.deal-created.v1 и провизионирует чат сделки (ADR-0055): по факту
создания сделки в crm создаёт ChatEntity (Kind = Deal), наполняет его участниками из состава Members
(роли ParticipantRole, единая модель ADR-0071), публикует в ленту чата информационную карточку
DealCreated и возвращает crm готовность чата событием pin.messenger.deal-chat-status-changed.v1
(проекция Deal.ChatStatus = Ready). Провизия идемпотентна по DealId (INV-CHAT-2): повторная доставка не
плодит второй чат — возвращает существующий и повторно эмитит Ready.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | messenger |
| Consumer | MessengerDealCreatedConsumer |
| Обрабатываемое событие | DealCreatedEvent |
| Topic | pin.crm.deal-created.v1 (key = DealId, 3 партиции — дом deal-created.md) |
| ContractVersion | ожидается 1; при несовпадении версии — warn-and-proceed (messenger_deal_created_contract_mismatch_total) |
| Consumer group | messenger-deal-created |
| Producer | crm: создание сделки / одобрение фиксации |
| Эффект (событие → что меняется) | Создаётся чат сделки с участниками и карточкой DealCreated; в crm возвращается готовность чата |
Границы. Consumer владеет только провизией чата и его составом на момент создания; последующая
синхронизация участников — deal-members-changed.md; закрытие чата на терминале
сделки — deal-stage-changed.md. Consumer не пишет в crm напрямую — готовность
возвращается событием (service-boundaries).
Контекст события
- Из метаданных события доступны: идентификатор сообщения (для дедупликации), время события, версия контракта,
идентификатор и тип агрегата-источника (
DealId/Deal). - Актор — системный; чаты
messengerне tenant-фильтруются (доступ по членству, ADR-0052);DealTenantIdсохраняется на чате только как scope арбитража Службы заботы (см. chat.md). - Корреляция сквозного трейса восстанавливается из метаданных события.
- Ключ бизнес-идемпотентности:
messenger-deal-created:{MessageId}; на уровне данных — уникальность чата поDealId(INV-CHAT-2) + де-дуп карточки по (ChatId,CardType = DealCreated,SourceEventId) (INV-MSG-8).
Входные данные / payload события
Контракт — дом deal-created.md (payload + вложенная DealMemberSnapshot).
Ниже — только поля, которые consumer реально читает, и их маппинг.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
DealId | Guid | Да | Chat.DealId; ключ идемпотентности провизии |
TenantId | Guid | Да | Chat.DealTenantId (DeveloperCompanyId; scope арбитража) |
ProjectId | Guid | Да | Chat.ProjectId (навигация/группировка Deal-чатов) |
Title | string | Да | Chat.Title («Объект №…, ЖК …»; PII не содержит) |
Members | IReadOnlyList<DealMemberSnapshot> | Да | Состав чата: UserId → участник, Role → ParticipantRole по маппингу |
Роли ParticipantRole (единая модель ADR-0071, participant.md):
Investor→Investor, Agent→Agent, SellManager→DeveloperManager, PinManager→PinManager. Неизвестная
роль — сохранить строку как есть + warning (fallback, не падать). AgentUserId/InvestorUserId/Status/
PaymentMethod/PropertyObjectId/StepId consumer не читает (на провизию чата не влияют; отображаемые ФИО
резолвятся из реплики messenger-user.md).
Алгоритм
- Контекст события. Восстановить метаданные события; сверить версию контракта с
1(при несовпадении — warn-and-proceed). Проверить бизнес-идемпотентность поmessenger-deal-created:{MessageId}: событие уже обработано → пропустить как дубль (skipped-duplicate, ack). - Проверка входа.
DealIdзадан;Titleнепустой (≤ 256, при превышении — усечь + warning);Membersсодержит ≥ 1 активного участника, в т.ч. ровно одинInvestorи одинAgent(INV-CHAT-7); нарушение структуры →MSG_DEAL_MEMBERS_INVALID(DLQ + алерт). - Что читаем. Найти чат сделки по
DealId(уникальность гарантирована, INV-CHAT-2). Если чат уже есть — провизия завершена ранее: перейти к шагу возврата готовности (re-emitReady), тело чата не мутируется (идемпотентность INV-CHAT-2). - Бизнес-правила и маппинг. Собрать состав: для каждого
DealMemberSnapshot—ParticipantRoleпо маппингу; отфильтровать дублиUserId(последняя роль побеждает). Сформировать карточкуDealCreated:CardType = DealCreated,Category = Info,ActionStatus = None,Payload = CardDealRefPayload { DealId },SourceEventId = MessageId, текст — «Сделка создана». Карточка видна всем (INV-MSG-5). - Что меняется. Создаётся чат сделки (
Kind = Deal), добавляются участники (INV-PART-1), в ленту публикуется карточкаDealCreated(Seqприсваивается монотонно, денормализуется last-message;SenderUserIdпустой,IsSystem = true, INV-MSG-4). - Что фиксируется. В системную ленту чата материализуются факты
DealChatProvisionedиParticipantJoined[](chat-event.md); участникам готовится realtime-доставкаChatUpdated/MessageReceived(persist-then-push). Наружу публикуется интеграционное событиеDealChatStatusChangedEvent { DealId, ChatId, Status = Ready, OccurredAt }(topicpin.messenger.deal-chat-status-changed.v1). ОтдельногоMessagePostedEventдля карточки нет: offline-push об этапах шлётnotificationsнапрямую из событийcrm(без двойного пуша). - Согласованность. Чат, участники, карточка, факты ленты, отметка идемпотентности и исходящее событие готовности фиксируются в одном согласованном изменении; offset подтверждается только после фиксации.
- Результат. Успех → ack (SLA
Provisioning → Ready< 5 мин). Инфраструктурный сбой → ретрай (backoff); падение провизии после исчерпания ретраев — воркерchat-provision-retryпомечаетFailedи повторно эмититdeal-chat-status-changed(Status = Failed) для проекцииcrm.
Диаграмма
Предусловия и постусловия
До обработки (истинно на входе):
- Сделка создана/фиксация одобрена в
crm— событиеdeal-createdопубликовано с полным составомMembersна момент создания. С точки зренияcrmпроекцияDeal.ChatStatus = Provisioning(ждёт готовности чата). - Чата по
DealIdещё нет (первичная провизия) или он уже создан (повторная доставка/replay — идемпотентно).
После успешной обработки (гарантируется):
- Существует ровно один
ChatEntity(Kind = Deal) поDealId(INV-CHAT-2); его состав — активные участники изMembers(роли по маппингу, дублиUserIdсхлопнуты, INV-CHAT-7: ≥1Investor+ ≥1Agent). - В ленте — карточка
DealCreated(Info,None) и системные фактыDealChatProvisioned+ParticipantJoined[]. - Наружу ушло ровно одно
DealChatStatusChangedEvent {Status = Ready}(pin.messenger.deal-chat-status-changed.v1, ключDealId); по немуcrmпереводит проекциюDeal.ChatStatus: Provisioning → Ready. - Всё выше — чат, участники, карточка, факты ленты, отметка идемпотентности, исходящее событие — в одном согласованном изменении; offset подтверждён после фиксации.
Промежуточное состояние (окно eventual-consistency): пока consumer не отработал, crm видит
Deal.ChatStatus = Provisioning, а пользователь — сделку без доступного чата (кнопка чата неактивна/спиннер).
Окно — обычно секунды; целевой SLA Provisioning → Ready < 5 мин. При падении провизии сверх ретраев проекция
станет Failed (ниже), и UI покажет ошибку провизии, а не вечный спиннер.
Кросс-сервисные контракты
Ожидается от crm (вход pin.crm.deal-created.v1): at-least-once доставка; ключ DealId (порядок
per-deal — deal-created предшествует deal-members-changed/deal-stage-changed); обязательные поля DealId,
TenantId, ProjectId, Title, Members (список DealMemberSnapshot с UserId + Role), MessageId,
ContractVersion. Гарантия продьюсера: состав содержит валидную пару Investor+Agent; Title без PII.
Предоставляет наружу — исходящее событие DealChatStatusChangedEvent:
| Поле | Тип | Смысл |
|---|---|---|
DealId | Guid | Ключ партиционирования (pin.messenger.deal-chat-status-changed.v1) и корреляции с проекцией crm |
ChatId | Guid | Идентификатор провизионированного чата |
Status | string (Ready/Failed) | Готовность чата для проекции Deal.ChatStatus |
OccurredAt | DateTimeOffset | Момент фиксации готовности/отказа |
Гарантии: at-least-once (потребитель crm идемпотентен — Ready/Failed можно применять повторно); порядок
per-deal по DealId; messenger не пишет в crm напрямую (готовность только событием — service-boundaries).
ФИО/телефоны в событии отсутствуют (только Guid).
Критерии приёмки (Given/When/Then)
- Happy (первичная провизия): Given нет чата по
DealIdи валидноеdeal-created(Investor+Agent вMembers); When обработка; Then создан один Deal-чат с участниками, в ленте карточкаDealCreated+ фактыDealChatProvisioned/ParticipantJoined[], наружу одноdeal-chat-status-changed = Ready, offset подтверждён. - Идемпотентная повторная доставка: Given чат по
DealIdуже есть; When повтор события/replay; Then тело чата не мутируется (no-op),Readyпереэмитится (crm-проекция идемпотентна), ack. - Невалидный состав: Given
MembersбезInvestorили безAgent(нарушение INV-CHAT-7); When обработка; ThenMSG_DEAL_MEMBERS_INVALID, DLQ + алерт, чат не создаётся. - Гонка уникальности: Given две параллельные доставки одного
DealId; When обе пытаются создать чат; Then выигрывает одна (INV-CHAT-2), проигравшая перечитывает → no-op →Ready; второго чата не возникает. - Неизвестная роль: Given
DealMemberSnapshot.Roleвне enum; When маппинг; Then строка сохраняется как есть- warning (fallback, не отказ).
- Длинный
Title: GivenTitle> 256; When обработка; Then усечение + warning, провизия успешна. - Провизия падает сверх лимита: Given устойчивый инфраструктурный сбой; When ретраи исчерпаны; Then воркер
chat-provision-retryпомечаетFailedи эмититdeal-chat-status-changed = Failed; UIcrmпоказывает отказ. - Иная версия контракта: Given
ContractVersion ≠ 1; When обработка; Then warn-and-proceed.
Идемпотентность и replay
- По сообщению: ключ
messenger-deal-created:{MessageId}. - По данным: уникальность чата по
DealId(INV-CHAT-2) — второй чат сделки невозможен; карточка де-дуплится по (ChatId,CardType,SourceEventId) (INV-MSG-8). - Replay: да. Повторный re-read топика безопасен: провизия no-op на существующем чате,
Readyпереэмитится (crm-проекция идемпотентна). Runbook — «пересоздание Deal-чатов» (runbooks/, будущий путь).
Retry и DLQ
| Сбой | Код | Retry | DLQ | Ручное действие |
|---|---|---|---|---|
| Инфраструктурный (PostgreSQL/Kafka недоступны) | — | Бесконечный backoff 1s→60s | Нет | Runbook: восстановить зависимость |
Гонка уникальности DealId (параллельная доставка) | — | 1 повтор (перечитать → no-op) | Нет | — (идемпотентно) |
Невалидный состав Members (нет Investor/Agent) | MSG_DEAL_MEMBERS_INVALID | Нет | Да + алерт | Проверить продьюсер crm |
| Провизия падает > N циклов | — | воркер chat-provision-retry | Нет | Эскалация: deal-chat-status-changed = Failed |
| ContractVersion mismatch | — | warn-and-proceed | Нет | Проверить версию контракта producer |
Наблюдаемость и безопасность
- Logs:
consumed/skipped-duplicate/provisioned(ChatId,members) /already-provisioned/failedс полямиMessageId,DealId,ChatId. PII в логах нет (только id). - Metrics:
messenger_deal_created_consume_duration_ms,messenger_deal_chats_provisioned_total,messenger_deal_created_dlq_total,messenger_deal_created_lag_seconds. - Traces: приём события → обработка → обращения к хранилищу; корреляция из метаданных события.
- Alerts: лаг > 5 мин; провизия
Failed> 10/ч; DLQ > 0. - Безопасность: системный актор, доступ по членству (санкционированный кросс-tenant — чат связывает
компании); событие не несёт ФИО/телефонов (только
Guid);DealTenantId— scope арбитража, не фильтр строк.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-MSG-002. Авто-провизия чата сделки по событию CRM, UC-MSG-008. Добавленный участник появляется в чате сделки/лида
- События (3): События сервиса messenger: реестр топиков, Event Contract: pin.crm.deal-created (DealCreatedEvent), Event Contract: pin.messenger.deal-chat-status-changed (DealChatStatusChangedEvent)
- Консюмеры (6): Consumer: Booking Expired Card (MessengerBookingExpiredConsumer), Consumer: Booking Paid Card (MessengerBookingPaidConsumer), Consumer: Deal Chat Members Sync (MessengerDealMembersChangedConsumer), Consumer: Deal Stage Card (MessengerDealStageChangedConsumer), Consumer: Document Request Card (MessengerDocumentRequestedConsumer), Consumer: Lead Chat Participants Sync (MessengerLeadParticipantsChangedConsumer)
- Фоновые задачи (1): Background Job: ChatProvisionRetryJob
Связанные документы
- Целевые агрегаты: chat.md (INV-CHAT-2/7,
DealChatStatusChangedEvent), participant.md (маппинг ролей), message.md (EventCardType.DealCreated,CardDealRefPayload), chat-event.md (DealChatProvisioned/ParticipantJoined). - Входное событие (дом): deal-created.md; исходящее — deal-chat-status-changed.md.
- Соседние консюмеры: deal-members-changed.md, deal-stage-changed.md.
- README контекста; ADR-0055, 0052, 0056.