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

Referral Service

Назначение

referral владеет реферальной/партнёрской системой PIN: реферальные ссылки пользователей по каналам (мониторинг, метрики), журнал регистраций по ссылкам, реферальные связи «кто привёл кого» (основание будущих выплат) и доменные правила привязки «ссылка → связь → лид». Сервис отдаёт пользователю управление своими ссылками и партнёрскую статистику, администратору платформы — журнал всех регистраций и ручные коррекции связей, а соседним контекстам (crm, rewards, notifications) — события о созданных и закрытых связях.

Metadata

ПолеЗначение
Статусdraft
Owning teamкоманда referral
Domainпривлечение/партнёрская программа (deals-цепочка, service-boundaries.md)
Data classificationMixed: ссылки/журнал — Internal; реплика пользователей — PII (ФИО, телефон)
Runtime criticalitymedium (недоступность не блокирует вход/каталог; ломает привязку и партнёрскую статистику)

Ownership

ArtifactOwner/Rule
PostgreSQL schemareferral
API areaspublic (self-service ссылки/журнал/реферер), admin (журнал всех, коррекции связей), internal (m2m-резолв slug)
Owned entitiesReferralLinkEntity, ReferralRegistrationEntity, ReferralBondEntity, ReferralUserReplica (replica)
Produced eventsReferralBondCreatedKafkaEvent, ReferralBondClosedKafkaEvent (pin.referral.bonds.v1); ReferralLeadAttachRequestedKafkaEvent (pin.referral.lead-attachments.v1)
Consumed eventsUserUpsertedKafkaEvent, UserBlockedKafkaEvent (pin.identity.users.v1); UserSignedInKafkaEvent (pin.identity.sign-ins.v1)
Search indexesnone
Analytics factsnone (метрики каналов — SQL-агрегаты по журналу/счётчикам, ADR-0056)

Ответственность И Границы

  • Делает: управление реферальными ссылками (slug, канал, default-ссылка); валидацию/резолв ссылки и счёт переходов; исполнение правил привязки (domain/attach-rules.md) по событиям identity; ведение журнала регистраций и реферальных связей; публикацию событий «связь создана» и «нужен лид»; партнёрскую статистику владельца (/profile/partner); admin-журнал и admin-коррекции связей.
  • Не делает: расчёт и выплату вознаграждений — контекст rewards: ставки реферального вознаграждения — referral-rate-rule.md, реферальные начисления по BondIdreferral-accrual.md, реплика связей — referral-bond.md; создание/жизненный цикл карточки лида (контекст crm — единственность активной пары «инвестор+агент»); фиксацию referrerUrl при регистрации/входе и cookie 30 дней (фронт + identity); контакты/чаты (messenger); СМС/уведомления (notifications).
  • Source of truth: ссылки, журнал регистраций, реферальные связи, правила привязки.
  • Реплики/projections: ReferralUserReplica (проекция pin.identity.users.v1 — тип/статус/ ФИО/телефон/дата регистрации); реплика связей в rewards — по событиям pin.referral.bonds.v1 (consumer — referral-bond.md).

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
PostgreSQL (schema referral)dbВсе сущностиСервис недоступен
Kafka pin.identity.users.v1queue (consume)Триггер T1 (регистрация с ReferrerUrl) + реплика пользователейПривязка/реплика отстают; consumer догоняет (at-least-once, идемпотентность first-seen)
Kafka pin.identity.sign-ins.v1queue (consume)Триггер T2 (авторизация со slug; поле ReferrerUrl)Привязка при входе отстаёт
Kafka producerqueue (produce)События pin.referral.* в crm/rewards/notifications/messengerСобытия копятся до доставки после восстановления
identity (косвенно)serviceИсточник учёток и slug referrerUrl; синхронных вызовов нет

Проекты

src/services/referral/
Pin.Referral.Api/
Pin.Referral.Application/
Pin.Referral.Domain/
Pin.Referral.Infrastructure/
Pin.Referral.Background/
Pin.Referral.Migrator/
Pin.Referral.Contracts/

Namespace по агрегатам — Pin.Referral.ReferralLinks, Pin.Referral.ReferralRegistrations, Pin.Referral.ReferralBonds (правило границ service-boundaries.md).

