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

Entity / Aggregate: ReferralBondEntity

Дом полей агрегата «Реферальная связь» контекста referral. Все проекции (модели GetMyReferrerQueryResult, AdminReferredInvestorEntry, payload ReferralBondCreatedKafkaEvent, будущие начисления в rewards) ссылаются сюда и описывают только дельту.

Назначение

ReferralBondEntity — действующее отношение «кто привёл кого, когда и через какую ссылку». Создаётся правилами привязки (attach-rules.md) и является основанием будущей выплаты вознаграждения за реферала: контекст rewards потребляет событие создания связи и на её Id ссылаются будущие начисления. Сами правила вознаграждений, начисления и выплаты — вне этого контекста: docs/06-services/rewards/README.md; referral владеет только фактом связи.

У пользователя — не более одной активной связи (INV-REF-001): getMyReferrer возвращает единственного реферера (hasReferrer, referrerInvestorId, referralLinkId, boundAt), admin getInvestorReferrals — список приглашённых реферером (AdminReferredInvestorEntry.boundAt).

Отличие от журнала регистраций: журнал — иммутабельная история всех срабатываний, связь — текущее состояние с жизненным циклом (ActiveReplaced/Revoked admin-коррекциями).

Классификация

ПолеЗначение
Контекстreferral
KindAggregateRoot
Source of truthreferral
Таблицаreferral.referral_bonds
Tenant isolationTenantId = CompanyId реферера на момент привязки (ADR-0052); приглашённый инвестор — record-level по ReferredUserId

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappИдентификатор связи; на него ссылаются начисления rewards.
ReferredUserIdGuidДаpartial unique ux_referral_bonds_referred_active (referred_user_id) where status = 'Active'; index ix_referral_bonds_referred_user_id; логическая ссылка на identity.users(id)триггер-событие (UserId)Приглашённый пользователь («кого привели»). Не более одной активной связи — INV-REF-001.
ReferrerUserIdGuidДаindex ix_referral_bonds_referrer_user_id; логическая ссылка на identity.users(id)ReferralLinkEntity.OwnerUserId на момент привязкиРеферер («кто привёл»). Проекция referrerInvestorId в GetMyReferrerQueryResult.
ReferralLinkIdGuidДаFK fk_referral_bonds_link → referral.referral_links(id), index ix_referral_bonds_referral_link_idрезолв slug (ReferrerUrl) → IdСсылка, через которую произошла привязка (проекция referralLinkId).
RegistrationIdGuid?НетFK fk_referral_bonds_registration → referral.referral_registrations(id), unique where not nullappЗапись журнала-основание (referral-registration.md). null только у admin-коррекций (Rebind), созданных без журнальной регистрации.
TenantIdGuid?Нетindex ix_referral_bonds_tenant_id where not nullкопия ReferralLinkEntity.TenantIdCompanyId реферера; null — инвестор (ADR-0052). Фиксируется на момент привязки (выплата принадлежит историческому контексту).
StatusReferralBondStatusДахранится как varchar(16), index ix_referral_bonds_statusappЖизненный цикл ниже.
BoundAtDateTimeOffsetДаindex ix_referral_bonds_bound_atвремя триггер-событияМомент привязки (проекция boundAt в GetMyReferrerQueryResult/AdminReferredInvestorEntry).
AppliedRuleReferralAttachRuleДахранится как varchar(16)app (правила привязки)Правило, создавшее связь: R1 (регистрация) или R2 (авторизация в окне lazy-регистрации); R6 — admin-коррекция. Дом enum — attach-rules.md.
ReplacedByBondIdGuid?НетFK self fk_referral_bonds_replaced_by → referral.referral_bonds(id)app (admin rebind)Ссылка на связь-замену при Status = Replaced (цепочка admin-коррекций).
RevokeReasonstring?Нетvarchar(512); обязателен при Status = Revoked (check ck_referral_bonds_revoke_reason)app (admin)Причина отзыва (фрод, ошибка привязки, претензия).
CreatedAtDateTimeOffsetДаappТехнический момент записи (= BoundAt c точностью до лага consumer-а).
CreatedByUserIdGuid?Нетappnull — создана процессом привязки (system); id администратора — admin-коррекция.
UpdatedAtDateTimeOffsetДаappМомент последнего изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший (admin-коррекции).
VersionlongДа>= 1appМонотонная доменная версия связи — guard для потребителей событий (rewards), инкремент на мутации.

Enum ReferralBondStatus

ЗначениеОписание
ActiveДействующая связь: учитывается в getMyReferrer, партнёрской статистике и будущих начислениях rewards.
ReplacedЗаменена admin-коррекцией (Rebind): ReplacedByBondId указывает на новую связь. Новые начисления по заменённой связи не создаются с момента замены; уже созданные сохраняются (referral-bond.md consumer rewards).
RevokedОтозвана администратором (RevokeReason обязателен). Новые начисления прекращаются; уже созданные сохраняются (отзыв по фроду — ручная отмена оператором ПИН, referral-accrual.md).

Value objects

VO не используются: все поля скалярные. Отображаемые данные реферера в GetMyReferrerQueryResult (referrerFirstName) и приглашённого в AdminReferredInvestorEntry (name, phoneNumber, email) — join к реплике ReferralUserReplica (attach-rules.md) в момент запроса, не колонки связи.

Модели JSONB

JSONB-модели не используются.

Связи

