Entity / Aggregate: ReferralRateRuleEntity
Дом полей агрегата «Ставка реферального вознаграждения» контекста rewards. Проекции (модели ответов
platform-API, снапшот применённой ставки в реферальном начислении) ссылаются сюда и описывают только
дельту.
Назначение
ReferralRateRuleEntity — настройка Менеджера ПИН, определяющая процент реферального вознаграждения
партнёра от конечной цены (FinalPrice) завершённой сделки приведённого им инвестора
(ADR-0066,
ADR-0068).
Ставки различаются по партнёрам и по иерархии объекта сделки (застройщик → проект → группа планировок)
и ограничены сроками действия (в т.ч. временно повышенные ставки на период акции конкретного проекта или
группы планировок).
Ставки живут в rewards (владелец денег); контекст referral владеет только фактом связи «кто кого
привёл» (../../referral/domain/referral-bond.md) и денег не
считает. Ставка резолвится при создании реферального начисления
(referral-accrual.md) на момент CompletedAt сделки; в начисление снимается
снапшот — последующая смена ставок существующие начисления не пересчитывает.
Классификация
| Поле | Значение |
|---|---|
| Контекст | rewards |
| Kind | AggregateRoot |
| Source of truth | rewards |
| Таблица | rewards.referral_rate_rules |
| Tenant isolation | вне tenant-модели — платформенная сущность (владелец процесса — ПИН, ADR-0052); управление — платформенный scope |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Уникальный идентификатор ставки; попадает в снапшот применённой ставки начисления. |
PartnerUserId | Guid? | Нет | index ix_referral_rate_rules_partner (partner_user_id) where not null; логическая ссылка на identity.users.id, без FK | app | Партнёр, для которого действует ставка; null — любой партнёр. |
DeveloperCompanyId | Guid? | Нет | index ix_referral_rate_rules_developer (developer_company_id) where not null; логическая ссылка на identity.companies.id (Type = Developer), без FK | app | Застройщик, по чьим сделкам действует ставка; null — любой застройщик. Родитель ProjectId в иерархии объекта (INV-RR-4). |
ProjectId | Guid? | Нет | index ix_referral_rate_rules_project (project_id) where not null; логическая ссылка на проект каталога (catalog/chessboard), без FK | app | Проект застройщика, по чьим сделкам действует ставка; null — любой проект застройщика. Требует заполненного DeveloperCompanyId (INV-RR-4). |
LayoutGroupId | Guid? | Нет | index ix_referral_rate_rules_layout_group (layout_group_id) where not null; логическая ссылка на группу планировок каталога (catalog/chessboard), без FK | app | Группа планировок проекта, по чьим объектам действует ставка; null — любая группа проекта. Требует заполненного ProjectId (INV-RR-4). |
Percent | decimal | Да | numeric(5,2), check percent > 0 AND percent <= 100 | app | Процент вознаграждения от FinalPrice сделки (INV-RR-1). |
EffectiveFrom | DateTimeOffset | Да | check effective_to is null or effective_to > effective_from | app | Начало периода действия. |
EffectiveTo | DateTimeOffset? | Нет | — | app | Конец периода действия (исключительно); null — бессрочно. |
Comment | string? | Нет | varchar(2000) | app | Комментарий Менеджера ПИН (основание, условия акции). |
Status | ReferralRateRuleStatus | Да | хранится как text, varchar(32), index ix_referral_rate_rules_status (status) | app | Draft / Active / Archived (stateDiagram ниже). |
CreatedAt | DateTimeOffset | Да | index | app | Момент создания (аудит). |
CreatedByUserId | Guid? | Нет | — | app | Менеджер ПИН, создавший ставку. |
UpdatedAt | DateTimeOffset | Да | — | app | Момент изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
Version | long | Да | >= 1 | app | Доменная версия (инкремент при мутации; идёт в снапшот применённой ставки). |
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 > без объекта).
| Значение | PartnerUserId | DeveloperCompanyId | ProjectId | LayoutGroupId | Специфичность | Описание |
|---|---|---|---|---|---|---|
PartnerDeveloperProjectLayoutGroup | задан | задан | задан | задан | 1 (макс.) | Персональная ставка партнёра по конкретной группе планировок конкретного проекта. |
PartnerDeveloperProject | задан | задан | задан | null | 2 | Персональная ставка партнёра по конкретному проекту застройщика. |
PartnerDeveloper | задан | задан | null | null | 3 | Персональная ставка партнёра по конкретному застройщику. |
Partner | задан | null | null | null | 4 | Персональная ставка партнёра по всем объектам. |
DeveloperProjectLayoutGroup | null | задан | задан | задан | 5 | Ставка по конкретной группе планировок проекта для всех партнёров (акция на группу планировок). |
DeveloperProject | null | задан | задан | null | 6 | Ставка по конкретному проекту застройщика для всех партнёров (акция проекта). |
Developer | null | задан | null | null | 7 | Ставка по сделкам конкретного застройщика для всех партнёров (акция застройщика). |
Global | null | null | null | null | 8 (мин.) | Глобальная ставка — страховочный уровень по умолчанию. |
Порядок резолюции (наиболее специфичное побеждает):
PartnerDeveloperProjectLayoutGroup → PartnerDeveloperProject → PartnerDeveloper → Partner →
DeveloperProjectLayoutGroup → DeveloperProject → Developer → Global.
Резолюция ставки
Выполняется при создании реферального начисления (referral-accrual.md, ветка
consumer-а ../consumers/crm-deal-completed.md). Вход:
PartnerUserId (реферер активной связи), координаты объекта сделки
(DeveloperCompanyId, ProjectId, LayoutGroupId — из снимка объекта сделки, catalog/chessboard),
момент asOf = CompletedAt.
- Отобрать кандидатов:
Status = Active, период покрываетasOf(EffectiveFrom <= asOfи (EffectiveTo is nullилиEffectiveTo > asOf)), измерения совместимы со сделкой (по каждому измерениюnullили равно координате сделки:PartnerUserId,DeveloperCompanyId,ProjectId,LayoutGroupId). Ставок единицы–десятки — выборка целиком (ADR-0056). - Выбрать наиболее специфичный непустой уровень в порядке специфичности:
PartnerDeveloperProjectLayoutGroup→PartnerDeveloperProject→PartnerDeveloper→Partner→DeveloperProjectLayoutGroup→DeveloperProject→Developer→Global(специфичное побеждает; партнёрская ось доминирует над объектной). - Внутри уровня кандидат ровно один — периоды активных ставок одной комбинации измерений не пересекаются (INV-RR-2), поэтому резолюция детерминирована.
- Кандидатов нет → реферальное начисление не создаётся: warn-метрика
rewards_referral_accrual_no_rate_total(глобальная бессрочная ставка — страховочный уровень, заводится первым шагом внедрения).
В начисление снимается снапшот применённой ставки (AppliedRateSnapshot —
referral-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) | PartnerUserId | restrict |
CompanyEntity (застройщик, identity) | reference (логическая, без FK) | DeveloperCompanyId | restrict |
Проект (catalog/chessboard) | reference (логическая, без FK) | ProjectId | restrict |
Группа планировок (catalog/chessboard) | reference (логическая, без FK) | LayoutGroupId | restrict |
ReferralAccrualEntity | reference (обратная, через снапшот) | AppliedRateSnapshot.RateRuleId (логическая) | — (ставка с применениями только архивируется) |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-RR-1 PercentRange | 0 < Percent <= 100 (numeric(5,2)) | проверка входа + db check | REWARDS_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 — досрочное завершение, только сокращение периода (не раньше текущего момента; сокращение пересечений не создаёт); остальные поля — только в Draft | guard по Status в операции изменения | REWARDS_REFERRAL_RATE_ACTIVE_IMMUTABLE |
| INV-RR-4 ScopeHierarchyConsistent | Объектные измерения образуют вложенную цепочку: ProjectId задан ⇒ задан DeveloperCompanyId; LayoutGroupId задан ⇒ задан ProjectId (и, транзитивно, DeveloperCompanyId). PartnerUserId ортогонален и не ограничен | проверка входа + db check | REWARDS_REFERRAL_RATE_SCOPE_HIERARCHY_INVALID |
Коды ошибок
| Код | HTTP | Условие |
|---|---|---|
REWARDS_REFERRAL_RATE_PERCENT_OUT_OF_RANGE | 400 | Процент вне диапазона (0; 100] (INV-RR-1). |
REWARDS_REFERRAL_RATE_PERIOD_INVALID | 400 | EffectiveTo <= EffectiveFrom. |
REWARDS_REFERRAL_RATE_SCOPE_HIERARCHY_INVALID | 400 | Нарушена вложенность измерений объекта: задан потомок без родителя (ProjectId без DeveloperCompanyId или LayoutGroupId без ProjectId) (INV-RR-4). |
REWARDS_REFERRAL_RATE_OVERLAP | 409 | Пересечение периодов активных ставок одной комбинации измерений при активации (INV-RR-2). |
REWARDS_REFERRAL_RATE_ACTIVE_IMMUTABLE | 409 | Попытка изменить Active-ставку сверх сокращения EffectiveTo (INV-RR-3). |
REWARDS_REFERRAL_RATE_NOT_FOUND | 404 | Ставка не найдена (операции platform-API). |
REWARDS_REFERRAL_RATE_INVALID_STATUS_TRANSITION | 409 | Недопустимый переход статуса (например, Activate архивной ставки). |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
ReferralRateRuleCreatedEvent | доменное | Успешный Create | Id, PartnerUserId?, DeveloperCompanyId?, ProjectId?, LayoutGroupId? |
ReferralRateRuleActivatedEvent | доменное | Draft → Active | Id, Percent, EffectiveFrom/EffectiveTo |
ReferralRateRuleArchivedEvent | доменное | * → Archived | Id, 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) |
| Партиционирование / retention | none — единицы–десятки ставок; retention бессрочный (исторические снапшоты ссылаются) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) + блокировка активных ставок той же комбинации измерений при Activate (INV-RR-2) |
| Permissions / PII | Управление и чтение реестра — rewards.referral-rates.manage (платформенный scope, Менеджер ПИН). Партнёру ставка отдельно не отдаётся — применённый процент виден в снапшоте его реферального начисления (rewards.referral.view-own). PII нет |
| Audit | Version + actor-stamping; применённая версия зафиксирована снапшотом ставки в начислении |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (4): Entity / Aggregate: AgencyCommissionPolicyEntity, Entity / Aggregate: PlatformCommissionTermsEntity, Entity / Aggregate: ReferralAccrualEntity, Entity / Aggregate: RewardRuleEntity
- API (10): GET /api/v1/my/referral-accruals — Мои реферальные начисления (партнёр), GET /api/v1/platform/referral-accruals — Реестр реферальных начислений, GET /api/v1/platform/referral-rate-rules — Реестр ставок реферального вознаграждения, POST /api/v1/platform/referral-accruals/{accrualId}/adjust — Корректировка суммы реферального начисления, POST /api/v1/platform/referral-rate-rules — Создание ставки реферального вознаграждения (черновик), POST /api/v1/platform/referral-rate-rules/{rateRuleId}/activate — Активация ставки реферального вознаграждения, POST /api/v1/platform/referral-rate-rules/{rateRuleId}/archive — Архивирование ставки реферального вознаграждения, POST /api/v1/referral-rewards-exports — Постановка Excel-выгрузки реферальных вознаграждений, PUT /api/v1/platform/referral-rate-rules/{rateRuleId} — Изменение ставки реферального вознаграждения, Rewards API — реестр операций и общие контрактные модели
- Use Cases (5): UC-RWD-008. Платформа настраивает ставки реферального вознаграждения, UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата), UC-RWD-014. ПИН корректирует размер реферального вознаграждения партнёра, US-реестр: Реферальный партнёр (US-PRT), US-реестр: Финансовый блок ПИН (US-FIN)
- События (1): Event Contract: pin.rewards.referral-accrual-created (ReferralAccrualCreatedKafkaEvent)
- Консюмеры (1): Consumer: CRM Deal Completed
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- Отчёты (1): Отчет/Export: Реферальные вознаграждения по партнёрам (referral-rewards-by-partner)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (2): Referral Service, Rewards Service
Связанные документы
- 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 — резолюция выборкой в память).