Entity / Aggregate: ReferralRegistrationEntity
Дом полей сущности «Регистрация по реферальной ссылке» (журнал) контекста referral. Все проекции
(модели ReferralRegistrationItem, AdminReferralRegistrationEntry, счётчик registrationsCount
ссылки) ссылаются сюда и описывают только дельту.
Назначение
ReferralRegistrationEntity — append-only журнальная запись факта «пользователь зарегистрировался
(или был привязан по правилам авторизации), придя по реферальной ссылке». Журнал — основа выдач
getLinkRegistrations (по одной ссылке) и getAllRegistrations (по всем ссылкам владельца),
admin-журнала и метрик каналов (конверсия «переход → регистрация»).
Отличие от реферальной связи: журнал — иммутабельная историческая лента (1:1 с
созданием/заменой связи: Initial — первая привязка, Rebind — admin-коррекция), а связь — текущее
действующее отношение «кто привёл кого» для выплат (не более одной активной, INV-REF-001).
Срабатывания правил, которые создают только лид без связи (правила R3/R4 в
attach-rules.md), журнал не пишут — их след живёт в карточке лида CRM.
Классификация
| Поле | Значение |
|---|---|
| Контекст | referral |
| Kind | Entity (append-only журнал; входит в границу согласованности процесса привязки, отдельным агрегатом не является) |
| Source of truth | referral |
| Таблица | referral.referral_registrations |
| Tenant isolation | TenantId = CompanyId реферера на момент записи (ADR-0052); приглашённый — инвестор, record-level по ReferredUserId |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор записи журнала. |
ReferralLinkId | Guid | Да | FK fk_referral_registrations_link → referral.referral_links(id), index ix_referral_registrations_referral_link_id | consumer (резолв slug → Id) | Ссылка, по которой пришёл пользователь (referralLinkId). Резолв slug ReferrerUrl события identity — в момент записи. |
LinkUrl | string | Да | varchar(64) | копия ReferralLinkEntity.Url на момент записи | Денормализованный slug (linkUrl в ReferralRegistrationItem/AdminReferralRegistrationEntry): журнал показывает исторический адрес даже после переименования/удаления ссылки. |
ReferrerUserId | Guid | Да | index ix_referral_registrations_referrer_user_id; логическая ссылка на identity.users(id) | копия ReferralLinkEntity.OwnerUserId на момент записи | Реферер (владелец ссылки в момент регистрации). Денормализация — метрики владельца не зависят от последующих изменений ссылки. |
ReferredUserId | Guid | Да | index ix_referral_registrations_referred_user_id; partial unique ux_referral_registrations_initial (referred_user_id) where registration_type = 'Initial'; логическая ссылка на identity.users(id) | UserUpsertedKafkaEvent.UserId / UserSignedInKafkaEvent.UserId | Приглашённый пользователь (referredInvestorId). Не более одной записи Initial на пользователя (идемпотентность first-seen). |
TenantId | Guid? | Нет | index ix_referral_registrations_tenant_id where not null | копия ReferralLinkEntity.TenantId | CompanyId реферера; null — инвестор (ADR-0052). |
RegistrationType | ReferralRegistrationType | Да | хранится как varchar(16) | app (правила привязки) | Initial / Rebind (см. ниже). |
RegisteredAt | DateTimeOffset | Да | index ix_referral_registrations_registered_at (desc; сортировка журнала) | время события-триггера | Момент регистрации/привязки (registeredAt). |
AppliedRule | ReferralAttachRule | Да | хранится как varchar(16) | app (правила привязки) | Какое правило матрицы сработало: R1 (регистрация), R2 (авторизация в окне lazy-регистрации), R6 (admin-коррекция) — attach-rules.md. |
LeadRequested | bool | Да | default false | app | Отправлен ли этой привязкой запрос на создание лида в CRM (ReferralLeadAttachRequestedKafkaEvent); false — владелец ссылки не агент/застройщик (INV-REF-002) или admin-коррекция. Фиксирует намерение (запрос-событие опубликовано), а не подтверждение, что CRM создала карточку: создание/дедуп карточки — асинхронно и на стороне crm (окно < 5 мин). |
CreatedAt | DateTimeOffset | Да | — | app | Технический момент записи (может отставать от RegisteredAt на лаг доставки события; в выдачах используется RegisteredAt). |
Enum ReferralRegistrationType
| Значение | Описание |
|---|---|
Initial | Первая привязка пользователя: регистрация с реф-ссылкой (правило R1) или авторизация в окне lazy-регистрации (правило R2). |
Rebind | Перепривязка к другой ссылке/рефереру. Автоматический rebind отключён — запись Rebind создаёт только admin-операция коррекции связи (решение в README.md; инвариант INV-REF-001). Пишется дополнительно к исходной Initial (на пользователя — ровно одна Initial + ноль-или-более Rebind, INV-REF-R1); ReferralLinkId/LinkUrl/ReferrerUserId записи Rebind указывают на новую ссылку/реферера коррекции, AppliedRule = R6. |
Enum ReferralAttachRule (R1…R6) — дом в attach-rules.md, здесь не
переописывается.
Value objects
VO не используются: все поля скалярные. ФИО/телефон/email приглашённого в admin-выдаче
(AdminReferralRegistrationEntry.name/phoneNumber/email) — не колонки журнала: join к реплике
пользователей ReferralUserReplica (attach-rules.md) в момент запроса.
Модели JSONB
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
ReferralLinkEntity | reference | ReferralLinkId (FK) | restrict (ссылка soft-delete, журнал живёт) |
ReferralBondEntity | reference (обратная) | referral_bonds.registration_id → referral_registrations.id | restrict |
ReferralUserReplica (приглашённый/реферер) | replica | ReferredUserId / ReferrerUserId | — |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-REF-R1 SingleInitial | На ReferredUserId — не более одной записи Initial (first-seen идемпотентность consumer-а: повторная доставка события не плодит записи) | partial unique ux_referral_registrations_initial + проверка записи Initial перед вставкой | идемпотентный no-op (не ошибка) |
| INV-REF-R2 Immutable | Запись после создания не изменяется и не удаляется | журнал только дополняется (нет update/delete) | — |
| INV-REF-R3 NoSelfRegistration | ReferredUserId != ReferrerUserId (нельзя зарегистрироваться по собственной ссылке) | guard процесса привязки (attach-rules.md, шаг 4) | запись не создаётся (лог warning) |
| INV-REF-R4 ConsistentDenorm | LinkUrl, ReferrerUserId, TenantId копируются из ReferralLinkEntity в том же согласованном изменении, что и запись | процесс привязки consumer-а | — |
Переходов состояний нет (иммутабельный журнал).
Согласованность записи и видимость
- Порядок и атомарность. Запись
Initialсоздаётся в одном согласованном изменении со связью (referral-bond.md) и исходящими событиями процесса привязки; порядок — связь → журнал → события (INV-REF-R4). Правила без связи (R3/R4) журнал не пишут — их след только в карточке лида CRM. - Идемпотентность при переигровке. Повторная доставка триггер-события не плодит записей:
Initialотсекается partial-uniqueux_referral_registrations_initial(INV-REF-R1) — повтор завершается идемпотентным no-op, а не ошибкой. Rebind. Пишется только admin-коррекцией связи, атомарно с переходом старой связи вReplacedи созданием замены; поляReferralLinkId/LinkUrl/ReferrerUserIdзаписиRebindуказывают на новую ссылку/реферера,AppliedRule = R6. ИсходнаяInitialне изменяется (INV-REF-R2).- Видимость admin-проекций. ФИО/телефон/email приглашённого в
AdminReferralRegistrationEntryподтягиваются join-ом к репликеReferralUserReplicaв момент запроса. Реплика отсутствует/отстаёт → graceful degradation (пустое ФИО,nullтелефон), не отказ;emailв реплику не реплицируется — всегдаnull(при необходимости админ-панель читаетidentityнапрямую).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
ReferralRegistrationRecordedEvent | доменное | Создание записи журнала | Id, ReferralLinkId, ReferredUserId, ReferrerUserId, RegistrationType, AppliedRule |
Собственных интеграционных событий журнал не публикует: наружу уходят события процесса привязки —
ReferralBondCreatedKafkaEvent (referral-bond.md) и
ReferralLeadAttachRequestedKafkaEvent (attach-rules.md); журнальная запись пишется
в одном согласованном изменении со связью и исходящими событиями.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK (id); ux_referral_registrations_initial unique btree (referred_user_id) where registration_type = 'Initial'; ix_referral_registrations_referral_link_id btree (журнал ссылки + COUNT для registrationsCount); ix_referral_registrations_referrer_user_id btree (журнал владельца getAllRegistrations); ix_referral_registrations_referred_user_id btree; ix_referral_registrations_registered_at btree desc; ix_referral_registrations_tenant_id btree where not null |
| Партиционирование / retention | none: ~1 млн регистраций всего (ADR-0056) — обычная таблица; журнал бессрочный (основание выплат) |
| Concurrency | нет (append-only; гонка двойной вставки решается partial unique INV-REF-R1) |
| Permissions / PII | Сама запись PII не содержит (только id). Выдача владельцу (getLinkRegistrations/getAllRegistrations) — обезличенная (referredInvestorId без ФИО/телефона), permission referral.registrations.view-self; admin-выдача с ФИО/телефоном/email (join реплики) — referral.registrations.view-all; телефоны в логах маскируются +7***1234 |
| Audit | append-only журнал сам является аудит-лентой привязок; actor — system (consumer), trace — на исходящих/consumer-записях |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Правила реферальной привязки (attach-rules) — дом доменных правил, Entity / Aggregate: ReferralBondEntity, Entity / Aggregate: ReferralLinkEntity
- API (9): GET /api/admin/referrals/links — Все реферальные ссылки платформы (admin, getReferralLinks), GET /api/admin/referrals/links/{linkId}/registrations — Журнал ссылки с PII (admin getLinkRegistrations), GET /api/investors/me/referral-links — Список моих реферальных ссылок (getMyReferralLinks), GET /api/investors/me/referral-links/{id}/registrations — Обезличенный журнал одной ссылки (getLinkRegistrations), GET /api/investors/me/referral-links/registrations — Обезличенный журнал по всем моим ссылкам (getAllRegistrations), GET /api/v1/partner/stats — Партнёрская сводка по каналам, GET /api/v1/referral-links/{referralLinkId}/stats — Статистика одной реферальной ссылки, POST /api/admin/referrals/bonds/{bondId}/rebind — коррекция связи (R6, Replace), Referral API — реестр операций и общие контрактные модели
- Use Cases (4): UC-REF-003. Регистрация по реферальной ссылке → связь + лид (правило R1), UC-REF-004. Авторизация со ссылкой в окне lazy-регистрации → связь + лид (правило R2), UC-REF-005. Авторизация: «Имя заполнено И регистрация ≥ 4 ч» → только лид, без связи (правило R3), UC-REF-006. Ссылка от инвестора (не агента/застройщика) → связь без лида (модификатор R5)
- События (1): Event Contract: pin.crm.lead-created (LeadCreatedEvent)
- Консюмеры (2): Consumer: ReferralUserSignedInConsumer (pin.identity.sign-ins.v1), Consumer: ReferralUserUpsertedConsumer (pin.identity.users.v1)
- registries (1): Реестр сущностей PIN
- index.md (1): Referral Service
Связанные документы
- referral-link.md (
registrationsCount), referral-bond.md (RegistrationId— основание), attach-rules.md (когда пишется запись, enumReferralAttachRule). - Выдачи:
referral/api/(getLinkRegistrations,getAllRegistrations; admin:getLinkRegistrations,getInvestorReferrals) — реестр prototype-mapping.md. - Триггер записи: user-upserted.md (
ReferrerUrl).