Consumer: Booking Paid Card (MessengerBookingPaidConsumer)
Назначение
Consumer слушает событие pin.crm.booking-paid.v1 и постит в чат сделки информационную карточку «Договор
бронирования оплачен» (CardType = BookingContractPaid, Category = Info, ActionStatus = None) с
подтверждённой суммой оплаты (US-INV-16). Карточка видна всем участникам чата (персональных строк-получателей
нет, INV-MSG-5). Обработка идемпотентна по источнику события (INV-MSG-8).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | messenger |
| Consumer | MessengerBookingPaidConsumer |
| Обрабатываемое событие | BookingPaidEvent |
| Topic | pin.crm.booking-paid.v1 (key = BookingId, 3 партиции) |
| ContractVersion | ожидается 1; при несовпадении версии — warn-and-proceed (messenger_booking_paid_contract_mismatch_total) |
| Consumer group | messenger-booking-paid |
| Producer | crm: менеджер застройщика подтвердил оплату (Booking.ConfirmPayment) |
| Эффект (событие → что меняется) | В ленту чата сделки добавляется информационная карточка BookingContractPaid с суммой |
Контекст события
- Из метаданных события доступны: идентификатор сообщения (для дедупликации), время события, версия контракта,
идентификатор и тип агрегата-источника (
BookingId/Booking). - Актор — системный; чат не tenant-фильтруется, доступ по членству (ADR-0052).
- Корреляция сквозного трейса восстанавливается из метаданных события.
- Ключ бизнес-идемпотентности:
messenger-booking-paid:{MessageId}; на уровне данных — уникальность карточки по (ChatId,CardType = BookingContractPaid,SourceEventId = MessageId) (INV-MSG-8).
Входные данные / payload события
Контракт — дом booking-paid.md (payload + Money; деньги —
CurrencyId + CurrencySnapshot, дом snapshot-модели —
handbook/domain/snapshots.md). Ниже — читаемые поля.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
BookingId | Guid | Да | CardBookingPaidPayload.BookingId |
DealId | Guid | Да | Поиск чата сделки по DealId; Payload.DealId |
PaymentAmount | Money {Amount, CurrencyId} | Да | Payload.Amount / Payload.CurrencyId; подстановка суммы в текст карточки |
CurrencySnapshot | CurrencySnapshot | Да | Payload.Currency; display валюты (знак/код) в тексте карточки; дом модели — handbook/domain/snapshots.md |
PaidAt | DateTimeOffset | Да | Payload.PaidAt; момент отправки карточки |
TenantId/PropertyObjectId/ConfirmedByUserId на карточку не влияют (id менеджера в карточке не раскрывается).
Модель payload карточки CardBookingPaidPayload
Дом — message.md (раздел «Модели JSONB»); здесь — только маппинг из события.
| Поле | Тип | Обязательность | Источник |
|---|---|---|---|
DealId | Guid | Да | BookingPaidEvent.DealId |
BookingId | Guid | Да | BookingPaidEvent.BookingId |
Amount | decimal | Да | BookingPaidEvent.PaymentAmount.Amount |
CurrencyId | short | Да | BookingPaidEvent.PaymentAmount.CurrencyId — ссылка на запись справочника валют handbook |
Currency | CurrencySnapshot | Да | BookingPaidEvent.CurrencySnapshot — display-снапшот валюты на момент публикации события (дом — handbook/domain/snapshots.md); карточка хранит его как исторический след, не «догоняя» переименования справочника |
PaidAt | DateTimeOffset | Да | BookingPaidEvent.PaidAt |
Алгоритм
- Контекст события. Восстановить метаданные события; сверить версию контракта с
1(при несовпадении — warn-and-proceed). Проверить бизнес-идемпотентность поmessenger-booking-paid:{MessageId}: событие уже обработано → пропустить как дубль (skipped-duplicate, ack). - Проверка входа.
DealIdиBookingIdзаданы; сумма оплаты > 0;CurrencyIdзадан; снапшот валютыCurrencySnapshotприсутствует и согласован сCurrencyId(CurrencySnapshot.CurrencyId = PaymentAmount.CurrencyId). Структурно невалидный payload →MSG_BOOKING_PAID_CARD_INVALID(DLQ + алерт). - Что читаем. Найти чат сделки по
DealId. Если чата ещё нет — он не провизионирован (событие пришло раньше deal-created): отказMSG_CHAT_NOT_PROVISIONEDс ретраем (backoff). - Бизнес-правила и маппинг. Собрать карточку
CardBookingPaidPayload(таблица выше);CardType = BookingContractPaid,Category = Info,ActionStatus = None,SourceEventId = MessageId; текст — «Договор бронирования оплачен» с подстановкой суммы и display-валюты изCurrencySnapshot(знакSign, при его отсутствии — кодIsoCode). Видимость — все участники (INV-MSG-5). Статус чата не меняется. - Что меняется. В ленту чата добавляется системная карточка (
Seqприсваивается монотонно, денормализуется last-message;SenderUserIdпустой,IsSystem = true, INV-MSG-4). - Что фиксируется. По факту публикации карточки участникам чата уходит realtime-доставка
MessageReceived(persist-then-push). Интеграционных Kafka-событий нет: offline-push шлётnotifications, факт оплаты в модель вознаграждений учитываетrewards— оба напрямую изbooking-paid(без двойного пуша). - Согласованность. Карточка и отметка идемпотентности фиксируются в одном согласованном изменении; offset подтверждается только после успешной фиксации.
- Результат. Успех → ack (SLA < 1 мин).
MSG_CHAT_NOT_PROVISIONED/инфраструктурный сбой → ретрай (backoff); структурная ошибка → DLQ + алерт.
Диаграмма
Предусловия и постусловия
До обработки (истинно на входе):
- Менеджер застройщика подтвердил оплату в
crm(Booking.ConfirmPayment) — факт оплаты уже зафиксирован в источнике;messengerего не перепроверяет, а отражает информационной карточкой. - Сделка (
DealId) существует; чат сделки, как правило, уже провизионирован (deal-created).
После успешной обработки (гарантируется):
- В ленте чата ровно одна карточка
BookingContractPaidдля данногоMessageId(Category = Info,ActionStatus = None) с подтверждённой суммой и валютой;last-messageденормализован,Seqвозрос на 1. - Онлайн-участникам ушла realtime-доставка
MessageReceived; оффлайн — при подключении (persist-then-push). - Статус чата и сделки не изменён; наружу не ушло ни одного Kafka-события; offset подтверждён после фиксации.
Кросс-сервисные контракты
Ожидается от crm (вход pin.crm.booking-paid.v1): at-least-once доставка; ключ BookingId (порядок
per-booking); обязательные поля DealId, BookingId, PaymentAmount {Amount, CurrencyId} +
CurrencySnapshot, PaidAt, MessageId, ContractVersion. Гарантия продьюсера: сумма > 0; CurrencyId —
ссылка на справочник валют handbook (RUB-only — ADR-0048), CurrencySnapshot снят на момент публикации;
DealId — реальная сделка. Реквизиты/эскроу/бинарь чека в событии не передаются (и не нужны карточке).
Ожидается от deal-created: чат по DealId провизионирован (иначе
MSG_CHAT_NOT_PROVISIONED + backoff).
Предоставляет наружу: ничего — исходящих Kafka-событий нет. Offline-push (notifications) и учёт факта
оплаты в модель вознаграждений (rewards) слушают booking-paid напрямую, не транзитом через messenger.
Критерии приёмки (Given/When/Then)
- Happy: Given провизионированный Deal-чат и событие
booking-paidс суммой; When обработка; Then в ленте карточкаBookingContractPaid(Info,None) с подставленной суммой/валютой,MessageReceivedучастникам, offset подтверждён, Kafka молчит. - Дубль: Given уже обработанный
MessageId; When повторная доставка; Then вторая карточка не создаётся (skipped-duplicate), ack. - Чат не готов: Given чат ещё не провизионирован; When обработка; Then
MSG_CHAT_NOT_PROVISIONED, ретрай с backoff, после лимита — DLQ. - Битая сумма/валюта: Given сумма ≤ 0, либо
CurrencyIdне задан, либоCurrencySnapshotотсутствует или не согласован сCurrencyId; When обработка; ThenMSG_BOOKING_PAID_CARD_INVALID, DLQ + алерт, без ретрая. - Иная версия контракта: Given
ContractVersion ≠ 1; When обработка; Then warn-and-proceed, обработка продолжается.
Идемпотентность и replay
- По сообщению: ключ
messenger-booking-paid:{MessageId}— повтор доставки не создаёт вторую карточку. - По данным: уникальность (
ChatId,CardType,SourceEventId) (INV-MSG-8). - Replay: да. Повторный re-read безопасен (де-дуп карточки). Порядок per-booking гарантирован ключом
BookingId.
Retry и DLQ
| Сбой | Код | Retry | DLQ | Ручное действие |
|---|---|---|---|---|
| Чат ещё не провизионирован | MSG_CHAT_NOT_PROVISIONED | Да, backoff | После лимита | Проверить лаг messenger-deal-created |
| Инфраструктурный (PostgreSQL/Kafka) | — | Бесконечный backoff 1s→60s | Нет | Runbook: восстановить зависимость |
| Структурная ошибка payload (сумма/валюта) | MSG_BOOKING_PAID_CARD_INVALID | Нет | Да + алерт | Проверить продьюсер crm |
| ContractVersion mismatch | — | warn-and-proceed | Нет | Проверить версию контракта |
Наблюдаемость и безопасность
- Logs:
consumed/skipped-duplicate/card-posted/retry-not-provisioned/failedcMessageId,DealId,ChatId,BookingId. Сумма в логах — не сверх необходимого; ФИО/телефонов нет. - Metrics:
messenger_booking_paid_consume_duration_ms,messenger_booking_paid_cards_posted_total,messenger_booking_paid_dlq_total,messenger_booking_paid_lag_seconds. - Traces: приём события → обработка → обращения к хранилищу; корреляция из метаданных события.
- Alerts: лаг > 5 мин; всплеск
MSG_CHAT_NOT_PROVISIONED; DLQ > 0. - Безопасность: системный актор, доступ по членству; событие — id + сумма (без ФИО/телефонов/реквизитов/ эскроу/бинаря чека); ФИО резолвятся из реплики messenger-user.md.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События сервиса messenger: реестр топиков, Event Contract: pin.crm.booking-paid (BookingPaidEvent)
- Консюмеры (2): Consumer: Booking Expired Card (MessengerBookingExpiredConsumer), Consumer: Document Request Card (MessengerDocumentRequestedConsumer)
Связанные документы
- Целевые агрегаты: message.md (
EventCardType.BookingContractPaid,CardBookingPaidPayload), chat.md (публикация карточки в ленту). - Входное событие (дом): booking-paid.md; snapshot-модель валюты
CurrencySnapshot— handbook/domain/snapshots.md. - Соседние консюмеры: document-requested.md, booking-expired.md, deal-created.md.
- README; ADR-0055.