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

Consumer: ReferralRegisteredConsumer (pin.referral.lead-attachments.v1)

Назначение

Консюмер Referral Registered обрабатывает приход инвестора по реферальной ссылке агента/застройщика и материализует в crm карточку пути клиента: get-or-create ClientEntity в базе агентства + get-or-create LeadEntity со Source = Referral для пары «агент ↔ инвестор» + запись в ленту лида (LeadEventEntity). На создание нового лида публикуется LeadCreatedEvent (pin.crm.lead-created.v1) — «подтверждение реф-привязки» рефереру (README).

Правила привязки исполняет referral, не CRM

Правила привязки (владелец ссылки — агент/застройщик по INV-REF-002; «Имя пустое / регистрация < 4 ч» — окно R2; самостоятельная регистрация — R3; открытие ссылки — R4) исполняются на стороне referral — CRM их не переисполняет: событие приходит только когда лид действительно нужен (см. attach-rules.md, матрица R1–R4 + модификатор R5). CRM отвечает за единственность активной пары «инвестор + агент» (дедупликация, глоссарий «Лид»).

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

ПолеЗначение
Сервис/контекстcrm
ConsumerReferral Registered
Обрабатываемое событиеpin.referral.lead-attach-requested
Topicpin.referral.lead-attachments.v1 (владелец — referral; контракт — lead-attach-requested.md)
ContractVersion1; при несовпадении версии события — warn-and-proceed
Partitions / key6 / key = InvestorUserId (порядок запросов per-инвестор)
Consumer groupcrm-referral-registered
Producerreferral (правила привязки R1–R4, attach-rules.md)
Эффект (событие → что меняется)get-or-create клиента и лида (Source = Referral) для пары «агент ↔ инвестор» + запись в ленту лида; при новом лиде — LeadCreatedEvent

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

  • Из метаданных события доступны: идентификатор сообщения MessageId (для дедупликации), CreatedAt, версия контракта, идентификатор инвестора (InvestorUserId).
  • Актор — системный контекст консюмера (не пользовательский): CreatedByUserId/UpdatedByUserId создаваемых ClientEntity/LeadEntity = null (система); запись выполняется в scope TenantId = AgentCompanyId (записи компании агента, ADR-0052 — не кросс-tenant).
  • Корреляция сквозного трейса восстанавливается из метаданных события и пробрасывается в исходящее событие LeadCreatedEvent.
  • Бизнес-идемпотентность — по ключу crm-referral-registered:{MessageId}; второй контур — бизнес-ключ пары (PrimaryInvestorUserId,ExecutorAgentUserId) через ux_leads_active_pair; третий — уникальность LeadEventEntity.SourceMessageId на ленте.

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

Контракт-дом — lead-attach-requested.md (полная таблица там; PII в payload нет). Ниже — только поля, которые consumer реально читает, и их маппинг.

Поле payloadТипОбяз.Как используется (маппинг)
InvestorUserIdGuidДаКлюч дедупа клиента (ux_clients_tenant_investor) и пары лида; ClientEntity.InvestorUserId, LeadEntity.PrimaryInvestorUserId.
AgentUserIdGuidДаClientEntity.OriginAgentUserId, LeadEntity.OriginAgentUserId + начальный ExecutorAgentUserId (Origin = Executor при создании); вторая половина бизнес-ключа активной пары (ExecutorAgentUserId).
AgentCompanyIdGuid?Да*TenantId создаваемых ClientEntity/LeadEntity (scope компании агента). null → аномалия (нарушение INV-REF-002 у producer) → DLQ (см. Retry).
ReferralLinkIdGuidДаLeadEntity.ReferralLinkId (атрибуция) + payload события ленты.
Channelstring (enum ReferralChannel)ДаАтрибуция канала в payload события ленты; unknown → Other + warn.
AppliedRulestring (enum ReferralAttachRule)ДаR1/R2/R3/R4 — в payload события ленты; unknown → warn + трактовать как R1 (карточка всё равно создаётся).
BondIdGuid?НетРеферальная связь, если создана (null для R3/R4/повтора); в payload события ленты.
RequestedAtDateTimeOffsetДаLeadEventEntity.OccurredAt (момент прихода лида).
Источник ФИО и телефона

ФИО/телефон инвестора в payload отсутствуют — берутся из CRM-реплики pin.identity.users.v1 (дом — identity-user-upserted.md); см. шаги 2–3.

