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

UC-MSG-002. Авто-провизия чата сделки по событию CRM

Назначение

Системный (событийный) сценарий: при создании сделки crm публикует pin.crm.deal-created.v1; консюмер messenger (deal-created) авто-создаёт групповой чат сделки (chat.md, Kind = Deal), зеркалит начальный состав участников (participant.md), пишет системную ленту (chat-event.md) и возвращает готовность событием pin.messenger.deal-chat-status-changed.v1 (Ready) → проекция Deal.ChatStatus в crm. Оркестрация чат↔сделка — через события CRM (ADR-0055); messenger — консюмер, владельцем жизненного цикла сделки не является. При сбое строка провизии остаётся Failed/Provisioning и добирается фоновым chat-provision-retry.

Metadata

ПолеЗначение
IDUC-MSG-002
Контекстmessenger
Типосновной (+ A1 повторная доставка/дубль, A2 сбой провизии → retry, A3 последующая реконсиляция состава, A4 событие сделки раньше провизии)
Статусdraft

Актор

ПолеЗначение
РольSystem (Kafka-консюмер провизии Deal-чата / фоновый worker)
Permissions / scopesystem-scope; доступ к Kafka-топику pin.crm.* (доверенный consumer)
Tenantне применяется — доступ по членству; чат несёт DealTenantId = DeveloperCompanyId только для scope арбитража (ADR-0052)
Record-levelне применяется (система); состав чата = активные DealMember (INV-CHAT-6)

Предусловие

  • Топик pin.crm.deal-created.v1 доступен; событие несёт DealId, Title/объект/ЖК, ProjectId, DeveloperCompanyId и начальный состав (Members: { UserId, ParticipantRole }).
  • Реплики MessengerUserEntity участников наполняются консюмером pin.identity.users.v1 (display-имена резолвятся на чтении; провизия от их наличия не зависит).

Триггер

ПолеЗначение
ИсточникKafka event pin.crm.deal-created.v1
Триггерящий элементконсюмер провизии Deal-чата + reliability-строка messenger.deal_chat_provisions

Основной Поток

Дом алгоритма/маппинга — consumers/deal-created.md; ниже — поток на уровне use-case.

  1. Приём события. Консюмер получает событие deal-created; бизнес-идемпотентность — по DealId.
  2. Идемпотентность/дубль. По DealId найти строку провизии — есть с Status = Ready (A1) → skip (повторная доставка Kafka не плодит чат, INV-CHAT-2). Иначе создать/взять строку намерения.
  3. Запись намерения + провизия. Создать Deal-чат (Chat.ProvisionDeal: Kind = Deal, Active, под ux_chats_deal_id, INV-CHAT-2); для каждого участника события — добавить участника с маппингом роли (SellManager→DeveloperManager и т.д., participant.md), идемпотентно (INV-PART-1); поднимается событие ChatCreated.
  4. Системная лента. По ChatCreated пишется ChatEventEntity DealChatProvisioned ({ DealId }) + ParticipantJoined на каждого участника — в той же транзакции (INV-CE-2); read-state и Seq не затрагиваются (отдельная лента, INV-CE-3).
  5. Готовность. Строка провизии → Status = Ready, ChatId = chat.Id; сформировать интеграционное DealChatStatusChangedEvent (pin.messenger.deal-chat-status-changed.v1, ContractVersion = 1, ключ = DealId, { DealId, ChatId, Status = Ready, OccurredAt }, без PII).
  6. Единая транзакция. Чат + участники + лента + строка провизии + исходящее событие фиксируются в одном согласованном изменении. Инфраструктурный сбой (A2) → откат; строка остаётся Failed/Provisioning, добор — chat-provision-retry.
  7. Проекция в CRM. Консюмер crm по deal-chat-status-changed проставляет Deal.ChatStatus = Ready — UI сделки показывает готовый чат; сам ChatEntity.Status остаётся Active (готовность — проекция crm, не статус чата).
  8. Realtime. Realtime-доставка ChatUpdated в chat:{ChatId} — участники онлайн видят чат сразу; offline — по последующим message-posted.

Диаграмма

Альтернативные Потоки

A1. Повторная доставка события (дубль)

  1. Kafka at-least-once → повтор deal-created. Строка провизии по DealId = Ready или Deal-чат по DealId уже существует (ux_chats_deal_id) → консюмер skip без мутаций (INV-CHAT-2); событие готовности эффектно не переиздаётся (idempotent by state).

A2. Сбой провизии → фоновый добор

  1. Транзакция шага 6 откатилась (PostgreSQL/Kafka недоступны) → строка провизии Failed (или зависла в Provisioning). Deal.ChatStatus в crm не переходит в Ready.
  2. chat-provision-retry (цикл 60 с) выбирает строку Failed/stuck Provisioning, идемпотентно повторяет провизию и re-emit Ready; poison после MaxAttemptsFailed
    • алерт (runbook). Возврат к готовности без ручного вмешательства.

