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

Entity / Aggregate: ReferralRateRuleEntity

Дом полей агрегата «Ставка реферального вознаграждения» контекста rewards. Проекции (модели ответов platform-API, снапшот применённой ставки в реферальном начислении) ссылаются сюда и описывают только дельту.

Назначение

ReferralRateRuleEntity — настройка Менеджера ПИН, определяющая процент реферального вознаграждения партнёра от конечной цены (FinalPrice) завершённой сделки приведённого им инвестора (ADR-0066, ADR-0068). Ставки различаются по партнёрам и по иерархии объекта сделки (застройщик → проект → группа планировок) и ограничены сроками действия (в т.ч. временно повышенные ставки на период акции конкретного проекта или группы планировок).

Ставки живут в rewards (владелец денег); контекст referral владеет только фактом связи «кто кого привёл» (../../referral/domain/referral-bond.md) и денег не считает. Ставка резолвится при создании реферального начисления (referral-accrual.md) на момент CompletedAt сделки; в начисление снимается снапшот — последующая смена ставок существующие начисления не пересчитывает.

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

ПолеЗначение
Контекстrewards
KindAggregateRoot
Source of truthrewards
Таблицаrewards.referral_rate_rules
Tenant isolationвне tenant-модели — платформенная сущность (владелец процесса — ПИН, ADR-0052); управление — платформенный scope

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappУникальный идентификатор ставки; попадает в снапшот применённой ставки начисления.
PartnerUserIdGuid?Нетindex ix_referral_rate_rules_partner (partner_user_id) where not null; логическая ссылка на identity.users.id, без FKappПартнёр, для которого действует ставка; null — любой партнёр.
DeveloperCompanyIdGuid?Нетindex ix_referral_rate_rules_developer (developer_company_id) where not null; логическая ссылка на identity.companies.id (Type = Developer), без FKappЗастройщик, по чьим сделкам действует ставка; null — любой застройщик. Родитель ProjectId в иерархии объекта (INV-RR-4).
ProjectIdGuid?Нетindex ix_referral_rate_rules_project (project_id) where not null; логическая ссылка на проект каталога (catalog/chessboard), без FKappПроект застройщика, по чьим сделкам действует ставка; null — любой проект застройщика. Требует заполненного DeveloperCompanyId (INV-RR-4).
LayoutGroupIdGuid?Нетindex ix_referral_rate_rules_layout_group (layout_group_id) where not null; логическая ссылка на группу планировок каталога (catalog/chessboard), без FKappГруппа планировок проекта, по чьим объектам действует ставка; null — любая группа проекта. Требует заполненного ProjectId (INV-RR-4).
PercentdecimalДаnumeric(5,2), check percent > 0 AND percent <= 100appПроцент вознаграждения от FinalPrice сделки (INV-RR-1).
EffectiveFromDateTimeOffsetДаcheck effective_to is null or effective_to > effective_fromappНачало периода действия.
EffectiveToDateTimeOffset?НетappКонец периода действия (исключительно); null — бессрочно.
Commentstring?Нетvarchar(2000)appКомментарий Менеджера ПИН (основание, условия акции).
StatusReferralRateRuleStatusДахранится как text, varchar(32), index ix_referral_rate_rules_status (status)appDraft / Active / Archived (stateDiagram ниже).
CreatedAtDateTimeOffsetДаindexappМомент создания (аудит).
CreatedByUserIdGuid?НетappМенеджер ПИН, создавший ставку.
UpdatedAtDateTimeOffsetДаappМомент изменения.
UpdatedByUserIdGuid?НетappПоследний изменивший.
VersionlongДа>= 1appДоменная версия (инкремент при мутации; идёт в снапшот применённой ставки).

Enum ReferralRateRuleStatus

ЗначениеОписание
DraftЧерновик: редактируется, в резолюции ставки не участвует.
ActiveДействующая ставка: участвует в резолюции (с учётом периода EffectiveFrom/EffectiveTo). Иммутабельна, кроме EffectiveTo (INV-RR-3).
ArchivedАрхив: выведена из резолюции навсегда; снапшоты в созданных начислениях живы.

Enum ReferralRateScope (специфичность; не хранится — выводится из заполненности полей)

Специфичность складывается из двух ортогональных осей: партнёрская (задан PartnerUserId или нет) и объектная — вложенная цепочка LayoutGroup ⊂ Project ⊂ Developer (глубина определяется самым конкретным заполненным измерением объекта; родитель обязателен при заполненном потомке — INV-RR-4). Партнёрская ось доминирует над объектной («партнёр уточняет ось» — ADR-0066/ADR-0068): любая партнёрская ставка специфичнее любой партнёр-агностичной. Внутри одной партнёрской оси специфичнее та, у которой объектная цепочка глубже (LayoutGroup > Project > Developer > без объекта).