Алгоритм

  1. Контекст события. Восстановить метаданные события (идентификатор сообщения, идентификатор инвестора, время, версия контракта); при несовпадении версии — warn-and-proceed (метрика crm_referral_contract_mismatch_total). Проверить идемпотентность по ключу crm-referral-registered:{MessageId}: если событие уже обработано → пропустить как дубль (ack).
  2. Проверка входа. InvestorUserId, AgentUserId, ReferralLinkId заданы; AgentCompanyId задан и не пуст (иначе CRM_REFERRAL_MISSING_TENANT — non-retriable → DLQ, аномалия producer); Channel/AppliedRule с неизвестным значением enum → warn + fallback (Other/R1), событие не теряется. Проверить существование identity-реплики инвестора (источник ФИО/телефона): если её нет → CRM_REFERRAL_INVESTOR_REPLICA_MISSINGretriable (out-of-order: событие pin.identity.users.v1 ещё не применилось). Guard-ы привязки (self-attach, Type=Investor, активность владельца) уже проверены producer-ом (trusted boundary) — здесь не дублируются.
  3. Что читаем.
    • identity-реплику инвестора — источник ФИО/телефона для создания клиента.
    • Клиента агентства по паре (AgentCompanyId, InvestorUserId) среди живых карточек (INV-CLI-3); если не найден — резервный поиск по телефону инвестора среди живых карточек того же tenant (ручная карточка до регистрации, INV-CLI-1) для связывания.
    • Активный лид пары (ExecutorAgentUserId, PrimaryInvestorUserId, Status = Active) — резолв по событийным AgentUserId/InvestorUserId, с блокировкой на чтение — сериализация гонки двойной привязки (INV-CRM-001).
  4. Правила (get-or-create + маппинг поле-в-поле).
    • Клиент. Найден → reuse; если найден по телефону с InvestorUserId == null — связать карточку с инвестором. Не найден → создать ClientEntity (TenantId = AgentCompanyId, OriginAgentUserId, InvestorUserId, ФИО/телефон из реплики, Source = Referral, Status = Active, CreatedByUserId = null); поднимается доменное ClientCreated.
    • Лид (дедуп активной пары). Активный лид найден → reuse (идемпотентный повтор; несколько агентов на одного инвестора — разные пары/лиды). Иначе создать лид (Source = Referral, OriginAgentUserId + начальный ExecutorAgentUserId (Origin = Executor при создании), PrimaryInvestorUserId, ReferralLinkId): фабрика проверяет INV-CRM-001, ставит TenantId = AgencyCompanyId, Status = Active, пустые хотелки (LeadWishes), ExpiresAt = CreatedAt + 1 месяц, CreatedByUserId = null; поднимается LeadCreated; затем лид привязывается к клиенту как активный (INV-CLI-2 — ActiveLeadId).
    • Лента. Дописать в ленту лида LeadEventEntity с SourceMessageId = MessageId (идемпотентность ленты), OccurredAt = RequestedAt, typed payload {ReferralLinkId, Channel, AppliedRule, BondId?}; обновить Lead.LastEventAt. Конкретный LeadEventType и модель payload — дом lead.md (см. «Известные ограничения»).
  5. Что меняется. Появляются/обновляются клиент, лид и событие ленты (порядок с учётом FK: клиент → лид → событие ленты). При reuse-ветке нового лида нет — только событие ленты. Все записи вместе с отметкой идемпотентности и (при новом лиде) исходящим событием фиксируются в одном согласованном изменении (R8). Конфликт ux_leads_active_pair (гонка) → перечитать активный лид и завершиться идемпотентно (пару уже создал конкурент). Offset подтверждается только после успешной фиксации.
  6. Какие события фиксируются. Только при создании нового лида — LeadCreatedEvent (контракт), topic pin.crm.lead-created.v1, ContractVersion = 1, key = LeadId; payload из лида: LeadId, TenantId, OriginAgentUserId, ExecutorAgentUserId, PrimaryInvestorUserId, Source = Referral, CreatedAt. Reuse активного лида → интеграционное событие не публикуется (идемпотентный повтор).
  7. Результат. Успех → ack. Retriable-код → retry по политике; non-retriable → DLQ + алерт. Метрики/логи — раздел «Наблюдаемость».

Диаграмма

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

  • На уровне консюмера: ключ crm-referral-registered:{MessageId} — повторная доставка после сбоя не меняет состояние.
  • На бизнес-уровне (пара): ux_leads_active_pair unique partial (AgentUserId,InvestorUserId) where Status='Active' — у пары одновременно один активный лид; повтор → reuse (INV-CRM-001).
  • На уровне ленты: ux_lead_events_source_message_id unique — одно MessageId не создаёт дубль строки ленты.
  • Replay: да. Повторный проход топика безопасен (get-or-create + три контура дедупа). Runbook — crm/runbooks/.

Retry и DLQ