Основные Сущности

EntityНазначениеSource Of TruthДокумент
ReferralLinkEntityСсылка (slug) пользователя по каналу; default-ссылка; счётчик переходовreferraldomain/referral-link.md
ReferralRegistrationEntityAppend-only журнал регистраций по ссылкам (Initial/Rebind)referraldomain/referral-registration.md
ReferralBondEntityДействующая связь «кто привёл кого, когда, через какую ссылку» — основание выплатreferraldomain/referral-bond.md
ReferralUserReplicaРеплика пользователей identity для условий правил и admin-проекцийidentitydomain/attach-rules.md (раздел «Реплика»)
Правила привязки + ReferralAttachRule + INV-REF-00xDecision table «связь?/лид?»referraldomain/attach-rules.md

ER-обзор

API

Карта областей; каждая операция документируется отдельным файлом (реестр — api/README.md).

  • Public (self-service; permissions referral.links.manage-self / referral.links.view-self / referral.registrations.view-self):
    • api/public/get-or-create-default-referral-link.md — идемпотентная выдача/создание default-ссылки;
    • api/public/create-referral-link.md — создание (slug ≤ 64 опц., description ≤ 256, channel);
    • api/public/update-referral-link.md — смена slug/описания;
    • api/public/delete-referral-link.md — soft-delete (кроме default, INV-REF-L5);
    • api/public/get-my-referral-links.md — список своих (ReferralLinkListItem + registrationsCount; paging Offset/Take ≤ 1000);
    • api/public/get-my-referrer.md — свой реферер;
    • api/public/lookup-referral-link-by-id.md, api/public/lookup-referral-links.md — lookup для выпадающих списков;
    • api/public/validate-referral-link.md — валидация slug (аноним: isValid/referrerDisplayName; инкремент VisitsCount, триггер T3 для авторизованного инвестора);
    • api/public/get-link-registrations.md, api/public/get-all-registrations.md — обезличенный журнал своих ссылок (ReferralRegistrationItem);
    • api/public/get-partner-stats.md — партнёрская сводка по каналам для /profile/partner (переходы/регистрации/связи per Channel); api/public/get-link-stats.md — детальная статистика одной ссылки (переходы/регистрации/запрошенные лиды/связи).
  • Admin (permission referral.registrations.view-all):
    • api/admin/get-referral-links.md (AdminReferralLinkEntry: владелец, телефон, счётчик), api/admin/get-referral-link-by-id.md, api/admin/get-link-registrations.md (AdminReferralRegistrationEntry с ФИО/телефоном), api/admin/get-investor-referrals.md (AdminReferredInvestorEntry, boundAt);
    • api/admin/rebind-bond.md (R6, Replace), api/admin/revoke-bond.md (Revoke).
  • Internal (m2m): api/internal/resolve-referral-link.md — синхронный read-only резолв slug для identity/crm (владелец, тип/статус, OwnerIsLeadEligible; PII не отдаёт); межконтекстные мутации остаются событийными.
  • Callbacks: not applicable.

Events И Consumers

TypeNameTopicНазначениеLink
ProducedReferralBondCreatedKafkaEventpin.referral.bonds.v1Связь создана → rewards (основание выплат), notifications (уведомление рефереру), messenger (контакт)events/bond-created.md; поля — domain/referral-bond.md
ProducedReferralBondClosedKafkaEventpin.referral.bonds.v1Admin-коррекция (Replaced/Revoked) → rewards останавливает начисленияevents/bond-closed.md; поля — domain/referral-bond.md
ProducedReferralLeadAttachRequestedKafkaEventpin.referral.lead-attachments.v1Запрос создания карточки лида → crm (дедуп пары «инвестор+агент»)events/lead-attach-requested.md; payload — domain/attach-rules.md шаг 6
ConsumedUserUpsertedKafkaEventpin.identity.users.v1Триггер T1 (привязка при регистрации, ReferrerUrl) + апсерт ReferralUserReplicaconsumers/user-upserted.md; правила — domain/attach-rules.md
ConsumedUserBlockedKafkaEventpin.identity.users.v1Обновление Status реплики (guard G4, лиды заблокированного владельца)consumers/user-upserted.md (тот же топик/consumer)
ConsumedUserSignedInKafkaEventpin.identity.sign-ins.v1Триггер T2 (привязка при авторизации; требует поля ReferrerUrl? — см. решения)consumers/user-signed-in.md