ЗначениеPartnerUserIdDeveloperCompanyIdProjectIdLayoutGroupIdСпецифичностьОписание
PartnerDeveloperProjectLayoutGroupзаданзаданзаданзадан1 (макс.)Персональная ставка партнёра по конкретной группе планировок конкретного проекта.
PartnerDeveloperProjectзаданзаданзаданnull2Персональная ставка партнёра по конкретному проекту застройщика.
PartnerDeveloperзаданзаданnullnull3Персональная ставка партнёра по конкретному застройщику.
Partnerзаданnullnullnull4Персональная ставка партнёра по всем объектам.
DeveloperProjectLayoutGroupnullзаданзаданзадан5Ставка по конкретной группе планировок проекта для всех партнёров (акция на группу планировок).
DeveloperProjectnullзаданзаданnull6Ставка по конкретному проекту застройщика для всех партнёров (акция проекта).
Developernullзаданnullnull7Ставка по сделкам конкретного застройщика для всех партнёров (акция застройщика).
Globalnullnullnullnull8 (мин.)Глобальная ставка — страховочный уровень по умолчанию.

Порядок резолюции (наиболее специфичное побеждает): PartnerDeveloperProjectLayoutGroupPartnerDeveloperProjectPartnerDeveloperPartnerDeveloperProjectLayoutGroupDeveloperProjectDeveloperGlobal.

Резолюция ставки

Выполняется при создании реферального начисления (referral-accrual.md, ветка consumer-а ../consumers/crm-deal-completed.md). Вход: PartnerUserId (реферер активной связи), координаты объекта сделки (DeveloperCompanyId, ProjectId, LayoutGroupId — из снимка объекта сделки, catalog/chessboard), момент asOf = CompletedAt.

  1. Отобрать кандидатов: Status = Active, период покрывает asOf (EffectiveFrom <= asOf и (EffectiveTo is null или EffectiveTo > asOf)), измерения совместимы со сделкой (по каждому измерению null или равно координате сделки: PartnerUserId, DeveloperCompanyId, ProjectId, LayoutGroupId). Ставок единицы–десятки — выборка целиком (ADR-0056).
  2. Выбрать наиболее специфичный непустой уровень в порядке специфичности: PartnerDeveloperProjectLayoutGroupPartnerDeveloperProjectPartnerDeveloperPartnerDeveloperProjectLayoutGroupDeveloperProjectDeveloperGlobal (специфичное побеждает; партнёрская ось доминирует над объектной).
  3. Внутри уровня кандидат ровно один — периоды активных ставок одной комбинации измерений не пересекаются (INV-RR-2), поэтому резолюция детерминирована.
  4. Кандидатов нет → реферальное начисление не создаётся: warn-метрика rewards_referral_accrual_no_rate_total (глобальная бессрочная ставка — страховочный уровень, заводится первым шагом внедрения).

В начисление снимается снапшот применённой ставки (AppliedRateSnapshotreferral-accrual.md): RateRuleId, Scope, применённая область (координаты специфичности PartnerUserId?, DeveloperCompanyId?, ProjectId?, LayoutGroupId?), Percent, EffectiveFrom/EffectiveTo, RuleVersion. Снапшот делает начисление объяснимым постфактум («ставка применена, потому что сделка попала в акцию проекта X, период …») и не пересчитывается при последующей смене ставок.

Value objects

VO не используются: ставка хранит только процент (decimal); денежные суммы появляются в реферальном начислении (referral-accrual.md).

Модели JSONB

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

Связи

СвязьТипКлючDelete behavior
Пользователь-партнёр (identity)reference (логическая, без FK)PartnerUserIdrestrict
CompanyEntity (застройщик, identity)reference (логическая, без FK)DeveloperCompanyIdrestrict
Проект (catalog/chessboard)reference (логическая, без FK)ProjectIdrestrict
Группа планировок (catalog/chessboard)reference (логическая, без FK)LayoutGroupIdrestrict
ReferralAccrualEntityreference (обратная, через снапшот)AppliedRateSnapshot.RateRuleId (логическая)— (ставка с применениями только архивируется)

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