A3. Последующая реконсиляция состава

  1. Изменение состава сделки → pin.crm.deal-members-changed.v1 → консюмер deal-members-changed делает set-реконсиляцию Participant (добавить/реактивировать/soft-remove, INV-CHAT-6/INV-PART-1/4) + ParticipantJoined/ Left/RoleChanged в ленту. Провизия чата (этот UC) при этом не повторяется.

A4. Событие сделки пришло раньше провизии чата (out-of-order)

  1. deal-created и deal-members-changed/booking-*/deal-stage-changed идут в разных топиках — общий ключ DealId даёт порядок только внутри одного топика; межтопикового порядка нет. Возможна ситуация: для DealId пришло deal-members-changed (или карточка booking), а Deal-чата ещё нет (провизия не выполнена/отстала).
  2. Консюмер зависимого события, не найдя чат по DealId, не создаёт его сам (провизия — только по deal-created, единая точка), а трактует как «чат ещё не готов»: паркует обработку (redeliver по offset с backoff) до появления чата; после исчерпания попыток — DLQ + алерт (runbook). Как только провизия по deal-created завершилась (Ready), припаркованное событие реконсилит состав/карточку идемпотентно.
  3. Обратный порядок безопасен по построению: deal-created несёт начальный состав (Members), поэтому базовый чат провизионируется корректно даже если deal-members-changed задержится; последующая реконсиляция (A3) лишь доводит состав.

Постусловие

  • Существует ровно один Deal-чат на DealId (ux_chats_deal_id), Active, с зеркалом активных DealMember (роли Investor/Agent/DeveloperManager/PinManager).
  • Лента chat_events содержит DealChatProvisioned + ParticipantJoined; строка провизии Ready.
  • Опубликован pin.messenger.deal-chat-status-changed.v1 (Ready) → crm.Deal.ChatStatus = Ready.

Возможные Ошибки

КодHTTPУсловиеПоведение
— (consumer)n/aсбой БД/Kafka в транзакции провизии (A2)строка Failed/Provisioning; добор фоновым retry, ретрай Kafka-offset
MSG_DEAL_CHAT_MEMBERS_INVALIDn/aпустой состав / Deal-чат без активных участников (INV-CHAT-7)лог + карантин строки, алерт (событие crm некорректно)
poisonn/aAttempts >= MaxAttempts в retry-jobStatus = Failed фиксируется, алерт, ручной re-enqueue (runbook)

Пользовательских HTTP-ошибок нет — сценарий системный (консюмер + фоновый worker).

Доменные События

СобытиеКогда поднимаетсяSubscribers
ChatCreatedChat.ProvisionDealлента ChatEventEntity DealChatProvisioned; realtime ChatUpdated
ParticipantAddedchat.AddParticipantлента ParticipantJoined; realtime ChatUpdated

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
DealChatStatusChangedEventpin.messenger.deal-chat-status-changed.v1провизия завершена (Ready) / retry re-emitcrm (проекция Deal.ChatStatus)

Payload { DealId, ChatId?, Status (Provisioning/Ready/Failed), OccurredAt }, ContractVersion = 1, ключ = DealId; публикуется в одном согласованном изменении с провизией. Потребляемое — pin.crm.deal-created.v1 (source — deal-created.md). Дом контракта готовности — deal-chat-status-changed.md.

Acceptance Criteria

#КритерийПроверка
AC1Провизия идемпотентна: повтор deal-created не создаёт второй чат (ux_chats_deal_id)Тест: две доставки → один Deal-чат, Ready не эффектно повторно (A1)
AC2Состав зеркалит Members события с маппингом ролей; лента фиксирует DealChatProvisioned+ParticipantJoinedТест консюмера: участники + роли, строки chat_events
AC3Готовность возвращается событием Readycrm.Deal.ChatStatus; ChatEntity.Status остаётся ActiveТест: deal-chat-status-changed Ready; чат Active
AC4Сбой провизии → строка Failed/Provisioning, добор chat-provision-retry, re-emit ReadyТест A2: инъекция сбоя → retry-job доводит до Ready идемпотентно
AC5Последующий deal-members-changed реконсилит состав без повторной провизииТест A3: add/remove участника → Participant set-реконсиляция, чат не пересоздаётся
AC6Дано deal-members-changed/booking-* пришло раньше провизии; когда консюмер не находит чат по DealId; тогда он паркует/ретраит (не создаёт чат сам), после Ready — идемпотентно реконсилит (A4)Тест out-of-order: событие до deal-created → park/retry, затем применяется без дубля
AC7Дано исчерпаны попытки припаркованного события; когда чат так и не появился; тогда событие уходит в DLQ + алерт, поток не блокируетсяТест: poison зависимого события → DLQ, offset продвинут

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

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

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