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 |
| Consumer | MessengerDocumentRequestedConsumer |
| Обрабатываемое событие | DocumentRequestedEvent |
| Topic | pin.crm.document-requested.v1 (key = DocumentId, 3 партиции) |
| ContractVersion | ожидается 1; при несовпадении версии — warn-and-proceed (messenger_document_requested_contract_mismatch_total) |
| Consumer group | messenger-document-requested |
| Producer | crm: запрос документа (менеджер застройщика / система) |
| Эффект (событие → что меняется) | В ленту чата добавляется адресная карточка запроса документа с целевым действием |
Контекст события
- Из метаданных события доступны: идентификатор сообщения (для дедупликации), время события, версия контракта,
идентификатор и тип агрегата-источника (
DocumentId/DealDocument). - Актор — системный; чат не tenant-фильтруется, доступ по членству (ADR-0052).
- Корреляция сквозного трейса восстанавливается из метаданных события.
- Ключ бизнес-идемпотентности:
messenger-document-requested:{MessageId}; на уровне данных — уникальность карточки по (ChatId,CardType,SourceEventId = MessageId) (INV-MSG-8).
Входные данные / payload события
Контракт — дом document-requested.md. Ниже — читаемые поля.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
DocumentId | Guid | Да | CardUploadDocumentsPayload.DocumentId |
DealId | Guid | Да | Поиск чата сделки по DealId; Payload.DealId |
DocumentType | string (DealDocumentType) | Да | Payload.DocumentType; выбор CardType (таблица ниже) |
Title | string | Да | Payload.Title; подстановка в текст карточки |
RequestComment | string? | Нет | Payload.RequestComment (что именно нужно, ≤ 1000) |
TargetParticipantUserId | Guid? | Нет | Адресат персональной видимости карточки |
DueAt | DateTimeOffset? | Нет | Payload.DueAt (индикация «просрочено» в карточке) |
Маппинг DocumentType → EventCardType: BookingPaymentProof/PaymentProof → AttachPaymentProof;
ClientPassport/SignedBookingContract → UploadBookingDocuments;
ContractDraftDocument/SignedSaleContract → UploadContractDocuments; прочее/Other/unknown →
UploadBookingDocuments (нейтральный fallback + warning). TenantId/RequestedByUserId/RequestedAt
на карточку не влияют.
Алгоритм
- Контекст события. Восстановить метаданные события; сверить версию контракта с
1(при несовпадении — warn-and-proceed). Проверить бизнес-идемпотентность поmessenger-document-requested:{MessageId}: событие уже обработано → пропустить как дубль (skipped-duplicate, ack). - Проверка входа.
DealIdиDocumentIdзаданы;Titleнепустой (≤ 256);RequestComment≤ 1000. Структурно невалидный payload →MSG_DOCUMENT_CARD_INVALID(DLQ + алерт). - Что читаем. Найти чат сделки по
DealIdвместе с активными участниками. Если чата ещё нет — он не провизионирован (событие пришло раньше deal-created): отказMSG_CHAT_NOT_PROVISIONEDс ретраем (backoff). - Бизнес-правила и маппинг. Выбрать
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, общий запрос). - Что меняется. В ленту чата добавляется карточка с персональными строками видимости (
Seqприсваивается монотонно, денормализуется last-message;SenderUserIdпустой,IsSystem = true, INV-MSG-4). - Что фиксируется. По факту публикации карточки участникам чата уходит realtime-доставка
MessageReceived(persist-then-push; персональный фильтр видимости применяется на доставке). Интеграционных Kafka-событий нет: offline-push «требуется загрузка» шлётnotificationsнапрямую изdocument-requested. - Согласованность. Карточка, строки видимости и отметка идемпотентности фиксируются в одном согласованном изменении; offset подтверждается только после фиксации.
- Результат. Успех → 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 обработка; ThenCardType = AttachPaymentProof(и аналогично по таблице маппинга). - Неизвестный тип: Given
DocumentTypeвне enum; When обработка; ThenCardType = 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 обработка; ThenMSG_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
| Сбой | Код | Retry | DLQ | Ручное действие |
|---|---|---|---|---|
| Чат ещё не провизионирован | MSG_CHAT_NOT_PROVISIONED | Да, backoff | После лимита | Проверить лаг messenger-deal-created |
| Инфраструктурный (PostgreSQL/Kafka) | — | Бесконечный backoff 1s→60s | Нет | Runbook: восстановить зависимость |
| Структурная ошибка payload | MSG_DOCUMENT_CARD_INVALID | Нет | Да + алерт | Проверить продьюсер crm |
| ContractVersion mismatch | — | warn-and-proceed | Нет | Проверить версию контракта |
Наблюдаемость и безопасность
- Logs:
consumed/skipped-duplicate/card-posted(CardType,hasTarget) /retry-not-provisioned/failedcMessageId,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/логах не раскрываются сверх нужного.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События сервиса messenger: реестр топиков, Event Contract: pin.crm.document-requested (DocumentRequestedEvent)
- Консюмеры (1): Consumer: Booking Paid Card (MessengerBookingPaidConsumer)
Связанные документы
- Целевые агрегаты: message.md (
EventCardType.UploadBookingDocuments/UploadContractDocuments/AttachPaymentProof,CardUploadDocumentsPayload, персональные строки получателей, INV-MSG-5), chat.md (публикация карточки в ленту). - Входное событие (дом): document-requested.md.
- Соседние консюмеры: deal-created.md, booking-paid.md, booking-expired.md.
- README; ADR-0055.