ИнвариантУсловиеГде проверяетсяОшибка
INV-RR-1 PercentRange0 < Percent <= 100 (numeric(5,2))проверка входа + db checkREWARDS_REFERRAL_RATE_PERCENT_OUT_OF_RANGE
INV-RR-2 NoOverlapWithinSpecificityПериоды `[EffectiveFrom, EffectiveTo∞) активных ставок с **одинаковой комбинацией значений** (PartnerUserId, DeveloperCompanyId, ProjectId, LayoutGroupId), включая null`-значения, не пересекаются — гарантирует детерминированность резолюцииоперация Activate, app-level: активные ставки той же комбинации значений читаются под блокировкой, пересечение периодов отклоняется
INV-RR-3 ImmutableWhenActiveВ Active меняется только EffectiveTo — досрочное завершение, только сокращение периода (не раньше текущего момента; сокращение пересечений не создаёт); остальные поля — только в Draftguard по Status в операции измененияREWARDS_REFERRAL_RATE_ACTIVE_IMMUTABLE
INV-RR-4 ScopeHierarchyConsistentОбъектные измерения образуют вложенную цепочку: ProjectId задан ⇒ задан DeveloperCompanyId; LayoutGroupId задан ⇒ задан ProjectId (и, транзитивно, DeveloperCompanyId). PartnerUserId ортогонален и не ограниченпроверка входа + db checkREWARDS_REFERRAL_RATE_SCOPE_HIERARCHY_INVALID

Коды ошибок

КодHTTPУсловие
REWARDS_REFERRAL_RATE_PERCENT_OUT_OF_RANGE400Процент вне диапазона (0; 100] (INV-RR-1).
REWARDS_REFERRAL_RATE_PERIOD_INVALID400EffectiveTo <= EffectiveFrom.
REWARDS_REFERRAL_RATE_SCOPE_HIERARCHY_INVALID400Нарушена вложенность измерений объекта: задан потомок без родителя (ProjectId без DeveloperCompanyId или LayoutGroupId без ProjectId) (INV-RR-4).
REWARDS_REFERRAL_RATE_OVERLAP409Пересечение периодов активных ставок одной комбинации измерений при активации (INV-RR-2).
REWARDS_REFERRAL_RATE_ACTIVE_IMMUTABLE409Попытка изменить Active-ставку сверх сокращения EffectiveTo (INV-RR-3).
REWARDS_REFERRAL_RATE_NOT_FOUND404Ставка не найдена (операции platform-API).
REWARDS_REFERRAL_RATE_INVALID_STATUS_TRANSITION409Недопустимый переход статуса (например, Activate архивной ставки).

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

СобытиеТипКогдаSnapshot/поля
ReferralRateRuleCreatedEventдоменноеУспешный CreateId, PartnerUserId?, DeveloperCompanyId?, ProjectId?, LayoutGroupId?
ReferralRateRuleActivatedEventдоменноеDraft → ActiveId, Percent, EffectiveFrom/EffectiveTo
ReferralRateRuleArchivedEventдоменное* → ArchivedId, actor

Интеграционные Kafka-события не публикуются — ставка применяется только внутри контекста rewards (при создании реферального начисления); витрин, зависящих от изменения ставок, нет.

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

АспектЗначение
ИндексыPK; ix_referral_rate_rules_status btree; ix_referral_rate_rules_partner btree (partner_user_id) where not null; ix_referral_rate_rules_developer btree (developer_company_id) where not null; ix_referral_rate_rules_project btree (project_id) where not null; ix_referral_rate_rules_layout_group btree (layout_group_id) where not null; ix_referral_rate_rules_created_at btree. Непересечение периодов (INV-RR-2) — app-level при Activate (ставок единицы–десятки, ADR-0056)
Партиционирование / retentionnone — единицы–десятки ставок; retention бессрочный (исторические снапшоты ссылаются)
Concurrencyоптимистическая блокировка (правка не должна затирать чужую) + блокировка активных ставок той же комбинации измерений при Activate (INV-RR-2)
Permissions / PIIУправление и чтение реестра — rewards.referral-rates.manage (платформенный scope, Менеджер ПИН). Партнёру ставка отдельно не отдаётся — применённый процент виден в снапшоте его реферального начисления (rewards.referral.view-own). PII нет
AuditVersion + actor-stamping; применённая версия зафиксирована снапшотом ставки в начислении

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

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

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

  • referral-accrual.md — точка применения (снапшот ставки, расчёт Amount).
  • Факт связи «кто кого привёл» — ../../referral/domain/referral-bond.md; события связей — ../../referral/events/bond-created.md, ../../referral/events/bond-closed.md.
  • Consumer-точка резолюции: ../consumers/crm-deal-completed.md.
  • ADR-0066 (базовые уровни специфичности Partner×Developer > Partner > Developer > Global, «специфичное побеждает»), ADR-0068 (единый паттерн scoped+time-bounded; ось объекта Developer → Project → LayoutGroup, снапшот применённой области и периода), ADR-0052 (tenancy — платформенная сущность), ADR-0056 (simplicity-first — резолюция выборкой в память).