СвязьТипКлючDelete behavior
ReferralLinkEntityreferenceReferralLinkId (FK)restrict
ReferralRegistrationEntityreferenceRegistrationId (FK, nullable)restrict
ReferralBondEntity (замена)reference selfReplacedByBondId (FK)restrict
ReferralUserReplica (реферер/приглашённый)replicaReferrerUserId / ReferredUserId
Начисления rewardsвнешняя (по событию)rewards.* → BondId (логическая)

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-REF-001 SingleActiveBondНа ReferredUserId — не более одной связи Status = Active; автоматическая перепривязка запрещена (сработавшее правило при существующей активной связи связь не создаёт)partial unique ux_referral_bonds_referred_active + проверка активной связи в процессе привязкипривязка → идемпотентный no-op; admin-создание второй активной → REFERRAL_BOND_ALREADY_EXISTS
INV-REF-B2 NoSelfBondReferredUserId != ReferrerUserIdсоздание связиREFERRAL_BOND_SELF_FORBIDDEN
INV-REF-B3 ImmutableCoreReferredUserId, ReferrerUserId, ReferralLinkId, BoundAt, AppliedRule, TenantId иммутабельны после создания (основание выплат не редактируется — только замена/отзыв целиком)admin-коррекция = новый агрегат + замена старого
INV-REF-B4 ReplaceConsistencyПереход в Replaced допустим только с одновременным созданием связи-замены и заполнением ReplacedByBondId (в одном согласованном изменении)операция заменыREFERRAL_BOND_INVALID_TRANSITION
INV-REF-B5 RevokeReasonRequiredStatus = RevokedRevokeReason непустойcheck constraint + операция отзываValidationError
INV-REF-B6 TerminalStatesReplaced и Revoked — терминальные: обратных переходов нет (восстановление = новая связь admin-коррекцией)guard переходовREFERRAL_BOND_INVALID_TRANSITION

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
ReferralBondCreatedEventдоменное[*] → ActiveId, ReferredUserId, ReferrerUserId, ReferralLinkId, AppliedRule
ReferralBondReplacedEventдоменноеActive → ReplacedId, ReplacedByBondId, actor
ReferralBondRevokedEventдоменноеActive → RevokedId, RevokeReason, actor
ReferralBondCreatedKafkaEventинтеграционное (ContractVersion = 1, topic pin.referral.bonds.v1, key = ReferredUserId)[*] → ActiveBondId, ReferredUserId, ReferrerUserId, ReferrerCompanyId (= TenantId), ReferralLinkId, Channel (из ссылки), BoundAt, AppliedRule, Version
ReferralBondClosedKafkaEventинтеграционное (тот же topic pin.referral.bonds.v1, key = ReferredUserId)Active → Replaced и Active → RevokedBondId, ReferredUserId, Status (Replaced/Revoked), ReplacedByBondId?, RevokeReason? (без PII), Version

Потребители pin.referral.bonds.v1: rewards (реплика связи — основание будущих начислений за реферала; правила/выплаты — docs/06-services/rewards/README.md), notifications (СМС/уведомление рефереру «по вашей ссылке зарегистрировался пользователь»), messenger (контакт «агент ↔ инвестор» по потоку «Реферальная привязка» из service-boundaries.md). Детальный контракт — bond-created.md. Интеграционные события публикуются в одном согласованном изменении со сменой состояния агрегата. Событие о лиде — отдельный контракт ReferralLeadAttachRequestedKafkaEvent (attach-rules.md): лид может требоваться и без создания связи (правила R3/R4).

Гарантии для потребителей pin.referral.bonds.v1. ReferralBondCreatedKafkaEvent и ReferralBondClosedKafkaEvent идут в один топик с одним ключом ReferredUserId — поэтому для одного приглашённого потребитель видит события в порядке смены состояний (Created раньше любого последующего Closed). Доставка at-least-once: потребитель обязан быть идемпотентным и применять монотонный Version-guard (устаревшее/повторное событие с Version <= уже применённого — отбрасывается). Channel/ReferrerCompanyId — денормализованный снапшот на момент публикации (последующая правка/переименование ссылки эти поля в уже отправленном событии не меняет; выплата принадлежит историческому контексту, INV-REF-B3).

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK (id); ux_referral_bonds_referred_active unique btree (referred_user_id) where status = 'Active' (INV-REF-001); ux_referral_bonds_registration unique btree (registration_id) where not null; ix_referral_bonds_referred_user_id; ix_referral_bonds_referrer_user_id (список приглашённых getInvestorReferrals + партнёрская статистика); ix_referral_bonds_referral_link_id; ix_referral_bonds_status; ix_referral_bonds_bound_at; ix_referral_bonds_tenant_id where not null
Партиционирование / retentionnone; связи — бессрочно (основание выплат: Replaced/Revoked сохраняются для разбора)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую) + монотонная Version; гонка двух одновременных привязок одного пользователя разрешается partial unique-индексом (проигравшая → идемпотентный no-op)
Permissions / PIIСвязь PII не содержит (id + даты). getMyReferrer (свой реферер) — referral.links.view-self; список своих приглашённых (обезличенный) — referral.registrations.view-self; admin-выдачи с ФИО/телефоном (join реплики) и коррекции Replace/Revoke — referral.registrations.view-all через админ-панель (ADR-0053), телефоны в логах маскируются +7***1234
AuditVersion + actor-stamping; каждая admin-коррекция — доменное событие (Replaced/Revoked) + ReferralBondClosedKafkaEvent; журнальная запись Rebind в referral-registration.md

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

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

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

  • Правила создания (когда связь появляется, а когда нет) — attach-rules.md; журнал-основание — referral-registration.md; ссылка — referral-link.md.
  • Выплаты вознаграждений за реферала — контекст rewards: docs/06-services/rewards/README.md (структуры начислений ссылаются на BondId).
  • Проекции: getMyReferrer (GetMyReferrerQueryResult), admin getInvestorReferrals (AdminReferredInvestorEntry) — реестр prototype-mapping.md.
  • ADR-0052 (tenancy), ADR-0053 (admin-коррекции).