Доставка и отказоустойчивость событий

Единая политика доставки для всех потоков (детали ретраев/DLQ — в consumer-документах и runbook runbooks/referral-consumer-lag.md):

  • Исходящие (produced). Публикуются транзакционным outbox в одном согласованном изменении со сменой состояния агрегата (связь/журнал) — гарантия «не потеряется» (at-least-once). Порядок per-ключ (ReferredUserId для pin.referral.bonds.v1, InvestorUserId для pin.referral.lead-attachments.v1) и монотонный Version-guard позволяют потребителю отбросить устаревшее/переигранное сообщение. При rebind в одном коммите публикуются оба события связи — потребитель видит closed строго раньше created (общий топик + ключ).
  • Входящие (consumed). At-least-once + идемпотентность: T1 — first-seen по журналу Initial (INV-REF-R1), апсерт реплики — по ReplicaVersion (Version > ReplicaVersion), T2 — guard G5. Повторная доставка не плодит связей/записей/событий.
  • Retry/DLQ. Транзиентная ошибка (недоступность БД, конфликт версий) → ограниченные повторы с backoff; при исчерпании — в DLQ топика без блокировки очереди (порядок по ключу сохраняется для остальных пользователей). Сообщение с неизвестной/несовместимой схемой (poison) → сразу в DLQ + алерт; разбор — по runbook. Out-of-order событие (устаревший Version) — не ошибка: no-op с логом.
  • Деградация. Недоступность consumer-потока откладывает привязку/реплику, но не блокирует вход/ каталог (Runtime criticality medium); после восстановления consumer догоняет lag. Отсутствие реплики на момент чтения admin-выдач — graceful degradation (пустое ФИО/null телефон), не отказ.

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

Ожидает от identity (Kafka, at-least-once, монотонный Version на пользователя):

  • UserUpsertedKafkaEvent (pin.identity.users.v1, ключ UserId, ContractVersion = 1): поля UserId, Type, CompanyId, Status, Name (FullNameSnapshot), PhoneNumber, ReferrerUrl?, Version; при Version = 1 время события = момент регистрации (источник RegisteredAt реплики). Наполняет ReferralUserReplica; триггер T1 — на first-seen UserId при ReferrerUrl != null.
  • UserSignedInKafkaEvent (pin.identity.sign-ins.v1, ключ UserId): additive-поле ReferrerUrl? (slug из подтверждения входа) — требование к identity (решение № 4), без bump ContractVersion; триггер T2.
  • UserBlockedKafkaEvent (pin.identity.users.v1): обновляет Status реплики (guard G4).
  • Гарантии/деградация: порядок per-UserId по ключу; идемпотентность — first-seen по журналу Initial
    • ReplicaVersion (апсерт только при Version > ReplicaVersion); отсутствие/устаревание реплики → RegisteredAt = null трактуется как «окно 4 ч истекло» (fail-closed), unknown Type/Status → «не агент/застройщик»/Blocked.

Предоставляет соседям (публикуется в одном согласованном изменении со сменой состояния агрегата):

  • ReferralBondCreatedKafkaEvent / ReferralBondClosedKafkaEvent (pin.referral.bonds.v1, ключ ReferredUserId, ContractVersion = 1, монотонный Version-guard): строгий порядок per-инвестор (при rebind closed раньше created) гарантируется общим топиком и ключом. Потребители — rewards (завести/остановить начисления по BondId), notifications (СМС рефереру), messenger (контакт).
  • ReferralLeadAttachRequestedKafkaEvent (pin.referral.lead-attachments.v1, ключ InvestorUserId, ContractVersion = 1): идемпотентен по паре (InvestorUserId, AgentUserId) — CRM дедуплицирует лид; может публиковаться и без создания связи (правила R3/R4).
  • Окно eventual-consistency: между сменой связи и реакцией rewards/CRM — лаг доставки (SLO привязки < 1 мин); в промежутке пользователь видит уже обновлённую связь в getMyReferrer, а начисления/лид подтягиваются асинхронно.