СбойКодRetryDLQРучное действие
Инфраструктурный (PostgreSQL/Kafka недоступны, timeout)— (exception)Бесконечный exponential backoff 1s→60s с паузой партицииНетВосстановить зависимость (runbook)
Нет identity-реплики инвестора (out-of-order)CRM_REFERRAL_INVESTOR_REPLICA_MISSING6 попыток, backoff 30s→16mДа, после исчерпанияПроверить приход pin.identity.users.v1; replay из DLQ
Пустой AgentCompanyId (нарушение INV-REF-002)CRM_REFERRAL_MISSING_TENANTНет (non-retriable)Да + алертРазбор с referral (почему лид без владельца-tenant)
Конфликт телефона с чужой ручной карточкой (INV-CLI-1, разные InvestorUserId)CRM_CLIENT_PHONE_DUPLICATEНетДа + тикет заботыРучной merge карточек клиента
Гонка активной пары (ux_leads_active_pair)3 попытки немедленно (перечитать)После исчерпанияРасследование гонки
Unknown enum Channel/AppliedRule— (warn + fallback)НетОбновить маппинг при релизе новых значений

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

  • Logs (structured): consumed / skipped-duplicate / client-created / client-linked / lead-created / lead-reused / dead-lettered с полями MessageId, InvestorUserId, AgentUserId, AppliedRule; PII (телефон/ФИО из реплики) — маскируется (+7***1234), в логи только id.
  • Metrics: crm_referral_consume_duration_ms, crm_referral_leads_created_total, crm_referral_leads_reused_total, crm_referral_retries_total, crm_referral_dlq_total, crm_referral_lag_seconds, crm_referral_contract_mismatch_total.
  • Traces: span приёма события → span обработки → spans обращений к хранилищу; корреляция из метаданных события пробрасывается в исходящее LeadCreatedEvent.
  • Alerts: лаг > 5 мин (README, Alerts); DLQ > 0; рост client-phone-duplicate.
  • Безопасность: system-actor (не пользовательский контекст); запись строго в scope TenantId = AgentCompanyId (ADR-0052) — застройщик не видит лид/клиентскую базу, инвестор не видит фиксаций; tenant отсутствует/пуст → fail closed (CRM_REFERRAL_MISSING_TENANT, DLQ). Доступ к топику — m2m ACL Kafka (только crm).

Известные ограничения и assumptions

Историческое несоответствие имени топика

Имя топика — по producer-у (referral). Consumer подписан на реальный топик referral pin.referral.lead-attachments.v1 (событие ReferralLeadAttachRequestedKafkaEvent, дом — lead-attach-requested.md). Класс-потребитель на стороне crm сохраняет доменное имя ReferralRegisteredConsumer (семантика «реф-регистрация → лид»), но топик и тип события берутся из контракта referral (реконсилировано; ранее crm-сторона использовала неверное имя топика — referral-registered вместо lead-attachments).

примечание
Открытая дельта: LeadEventType реф-регистрации

Конкретное значение enum и модель payload события ленты определяются в доме lead.md; в текущем перечне LeadEventType отдельного значения под реф-приход нет — это дельта, которую нужно зарегистрировать в lead.md (поля ReferralLinkId, Channel, AppliedRule, BondId?), а не здесь.

  • Источник ФИО/телефона. CRM-реплика pin.identity.users.v1 (дом — identity-user-upserted.md); при её отсутствии на момент реф-события — retriable (см. Retry). Правила привязки (окно 4 ч, «имя пустое», владелец-агент) — дом attach-rules.md, CRM их не вычисляет.

Acceptance Criteria

  • AC-1 (новый клиент и лид). Дано: identity-реплика инвестора есть, клиента/активного лида пары нет. Когда: событие реф-привязки. Тогда: создан ClientEntity (Source = Referral, TenantId = AgentCompanyId), создан LeadEntity (Source = Referral, Status = Active, ExpiresAt = CreatedAt + 1 месяц), запись в ленту лида с SourceMessageId = MessageId, публикуется LeadCreatedEvent, ack.
  • AC-2 (переиспользование пары). Дано: активный лид пары уже существует. Когда: событие. Тогда: новый лид не создаётся, добавляется только событие ленты, LeadCreatedEvent не публикуется (lead-reused), ack.
  • AC-3 (нет tenant владельца). Дано: AgentCompanyId пуст (нарушение INV-REF-002). Когда: событие. Тогда: CRM_REFERRAL_MISSING_TENANT (non-retriable) → DLQ + алерт.
  • AC-4 (нет identity-реплики). Дано: реплики инвестора ещё нет (out-of-order против pin.identity.users.v1). Когда: событие. Тогда: CRM_REFERRAL_INVESTOR_REPLICA_MISSING — ретраи (30s→16m), после исчерпания DLQ.
  • AC-5 (связывание ручной карточки). Дано: клиента по паре нет, но есть живая карточка того же tenant по телефону с InvestorUserId == null. Когда: событие. Тогда: карточка связывается с инвестором (client-linked), лид создаётся/переиспользуется.
  • AC-6 (гонка активной пары). Дано: два события пары приходят конкурентно. Когда: обработка. Тогда: единственность ux_leads_active_pair — победитель создаёт лид, проигравший перечитывает и завершается идемпотентно (reuse), одно LeadCreatedEvent.
  • AC-7 (дубль). Дано: уже обработанный MessageId. Когда: повторная доставка. Тогда: skipped-duplicate, без изменений, ack.

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

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

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