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

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
ConsumerMessengerBookingPaidConsumer
Обрабатываемое событиеBookingPaidEvent
Topicpin.crm.booking-paid.v1 (key = BookingId, 3 партиции)
ContractVersionожидается 1; при несовпадении версии — warn-and-proceed (messenger_booking_paid_contract_mismatch_total)
Consumer groupmessenger-booking-paid
Producercrm: менеджер застройщика подтвердил оплату (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ТипОбязательностьКак используется (маппинг)
BookingIdGuidДаCardBookingPaidPayload.BookingId
DealIdGuidДаПоиск чата сделки по DealId; Payload.DealId
PaymentAmountMoney {Amount, CurrencyId}ДаPayload.Amount / Payload.CurrencyId; подстановка суммы в текст карточки
CurrencySnapshotCurrencySnapshotДаPayload.Currency; display валюты (знак/код) в тексте карточки; дом модели — handbook/domain/snapshots.md
PaidAtDateTimeOffsetДаPayload.PaidAt; момент отправки карточки

TenantId/PropertyObjectId/ConfirmedByUserId на карточку не влияют (id менеджера в карточке не раскрывается).

Модель payload карточки CardBookingPaidPayload

Дом — message.md (раздел «Модели JSONB»); здесь — только маппинг из события.

ПолеТипОбязательностьИсточник
DealIdGuidДаBookingPaidEvent.DealId
BookingIdGuidДаBookingPaidEvent.BookingId
AmountdecimalДаBookingPaidEvent.PaymentAmount.Amount
CurrencyIdshortДаBookingPaidEvent.PaymentAmount.CurrencyId — ссылка на запись справочника валют handbook
CurrencyCurrencySnapshotДаBookingPaidEvent.CurrencySnapshot — display-снапшот валюты на момент публикации события (дом — handbook/domain/snapshots.md); карточка хранит его как исторический след, не «догоняя» переименования справочника
PaidAtDateTimeOffsetДаBookingPaidEvent.PaidAt

Алгоритм

  1. Контекст события. Восстановить метаданные события; сверить версию контракта с 1 (при несовпадении — warn-and-proceed). Проверить бизнес-идемпотентность по messenger-booking-paid:{MessageId}: событие уже обработано → пропустить как дубль (skipped-duplicate, ack).
  2. Проверка входа. DealId и BookingId заданы; сумма оплаты > 0; CurrencyId задан; снапшот валюты CurrencySnapshot присутствует и согласован с CurrencyId (CurrencySnapshot.CurrencyId = PaymentAmount.CurrencyId). Структурно невалидный payload → MSG_BOOKING_PAID_CARD_INVALID (DLQ + алерт).
  3. Что читаем. Найти чат сделки по DealId. Если чата ещё нет — он не провизионирован (событие пришло раньше deal-created): отказ MSG_CHAT_NOT_PROVISIONED с ретраем (backoff).
  4. Бизнес-правила и маппинг. Собрать карточку CardBookingPaidPayload (таблица выше); CardType = BookingContractPaid, Category = Info, ActionStatus = None, SourceEventId = MessageId; текст — «Договор бронирования оплачен» с подстановкой суммы и display-валюты из CurrencySnapshot (знак Sign, при его отсутствии — код IsoCode). Видимость — все участники (INV-MSG-5). Статус чата не меняется.
  5. Что меняется. В ленту чата добавляется системная карточка (Seq присваивается монотонно, денормализуется last-message; SenderUserId пустой, IsSystem = true, INV-MSG-4).
  6. Что фиксируется. По факту публикации карточки участникам чата уходит realtime-доставка MessageReceived (persist-then-push). Интеграционных Kafka-событий нет: offline-push шлёт notifications, факт оплаты в модель вознаграждений учитывает rewards — оба напрямую из booking-paid (без двойного пуша).
  7. Согласованность. Карточка и отметка идемпотентности фиксируются в одном согласованном изменении; offset подтверждается только после успешной фиксации.
  8. Результат. Успех → 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 обработка; Then MSG_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

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

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

  • Logs: consumed / skipped-duplicate / card-posted / retry-not-provisioned / failed c MessageId, 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.

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

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

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