Синхронно предоставляет read-only резолв slug (api/internal/resolve-referral-link.md) для identity/crm: PII не отдаёт, owner-поля могут отставать на лаг реплики (вызывающий трактует их как подсказку — финальное решение «связь/лид» остаётся за referral).

Runtime И Эксплуатация

ОбластьТребование
Health checks/health/live, /health/ready (PostgreSQL, Kafka consumer lag), /health/startup (Migrator применён)
SLO/SLAПривязка после регистрации — < 1 мин (СМС рефереру, правило user-upserted.md); public API p95 < 300 мс
Dashboardreferral-overview (срабатывания правил R1–R6, конверсия переход→регистрация→связь per канал, consumer lag)
Alertsconsumer lag > 5 мин; рост no-op по G1 (битые slug в cookie); ошибки доставки событий
Runbooksrunbooks/referral-consumer-lag.md, runbooks/referral-bond-corrections.md
Feature flagsReferral:LazyRegistrationWindow (default 04:00:00); Referral:LinkOpenAttachEnabled (триггер T3, default off)

Безопасность

  • Actor types: пользователь (self-service, любой тип учётки), аноним (validateReferralLink), system (consumers, m2m ACL Kafka), администратор платформы (admin API).
  • Permission model: referral.links.manage-self, referral.links.view-self, referral.registrations.view-self, referral.registrations.view-all — каталог permission.md.
  • Sensitive data classes: ФИО/телефон в ReferralUserReplica (PII; телефон в логах — +7***1234); публичные выдачи журнала — обезличенные (только referredInvestorId), PII — только admin.
  • Audit: журнал регистраций append-only; связи — версия + actor-stamping + события коррекций; ссылки — actor-stamping + доменные события.
  • Tenant isolation: TenantId = CompanyId владельца/реферера, null у инвестора (ADR-0052); self-выдачи — record-level по OwnerUserId/ReferredUserId; кросс-tenant — только платформенные permissions.

Принятые Решения

#РешениеОбоснование
1Три сущности + реплика; правила привязки — отдельный дом attach-rules.md (decision table R1–R6, инварианты INV-REF-001…005)Сценарии/консюмеры ссылаются на один дом
2Журнал регистраций пишется 1:1 с созданием/заменой связи (Initial/Rebind); лид без связи (R3/R4) журнал не пишетСохраняет семантику registrationsCount; след лида — в CRM
3Автоматический Rebind отключён; перепривязка — только admin-коррекция (R6, Replace + Replaced)Связь — основание денежных выплат: смена реферера без ручного контроля — фрод-риск
4Требование к identity: additive-поле ReferrerUrl? в UserSignedInKafkaEvent (pin.identity.sign-ins.v1) — slug из confirmSignIn существующего пользователяИначе триггер T2 нереализуем событийно; additive-only — без bump ContractVersion (user-signed-in.md)
5RegisteredAt в реплике из времени события; неизвестен → окно 4 ч истекло (fail-closed)Не переплачивать выплаты при неполных данных
6Метрики каналов — SQL-агрегаты по журналу/связям + единственный счётчик-колонка VisitsCount; без ClickHouse/RedisADR-0056 simplicity-first: ~1 млн регистраций — PostgreSQL + индексы
7События выплат не публикуются из referral; rewards ведёт реплику связей по pin.referral.bonds.v1 (referral-bond.md) и при завершении сделки создаёт реферальное начисление по BondId (referral-accrual.md) со ставкой из referral-rate-rule.mdГраницы: referral — факт связи, rewards — деньги

Известные Ограничения И Assumptions

  • Смена slug ссылки инвалидирует ранее розданные cookie со старым slug (резолв только текущего slug; история slug-ов не ведётся) — осознанное упрощение (INV-REF-L6).
  • Правило T3 (лид при открытии ссылки) включается флагом после согласования с CRM (объём авто-лидов).
  • Деньги по связям живут в rewards: реферальное начисление создаётся при завершении сделки приведённого инвестора (referral-accrual.md); закрытие связи прекращает новые начисления, уже созданные сохраняются (referral-bond.md).

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