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

Consumer: Document Request Card (MessengerDocumentRequestedConsumer)

Назначение

Consumer слушает событие pin.crm.document-requested.v1 и постит в чат сделки карточку целевого действия «Загрузите документы…» (Category = TargetAction, ActionStatus = Pending) при запросе документа у участника (US-INV-16). Карточка адресуется инвестору (TargetParticipantUserId) через персональную видимость/тексты (message.md, INV-MSG-5): адресат видит actionable-текст, остальные участники — информационный. Тип карточки выбирается по DocumentType.

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

ПолеЗначение
Сервис/контекстmessenger
ConsumerMessengerDocumentRequestedConsumer
Обрабатываемое событиеDocumentRequestedEvent
Topicpin.crm.document-requested.v1 (key = DocumentId, 3 партиции)
ContractVersionожидается 1; при несовпадении версии — warn-and-proceed (messenger_document_requested_contract_mismatch_total)
Consumer groupmessenger-document-requested
Producercrm: запрос документа (менеджер застройщика / система)
Эффект (событие → что меняется)В ленту чата добавляется адресная карточка запроса документа с целевым действием

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

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

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

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

Поле payloadТипОбязательностьКак используется (маппинг)
DocumentIdGuidДаCardUploadDocumentsPayload.DocumentId
DealIdGuidДаПоиск чата сделки по DealId; Payload.DealId
DocumentTypestring (DealDocumentType)ДаPayload.DocumentType; выбор CardType (таблица ниже)
TitlestringДаPayload.Title; подстановка в текст карточки
RequestCommentstring?НетPayload.RequestComment (что именно нужно, ≤ 1000)
TargetParticipantUserIdGuid?НетАдресат персональной видимости карточки
DueAtDateTimeOffset?НетPayload.DueAt (индикация «просрочено» в карточке)

Маппинг DocumentType → EventCardType: BookingPaymentProof/PaymentProof → AttachPaymentProof; ClientPassport/SignedBookingContract → UploadBookingDocuments; ContractDraftDocument/SignedSaleContract → UploadContractDocuments; прочее/Other/unknown → UploadBookingDocuments (нейтральный fallback + warning). TenantId/RequestedByUserId/RequestedAt на карточку не влияют.

Алгоритм

  1. Контекст события. Восстановить метаданные события; сверить версию контракта с 1 (при несовпадении — warn-and-proceed). Проверить бизнес-идемпотентность по messenger-document-requested:{MessageId}: событие уже обработано → пропустить как дубль (skipped-duplicate, ack).
  2. Проверка входа. DealId и DocumentId заданы; Title непустой (≤ 256); RequestComment ≤ 1000. Структурно невалидный payload → MSG_DOCUMENT_CARD_INVALID (DLQ + алерт).
  3. Что читаем. Найти чат сделки по DealId вместе с активными участниками. Если чата ещё нет — он не провизионирован (событие пришло раньше deal-created): отказ MSG_CHAT_NOT_PROVISIONED с ретраем (backoff).
  4. Бизнес-правила и маппинг. Выбрать CardType по DocumentType; Payload = CardUploadDocumentsPayload { DealId, DocumentId, DocumentType, Title, RequestComment?, DueAt? }; Category = TargetAction, ActionStatus = Pending, SourceEventId = MessageId; текст — по CardType с подстановкой Title. Видимость: если TargetParticipantUserId задан — создать персональные строки получателей: адресат видит actionable «Загрузите документы: {Title}»; прочие активные участники видят информационный «Ожидается загрузка: {Title}». Если адресат не задан — строк-получателей нет, карточка видна всем с текстом по умолчанию (INV-MSG-5, общий запрос).
  5. Что меняется. В ленту чата добавляется карточка с персональными строками видимости (Seq присваивается монотонно, денормализуется last-message; SenderUserId пустой, IsSystem = true, INV-MSG-4).
  6. Что фиксируется. По факту публикации карточки участникам чата уходит realtime-доставка MessageReceived (persist-then-push; персональный фильтр видимости применяется на доставке). Интеграционных Kafka-событий нет: offline-push «требуется загрузка» шлёт notifications напрямую из document-requested.
  7. Согласованность. Карточка, строки видимости и отметка идемпотентности фиксируются в одном согласованном изменении; offset подтверждается только после фиксации.
  8. Результат. Успех → ack (SLA < 1 мин). MSG_CHAT_NOT_PROVISIONED/инфраструктурный сбой → ретрай (backoff); структурная ошибка → DLQ + алерт.

