UC-REF-003. Регистрация по реферальной ссылке → связь + лид (правило R1)
Назначение
Инвестор регистрируется с переданной реферальной ссылкой: по событию регистрации UserUpsertedKafkaEvent
с заполненным ReferrerUrl контекст referral создаёт реферальную связь ReferralBondEntity (основание
будущих выплат в rewards), запись журнала Initial и — если владелец ссылки агент/застройщик —
запрашивает создание карточки лида в CRM. Правило R1 RegistrationWithReferral матрицы
attach-rules.md. Связан с контекстом referral.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-REF-003 |
| Контекст | referral (см. docs/06-services/referral/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | System (обработка события регистрации; m2m ACL Kafka на топик pin.identity.users.v1); инициатор бизнес-действия — Гость, ставший Инвестором (UC-IDN-001) |
| Permissions / scope | permission не применяется (system actor); доступ — ACL топика |
| Tenant | создаваемые связь/журнал получают TenantId = link.TenantId (= CompanyId реферера; null — реферер-инвестор, ADR-0052) |
| Record-level | не применяется на записи (system); последующее чтение — по ReferredUserId/OwnerUserId |
Предусловие
- Гость ранее открыл реферальную ссылку — cookie
referralUrlжива (30 дней, UC-REF-002). - Регистрация прошла:
identityсоздал учётку и опубликовалUserUpsertedKafkaEventcReferrerUrl = 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).
- Инвестор регистрируется в auth-модалке; клиент подставляет
referrerUrlиз cookie в регистрацию (UC-IDN-001). identityпубликуетUserUpsertedKafkaEvent(pin.identity.users.v1) сUserId,Type,Name,PhoneNumber,Status,CompanyId,ReferrerUrl,Version = 1; время события = момент регистрации.- Контекст и идемпотентность. Обработка запускается один раз на
UserId(first-seen). Повтор отсекается: если журналInitialдляReferredUserId = UserIdуже есть (INV-REF-R1) либо событие уже обработано (MessageId) → идемпотентный no-op. Реплика пользователяReferralUserReplicaобновляется с monotonic-guard (event.Version > ReplicaVersion; детально — user-upserted.md). - Проверки и загрузка (guard-ы G1–G5). Формат slug валиден; ссылка резолвится и не удалена (G1);
зарегистрировавшийся —
Type = Investor(G2); не владелец ссылки (G3); неBlocked/Archived(G4); активной связи у пользователя ещё нет — проверяется с блокировкой от гонки (G5). Данные пользователя берутся из payload события (реплика могла только что создаться); тип владельца ссылки читается для INV-REF-002. - Правило 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. - Журнал
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). - Интеграционные события. Публикуется
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()). - Согласованность. Реплика + связь + журнал + оба события фиксируются в одном согласованном изменении; конфликт единственности активной связи (гонка, INV-REF-001) → перечитать и завершиться идемпотентно. Событие подтверждается (ack).
- Downstream.
crmсоздаёт карточку лида (дедуп активной пары «инвестор+агент» на стороне CRM);rewardsфиксирует связь как основание будущих начислений поBondId;notificationsшлёт СМС рефереру «по вашей ссылке зарегистрировался пользователь»;messengerсоздаёт контакт «агент ↔ инвестор».
Альтернативные Потоки
A1. Владелец ссылки — не агент/застройщик
- INV-REF-002 не выполнен (модификатор R5) → lead-событие не публикуется, журнал пишется с
LeadRequested = false. Связь создаётся как обычно — детально UC-REF-006.
A2. Guard-нарушение (G1–G4)
- Slug не резолвится / пользователь не инвестор / self-attach / заблокирован → no-op c warning-логом (исходы по таблице guard-ов attach-rules.md); связь, журнал и лид не создаются; событие подтверждается (не ошибка). Завершение.
A3. Активная связь уже существует (G5, INV-REF-001)
- У пользователя уже есть
ReferralBondEntity (Active)(например, admin-коррекция или гонка) — связь и журнал не создаются. - Лид запрашивается по матрице (владелец — агент/застройщик): несколько агентов могут отслеживать одного инвестора, единственность активной пары — CRM. Событие подтверждается.
A4. Повторная доставка события (at-least-once)
- Журнал
InitialдляReferredUserIdуже есть (INV-REF-R1) либо совпалMessageId→ идемпотентный no-op, дублей связи/журнала/событий нет. Событие подтверждается.
A5. Владелец-агент заблокирован/архивирован
- Владелец ссылки —
Agent/DeveloperManager, ноowner.Status ∈ {Blocked, Archived}: связь по R1 создаётся (основание выплат оцениваетrewards), однако лид не запрашивается — guardowner.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 |
|---|---|---|---|
ReferralBondCreatedKafkaEvent | pin.referral.bonds.v1 | Связь создана (шаг 7) | rewards (основание выплат), notifications (СМС рефереру), messenger (контакт) |
ReferralLeadAttachRequestedKafkaEvent | pin.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 |
| AC7 | TenantId связи/журнала = TenantId ссылки; BoundAt = время события | проверка колонок |
| AC8 | Владелец-агент со Status ∈ {Blocked, Archived} → связь создаётся, lead-событие НЕ публикуется (LeadRequested = false) | тест заблокированного владельца-агента (A5) |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (4): UC-REF-001. Создание реферальной ссылки с каналом и шаринг, UC-REF-002. Аноним открыл реферальную ссылку (cookie 30 дней, validateReferralLink), UC-REF-004. Авторизация со ссылкой в окне lazy-регистрации → связь + лид (правило R2), UC-REF-006. Ссылка от инвестора (не агента/застройщика) → связь без лида (модификатор R5)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Правила привязки (дом, нормативный алгоритм): ../domain/attach-rules.md — правило R1, guard-ы G1–G5.
- Consumer: ../consumers/user-upserted.md.
- Domain entity: ../domain/referral-bond.md, ../domain/referral-registration.md, ../domain/referral-link.md.
- События: ../events/bond-created.md, ../events/lead-attach-requested.md; входное — user-upserted.md.
- Соседние контексты: crm/README.md (лид),
rewards/README.md (выплаты по
BondId). - Связанные UC: UC-REF-002 (cookie), UC-REF-004 (привязка при авторизации), UC-REF-006 (владелец не агент), UC-IDN-001.