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

Consumer: DevStatsFunnelDealCompletedConsumer

Наполняет этап DealsCompleted воронки funnel_daily: терминальное завершение сделки → uniqState(DealId) по срезу (иерархия × дата). Обогащение иерархией — из dim_property_object по PropertyObjectId. Терминальный этап воронки конверсии (ADR-0057).

Назначение

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

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

ПолеЗначение
Сервис/контекстdev-stats (DevStats.Background)
ConsumerDevStatsFunnelDealCompletedConsumer
Обрабатываемое событиеdeal-completed
Topicpin.crm.deal-completed.v1
ContractVersion1 (warn-and-proceed)
Consumer groupdev-stats-funnel-crm-deal-completed
Producercrm (завершение сделки после Росреестра)
Эффект (событие → что меняется)pin_dev_stats.funnel_daily, этап DealsCompleted = uniqState(DealId)
Feature flagdev-stats.funnel.enabled

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

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

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

Контракт: event: deal-completed. Consumer читает:

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

FinalPrice/ContractType/Investor/Agent в воронке не используются (счётчик закрытых сделок, не сумма).

Алгоритм

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

  1. Проверить контекст события: сверить ContractVersion (= 1; при несовпадении — warn-and-proceed); идемпотентность по DealId/MessageId — повтор пропускается (no-op).
  2. Проверить payload (DealId/PropertyObjectId/TenantId/CompletedAt); при невалидном — пропуск с 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, DealsCompleted = uniqState(DealId), прочие этапы — пустые *State().
  5. Пакетно вставить строку в хранилище funnel_daily (батч ≤ 1000 строк / 5 с); AggregatingMergeTree сольёт частичные состояния по ключу. Смещение (offset) фиксируется только после подтверждённой вставки.
  6. Записать метрики/логи; успех → ack.

Диаграмма

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

  • Business key DealId (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(DealId) идемпотентен

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

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

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

НаправлениеКонтракт
Ожидает от crmСобытие deal-completed (pin.crm.deal-completed.v1, ContractVersion = 1, at-least-once) при терминальном завершении сделки (после Росреестра). Событие единичное на сделку. Обязательные для воронки поля: DealId (тот же, что у deal-createdDealsCompleted — подмножество Deals), PropertyObjectId, TenantId = DeveloperId, CompletedAt. FinalPrice/ContractType/ФИО в воронку не переносятся (счётчик закрытых сделок, не деньги).
Ожидает от chessboard (через dim-ingest)Строку dim_property_object с иерархией и DeveloperId; при отставании — late-dimension (park).
ПредоставляетЧастичную строку funnel_daily: DealsCompleted = uniqState(DealId) по срезу (иерархия × EventDate) — терминальный этап воронки. Потребитель — read API get-conversion-funnel (uniqMerge, без FINAL).
Окно eventual-consistency≤ 5 мин. В промежутке конверсия dealsCompleted/deals по объекту занижена — догоняет после вставки; при parked-событии — после backfill измерения.

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

  • Предусловия: включён флаг dev-stats.funnel.enabled; для объекта наполнена строка dim_property_object; событие проходит валидацию payload и tenant-guard.
  • Постусловия: в funnel_daily по срезу (иерархия объекта, день CompletedAt) состояние DealsCompleted учитывает DealId ровно один раз; offset группы dev-stats-funnel-crm-deal-completed продвинут строго после подтверждённой вставки; операционные данные crm не изменены.

Acceptance Criteria

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

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

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

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