Диаграмма

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

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

  • Событие document-requested опубликовано crm при запросе документа у участника сделки (US-INV-16); messenger факт запроса отражает, не инициирует.
  • Deal-чат по DealId, как правило, провизионирован (deal-created); иначе — допустимая гонка, разрешается ретраем (шаг 3).
  • TargetParticipantUserId (если задан) — участник сделки; его строка участника, как правило, синхронизирована (deal-members-changed).

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

  • В ленте ровно одна карточка запроса документа для данного MessageId (Category = TargetAction, ActionStatus = Pending); CardType выбран по DocumentType.
  • Если TargetParticipantUserId задан и он активный участник — созданы персональные строки видимости: адресат видит actionable-текст, прочие активные участники — информационный; если не задан — строк-получателей нет, карточка видна всем с текстом по умолчанию (INV-MSG-5).
  • last-message денормализован; Seq монотонно возрос; онлайн-участникам ушла MessageReceived (персональный фильтр видимости применяется на доставке).
  • Статус чата и статус сделки не изменены; наружу Kafka не ушло; offset подтверждён после фиксации.

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

Ожидается от crm (вход pin.crm.document-requested.v1): at-least-once доставка; ключ партиционирования DocumentId (порядок per-document); обязательные DocumentId, DealId, DocumentType, Title; опциональные RequestComment, TargetParticipantUserId, DueAt; MessageId, ContractVersion. Гарантии продьюсера: DealId — реальная сделка; DocumentType — enum (неизвестное → нейтральный fallback UploadBookingDocuments, не отказ); Title/RequestComment — свободный текст менеджера без реквизитов и ФИО (гарантия продьюсера).

Ожидается от deal-created: чат по DealId провизионирован (eventual; пока нет — MSG_CHAT_NOT_PROVISIONED + backoff). От messenger-user.md / deal-members-changed: ФИО адресата резолвится из реплики; при лаге реплики карточка всё равно постится (её текст ФИО не содержит), display-имя догонит.

Предоставляет наружу: ничего — исходящих Kafka-событий нет. Offline-push «требуется загрузка» шлёт notifications напрямую из document-requested. Единственный внешний эффект — realtime-доставка карточки участникам внутри messenger.

Особый случай — адресат не участник чата: если TargetParticipantUserId задан, но пользователь не активный участник Deal-чата (рассинхрон состава) — actionable-строка ему не материализуется (ленту он не видит); прочие активные участники видят информационный текст; фиксируется warning.

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

  • Happy (адресная): Given провизионированный чат и document-requested с TargetParticipantUserId — активным участником; When обработка; Then карточка CardType по DocumentType, адресат видит «Загрузите документы: {Title}», прочие — «Ожидается загрузка: {Title}», MessageReceived, Kafka молчит.
  • Happy (общая): Given событие без TargetParticipantUserId; When обработка; Then карточка видна всем с текстом по умолчанию, строк-получателей нет.
  • Маппинг типа: Given DocumentType = BookingPaymentProof; When обработка; Then CardType = AttachPaymentProof (и аналогично по таблице маппинга).
  • Неизвестный тип: Given DocumentType вне enum; When обработка; Then CardType = UploadBookingDocuments (нейтральный fallback) + warning, не отказ.
  • Адресат не участник: Given TargetParticipantUserId не активный участник чата; When обработка; Then actionable-строка не материализуется, все видят информационный текст, warning.
  • Дубль: Given уже обработанный MessageId; When повторная доставка; Then вторая карточка не создаётся (skipped-duplicate), ack.
  • Чат не готов: Given чат не провизионирован; When обработка; Then MSG_CHAT_NOT_PROVISIONED, backoff, после лимита — DLQ.
  • Битый payload / пустой Title / RequestComment > 1000: Given структурно невалидное событие; When обработка; Then MSG_DOCUMENT_CARD_INVALID, DLQ + алерт, без ретрая.
  • Иная версия контракта: Given ContractVersion ≠ 1; When обработка; Then warn-and-proceed.

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

  • По сообщению: ключ messenger-document-requested:{MessageId}.
  • По данным: уникальность (ChatId, CardType, SourceEventId) — повтор доставки не создаёт вторую карточку (INV-MSG-8).
  • Replay: да. Повторный re-read безопасен (де-дуп карточки). Порядок per-document гарантирован ключом DocumentId.

Retry и DLQ

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

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

  • Logs: consumed / skipped-duplicate / card-posted(CardType,hasTarget) / retry-not-provisioned / failed c MessageId, DealId, ChatId, DocumentId. PII нет (id/типы).
  • Metrics: messenger_document_requested_consume_duration_ms, messenger_document_cards_posted_total{card_type}, messenger_document_requested_dlq_total, messenger_document_requested_lag_seconds.
  • Traces: приём события → обработка → обращения к хранилищу; корреляция из метаданных события.
  • Alerts: лаг > 5 мин; всплеск MSG_CHAT_NOT_PROVISIONED; DLQ > 0.
  • Безопасность: системный актор, доступ по членству; событие — id/типы/свободный текст менеджера (ФИО/телефонов нет); Title/RequestComment — не реквизиты (гарантия продьюсера); ФИО адресата резолвятся из реплики messenger-user.md, в realtime/логах не раскрываются сверх нужного.

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

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

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