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

UC-REF-003. Регистрация по реферальной ссылке → связь + лид (правило R1)

Назначение

Инвестор регистрируется с переданной реферальной ссылкой: по событию регистрации UserUpsertedKafkaEvent с заполненным ReferrerUrl контекст referral создаёт реферальную связь ReferralBondEntity (основание будущих выплат в rewards), запись журнала Initial и — если владелец ссылки агент/застройщик — запрашивает создание карточки лида в CRM. Правило R1 RegistrationWithReferral матрицы attach-rules.md. Связан с контекстом referral.

Metadata

ПолеЗначение
IDUC-REF-003
Контекстreferral (см. docs/06-services/referral/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (обработка события регистрации; m2m ACL Kafka на топик pin.identity.users.v1); инициатор бизнес-действия — Гость, ставший Инвестором (UC-IDN-001)
Permissions / scopepermission не применяется (system actor); доступ — ACL топика
Tenantсоздаваемые связь/журнал получают TenantId = link.TenantId (= CompanyId реферера; null — реферер-инвестор, ADR-0052)
Record-levelне применяется на записи (system); последующее чтение — по ReferredUserId/OwnerUserId

Предусловие

  • Гость ранее открыл реферальную ссылку — cookie referralUrl жива (30 дней, UC-REF-002).
  • Регистрация прошла: identity создал учётку и опубликовал UserUpsertedKafkaEvent c ReferrerUrl = slug (UC-IDN-001, контракт user-upserted.md).
  • Guard-предусловия G1–G5 attach-rules.md: ссылка резолвится и не удалена; зарегистрировавшийся — Type = Investor; не владелец ссылки; не Blocked/Archived; активной связи нет.

Триггер

ПолеЗначение
ИсточникKafka event
Триггерящий элементСобытие UserUpsertedKafkaEvent (топик pin.identity.users.v1), first-seen UserId при ReferrerUrl != null (триггер T1, attach-rules.md)

Основной Поток

Ниже — дельта входа T1/R1 к общему алгоритму привязки (дом — attach-rules.md).

  1. Инвестор регистрируется в auth-модалке; клиент подставляет referrerUrl из cookie в регистрацию (UC-IDN-001).
  2. identity публикует UserUpsertedKafkaEvent (pin.identity.users.v1) с UserId, Type, Name, PhoneNumber, Status, CompanyId, ReferrerUrl, Version = 1; время события = момент регистрации.
  3. Контекст и идемпотентность. Обработка запускается один раз на UserId (first-seen). Повтор отсекается: если журнал Initial для ReferredUserId = UserId уже есть (INV-REF-R1) либо событие уже обработано (MessageId) → идемпотентный no-op. Реплика пользователя ReferralUserReplica обновляется с monotonic-guard (event.Version > ReplicaVersion; детально — user-upserted.md).
  4. Проверки и загрузка (guard-ы G1–G5). Формат slug валиден; ссылка резолвится и не удалена (G1); зарегистрировавшийся — Type = Investor (G2); не владелец ссылки (G3); не Blocked/Archived (G4); активной связи у пользователя ещё нет — проверяется с блокировкой от гонки (G5). Данные пользователя берутся из payload события (реплика могла только что создаться); тип владельца ссылки читается для INV-REF-002.
  5. Правило R1 — создание связи. Создаётся ReferralBondEntity: ReferredUserId = event.UserId, ReferrerUserId = link.OwnerUserId, ReferralLinkId = link.Id, TenantId = link.TenantId, BoundAt = время события, AppliedRule = R1, Status = Active, Version = 1; инвариант INV-REF-B2 (не сам себе). Фиксируется доменное событие ReferralBondCreatedEvent.
  6. Журнал Initial. Пишется запись: ReferralLinkId = link.Id, LinkUrl = link.Url (денормализация), ReferrerUserId = link.OwnerUserId, ReferredUserId = event.UserId, TenantId = link.TenantId, RegistrationType = Initial, RegisteredAt = время события, AppliedRule = R1, LeadRequested = исход INV-REF-002; связь ссылается на запись журнала (RegistrationId, INV-REF-R4).
  7. Интеграционные события. Публикуется ReferralBondCreatedKafkaEvent (pin.referral.bonds.v1, key = ReferredUserId, payload — referral-bond.md: BondId, ReferredUserId, ReferrerUserId, ReferrerCompanyId, ReferralLinkId, Channel, BoundAt, AppliedRule = R1, Version); если владелец — Agent/DeveloperManager и owner.Status = Active (INV-REF-002) — дополнительно ReferralLeadAttachRequestedKafkaEvent (pin.referral.lead-attachments.v1, key = InvestorUserId; payload: InvestorUserId = event.UserId, AgentUserId = link.OwnerUserId, AgentCompanyId = link.TenantId, ReferralLinkId, Channel = link.Channel, AppliedRule = R1, BondId = bond.Id, RequestedAt = now()).
  8. Согласованность. Реплика + связь + журнал + оба события фиксируются в одном согласованном изменении; конфликт единственности активной связи (гонка, INV-REF-001) → перечитать и завершиться идемпотентно. Событие подтверждается (ack).
  9. Downstream. crm создаёт карточку лида (дедуп активной пары «инвестор+агент» на стороне CRM); rewards фиксирует связь как основание будущих начислений по BondId; notifications шлёт СМС рефереру «по вашей ссылке зарегистрировался пользователь»; messenger создаёт контакт «агент ↔ инвестор».

Альтернативные Потоки

A1. Владелец ссылки — не агент/застройщик

  1. INV-REF-002 не выполнен (модификатор R5) → lead-событие не публикуется, журнал пишется с LeadRequested = false. Связь создаётся как обычно — детально UC-REF-006.

A2. Guard-нарушение (G1–G4)

  1. Slug не резолвится / пользователь не инвестор / self-attach / заблокирован → no-op c warning-логом (исходы по таблице guard-ов attach-rules.md); связь, журнал и лид не создаются; событие подтверждается (не ошибка). Завершение.

A3. Активная связь уже существует (G5, INV-REF-001)

  1. У пользователя уже есть ReferralBondEntity (Active) (например, admin-коррекция или гонка) — связь и журнал не создаются.
  2. Лид запрашивается по матрице (владелец — агент/застройщик): несколько агентов могут отслеживать одного инвестора, единственность активной пары — CRM. Событие подтверждается.

A4. Повторная доставка события (at-least-once)

  1. Журнал Initial для ReferredUserId уже есть (INV-REF-R1) либо совпал MessageId → идемпотентный no-op, дублей связи/журнала/событий нет. Событие подтверждается.

A5. Владелец-агент заблокирован/архивирован

  1. Владелец ссылки — Agent/DeveloperManager, но owner.Status ∈ {Blocked, Archived}: связь по R1 создаётся (основание выплат оценивает rewards), однако лид не запрашивается — guard owner.Status = Active дополняет INV-REF-002 (attach-rules.md, edge case «владелец заблокирован/архивирован»). Журнал пишется с LeadRequested = false. Событие подтверждается.

Постусловие

  • Создана ReferralBondEntity (Status = Active, AppliedRule = R1) — единственная активная для ReferredUserId (INV-REF-001); rewards получил основание выплат.
  • Создана журнальная запись Initial (учитывается в registrationsCount ссылки и партнёрской статистике per-канал).
  • При владельце-агенте/застройщике в CRM запрошена карточка лида (LeadRequested = true); рефереру отправлено уведомление; в messenger создан контакт.

Возможные Ошибки

КодHTTPУсловиеПоведение
— (no-op, warning)— (Kafka consumer, HTTP нет)Guard G1–G4 не пройден (мусорный slug, не инвестор, self-attach, blocked)Мониторинг по метрике no-op guard-ов
— (идемпотентный no-op)G5/INV-REF-001: активная связь уже есть; повторная доставка (INV-REF-R1)Событие подтверждается
retry consumer-аРеплика владельца ссылки отсутствует (событие owner-а ещё не применено)Повтор после retry (attach-rules.md, шаг 2)
DLQ после исчерпания retryИнфраструктурный сбой (хранилище недоступно)Runbook referral-consumer-lag (README.md)

Доменные События

СобытиеКогда поднимаетсяSubscribers
ReferralBondCreatedEventСоздание связи (шаг 5)in-proc аудит (referral-bond.md)
ReferralRegistrationRecordedEventЗапись журнала Initial (шаг 6)in-proc (referral-registration.md)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
ReferralBondCreatedKafkaEventpin.referral.bonds.v1Связь создана (шаг 7)rewards (основание выплат), notifications (СМС рефереру), messenger (контакт)
ReferralLeadAttachRequestedKafkaEventpin.referral.lead-attachments.v1Владелец — Agent/DeveloperManager, Status = Active (INV-REF-002)crm (карточка лида, дедуп пары «инвестор+агент»)

Acceptance Criteria

#КритерийПроверка
AC1Регистрация с валидным ReferrerUrl создаёт связь Active + журнал Initial (AppliedRule = R1) в одном согласованном измененииинтеграционный тест
AC2Владелец-агент → публикуются оба события (bond + lead, BondId заполнен); владелец-инвестор → только bond, LeadRequested = falseконтракт-тесты событий
AC3Повторная доставка того же события не создаёт дублей (INV-REF-R1 + MessageId)двойная доставка в тесте
AC4Гонка «две привязки одного пользователя» → ровно одна связь (единственность активной связи + блокировка от гонки)конкурентный тест
AC5Регистрация без ReferrerUrl не порождает записей/событий referralнегативный тест
AC6Событие с UserId = link.OwnerUserId (пользователь пришёл по собственной ссылке) → no-op (INV-REF-004)тест guard G3
AC7TenantId связи/журнала = TenantId ссылки; BoundAt = время событияпроверка колонок
AC8Владелец-агент со Status ∈ {Blocked, Archived} → связь создаётся, lead-событие НЕ публикуется (LeadRequested = false)тест заблокированного владельца-агента (A5)

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

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

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