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

Consumer: DevStatsFunnelBookingPaidConsumer

Наполняет этап BookingsPaid воронки funnel_daily: подтверждённая оплата брони → uniqState(BookingId) по срезу (иерархия × дата). Обогащение иерархией — из dim_property_object по PropertyObjectId. Промежуточный этап между bookings и deals (ADR-0057).

Назначение

Consumer DevStatsFunnelBookingPaidConsumer слушает событие booking-paid (топик pin.crm.booking-paid.v1), по PropertyObjectId резолвит иерархию объекта недвижимости из dim_property_object и добавляет частичную строку funnel_daily с заполненным состоянием этапа BookingsPaid; прочие этапы — пустые *State(). Запись — append-only в аналитическое хранилище pin_dev_stats, не source of truth (оплаты брони — в crm).

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

ПолеЗначение
Сервис/контекстdev-stats (DevStats.Background)
ConsumerDevStatsFunnelBookingPaidConsumer
Обрабатываемое событиеbooking-paid
Topicpin.crm.booking-paid.v1
ContractVersion1 (warn-and-proceed)
Consumer groupdev-stats-funnel-crm-booking-paid
Producercrm (подтверждение оплаты брони — менеджер застройщика по чеку)
Эффект (событие → что меняется)pin_dev_stats.funnel_daily, этап BookingsPaid = uniqState(BookingId)
Feature flagdev-stats.funnel.enabled

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

  • Из метаданных события доступны идентификатор сообщения (MessageId), время события (CreatedAt) и идентификатор источника (AggregateId = BookingId).
  • Сквозной трейс восстанавливается из метаданных события.
  • Ключ идемпотентности — uniqState(BookingId) + отсев повторного MessageId в батче.

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

Контракт: event: booking-paid. Consumer читает:

Поле payloadТипОбяз.Как используется
BookingIdGuidДабизнес-ключ этапа → uniqState(BookingId)
PropertyObjectIdGuidДаlookup dim_property_object → иерархия
TenantIdGuidДаtenant-guard: должен = dim_property_object.DeveloperId
PaidAtDateTimeOffsetДаEventDate = toDate(PaidAt)

PaymentAmount/ConfirmedByUserId в воронке не используются (счётчик оплат, не сумма).

Алгоритм

Консюмер батчевый: шаги 1–4 — на сообщение, шаги 5–6 — на батч (порог ≤ 1000 строк или 5 секунд; freshness SLA ≤ 5 мин).

  1. Проверить контекст события: сверить ContractVersion (= 1; при несовпадении — warn-and-proceed); идемпотентность по BookingId/MessageId — повтор пропускается (no-op).
  2. Проверить payload (BookingId/PropertyObjectId/TenantId/PaidAt); при невалидном — пропуск с warning (skip + warning).
  3. Обогащение иерархией: по PropertyObjectId взять из dim_property_object последнюю версию пути (argMax(…иерархия…, UpsertedAt), без FINAL); строки нет → отложенное повторное обогащение (deferred re-enrich, backoff, ≤ N попыток; при устойчивом промахе → DLQ + алерт «dimension gap»). Tenant-guard: dim_property_object.DeveloperId == TenantId; иначе — пропуск + alert, строка не пишется (fail-closed).
  4. Собрать частичную строку funnel_daily: ключ иерархии + EventDate, BookingsPaid = uniqState(BookingId), прочие этапы — пустые *State().
  5. Пакетно вставить строку в хранилище funnel_daily (батч ≤ 1000 строк / 5 с); AggregatingMergeTree сольёт частичные состояния по ключу. Смещение (offset) фиксируется только после подтверждённой вставки.
  6. Записать метрики/логи; успех → ack.

Диаграмма

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

  • Business key BookingId (uniqState) — повтор не растит счётчик; + MessageId в батче. Replay с offset безопасен. Late-dimension → deferred re-enrich (как property_object_view_facts).

Retry и DLQ

СбойRetryDLQРучное действие
Транзиент ClickHouseДа (offset не закоммичен)Нетавто-догон
Нет строки dim_property_object (late-dimension)deferred re-enrich (backoff, ≤ N попыток)Да при устойчивом промахе + алерт «dimension gap»backfill dim_property_object, затем replay из DLQ
TenantId ≠ dim_property_object.DeveloperIdНетНетskip + alert
Битый payloadНетНетskip + warning
Версия события ≠ ContractVersionНе сбой: warning-логНетсверить миграцию контракта crm
Сбой между вставкой и commit offsetПовторная доставка (at-least-once)Нетне требуется — uniqState(BookingId) идемпотентен

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

  • Logs/metrics/traces — consume/enrich/ingest, счётчик tenant-mismatch. Сумма оплаты в воронку не пишется; PII нет; DeveloperId из dim_property_object + сверка с TenantId (fail-closed). Воронка агрегатна.

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

НаправлениеКонтракт
Ожидает от crmСобытие booking-paid (pin.crm.booking-paid.v1, ContractVersion = 1, at-least-once) при подтверждении оплаты брони. Обязательные для воронки поля: BookingId (тот же, что у booking-created → этап BookingsPaid — подмножество Bookings), PropertyObjectId, TenantId = DeveloperId, PaidAt. Сумма (PaymentAmount) и подтвердивший (ConfirmedByUserId) в воронку не переносятся (счётчик оплат, не деньги).
Ожидает от chessboard (через dim-ingest)Строку dim_property_object с иерархией и DeveloperId; при отставании — late-dimension (park).
ПредоставляетЧастичную строку funnel_daily: BookingsPaid = uniqState(BookingId) по срезу (иерархия × EventDate). Потребитель — read API get-conversion-funnel (uniqMerge, без FINAL).
Окно eventual-consistency≤ 5 мин. В промежутке конверсия bookingsPaid/bookings по объекту занижена — догоняет после вставки; при parked-событии — после backfill измерения.

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

  • Предусловия: включён флаг dev-stats.funnel.enabled; для объекта наполнена строка dim_property_object; событие проходит валидацию payload и tenant-guard. Обычно booking-created по этой брони уже учтён, но порядок этапов не обязателен (AggregatingMergeTree сольёт состояния независимо от порядка).
  • Постусловия: в funnel_daily по срезу (иерархия объекта, день PaidAt) состояние BookingsPaid учитывает BookingId ровно один раз; offset группы dev-stats-funnel-crm-booking-paid продвинут строго после подтверждённой вставки; операционные данные crm не изменены.

Acceptance Criteria

  • Happy path: Дано — флаг включён, dim_property_object для PO1 есть, TenantId = dim.DeveloperId; Когда — приходит booking-paid (B1, PO1, день D); ТогдаuniqMerge(BookingsPaid) по срезу PO1 × D увеличивается на 1.
  • Идемпотентность (повтор доставки): ДаноB1 учтён; Когда — повторная доставка; ТогдаBookingsPaid не меняется.
  • Порядок этапов: Даноbooking-paid пришёл раньше booking-created этой брони; Тогда — оба этапа учитываются корректно по своим ключам (порядок не важен).
  • Tenant-guard: ДаноTenantId события ≠ dim.DeveloperId; Тогда — строка не пишется, skip + alert.
  • Late-dimension: Дано — строки dim_property_object нет; Тогда — park → дозаполнение после измерения; при устойчивом промахе — DLQ + алерт.
  • Отказ хранилища: Дано — ClickHouse недоступен; Тогда — offset не коммитится, retry с backoff, догон без дублей.

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

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

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