Entity / Aggregate: PlatformCommissionTermsEntity
Дом полей агрегата «Условия платформенной комиссии (удержание ПИН)» контекста rewards. Проекции
(модели ответов platform-API, снапшот применения в начислении) ссылаются сюда и описывают только дельту.
Назначение
PlatformCommissionTermsEntity — настройка Менеджера ПИН, определяющая долю платформы в комиссии по
завершённой сделке: сколько процентных пунктов ставки застройщика ПИН удерживает в свою пользу
(ADR-0066).
Модель монетизации — только пункты ставки (не процент от суммы комиссии): при ставке застройщика 10% и
удержании 1.00 п.п. платформа получает 1% от цены сделки, пул агентской стороны — 9%.
Условия задаются на пересечении измерений специфичности (агентство AgencyCompanyId? и ось объекта
сделки DeveloperCompanyId? → ProjectId? → LayoutGroupId?; все null = глобальный дефолт) и
периода действия (EffectiveFrom/EffectiveTo?) — единый scoped+time-bounded паттерн
(ADR-0068).
Период даёт выразить временные акции ПИН — понижение удержания на срок промо — обычной записью с
ограниченным EffectiveTo, без отдельных промо-флагов. Условия применяются при создании начисления
(commission-accrual.md) consumer-ом завершения сделки
(../consumers/crm-deal-completed.md): резолвится наиболее
специфичная активная запись на момент asOf = CompletedAt сделки, её снапшот (область + период +
процент) снимается в начисление — последующая смена условий существующие начисления не пересчитывает.
Политика агентства (agency-commission-policy.md) делит пул
(gross − удержание ПИН), а не gross.
Классификация
| Поле | Значение |
|---|---|
| Контекст | rewards |
| Kind | AggregateRoot |
| Source of truth | rewards |
| Таблица | rewards.platform_commission_terms |
| Tenant isolation | вне tenant-модели — платформенная сущность (владелец процесса — ПИН, ADR-0052); управление — платформенный scope |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Уникальный идентификатор условий; на него ссылается AppliedPlatformTermsId начисления. |
AgencyCompanyId | Guid? | Нет | index ix_platform_terms_agency (agency_company_id) where not null; логическая ссылка на identity.companies.id (Type ∈ {Agency, Broker}), без FK | app | Измерение специфичности: агентство, для которого действуют условия; null — по всем агентствам. |
DeveloperCompanyId | Guid? | Нет | index ix_platform_terms_developer (developer_company_id) where not null; логическая ссылка на identity.companies.id (Type = Developer), без FK | app | Измерение специфичности: застройщик объекта сделки; null — по всем застройщикам. |
ProjectId | Guid? | Нет | index ix_platform_terms_project (project_id) where not null; логическая ссылка на проект каталога (catalog/chessboard), без FK | app | Измерение специфичности: проект объекта сделки; null — по всем проектам застройщика. |
LayoutGroupId | Guid? | Нет | index ix_platform_terms_layout_group (layout_group_id) where not null; логическая ссылка на группу планировок (catalog/chessboard), без FK | app | Измерение специфичности (самое узкое по оси объекта): группа планировок объекта сделки; null — по всем группам проекта. |
RetentionPercentagePoints | decimal | Да | numeric(4,2), check retention_percentage_points >= 0.25 AND retention_percentage_points <= 1.50 | app | Удержание ПИН в процентных пунктах ставки застройщика (INV-PT-1). |
EffectiveFrom | DateTimeOffset | Да | check effective_to is null or effective_to > effective_from | app | Начало периода действия. |
EffectiveTo | DateTimeOffset? | Нет | — | app | Конец периода действия (исключительно); null — бессрочно. |
Comment | string? | Нет | varchar(2000) | app | Комментарий Менеджера ПИН (основание, договорённость, условия акции). |
Status | PlatformCommissionTermsStatus | Да | хранится как text, varchar(32), index ix_platform_terms_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 PlatformCommissionTermsStatus
| Значение | Описание |
|---|---|
Draft | Черновик: редактируется, в резолюции удержания не участвует. |
Active | Действующие условия: участвуют в резолюции (с учётом периода EffectiveFrom/EffectiveTo и измерений специфичности). Иммутабельны, кроме EffectiveTo (INV-PT-3). |
Archived | Архив: выведены из резолюции навсегда; ссылки AppliedPlatformTermsId из начислений живы, снятый снапшот остаётся источником истины для исторических начислений. |
Enum PlatformTermsScope (специфичность; не хранится — выводится из заполненности измерений)
Специфичность условий определяется заполненностью двух осей: оси объекта сделки
(DeveloperCompanyId → ProjectId → LayoutGroupId, от широкого к узкому) и измерения агентства
(AgencyCompanyId). При резолюции сравниваются лексикографически: сначала глубина по оси объекта
(LayoutGroup > Project > Developer > нет), при равной глубине — наличие агентства (задано уточняет
запись «по всем агентствам»). Итоговый порядок «наиболее специфичное побеждает» —
LayoutGroup > Project > Developer > Agency > Global, с агентским уточнением на каждом уровне объекта:
| Значение | AgencyCompanyId | DeveloperCompanyId | ProjectId | LayoutGroupId | Ранг | Описание |
|---|---|---|---|---|---|---|
AgencyLayoutGroup | задан | задан | задан | задан | 8 | Удержание по группе планировок для конкретного агентства — максимальная специфичность. |
LayoutGroup | null | задан | задан | задан | 7 | Удержание по группе планировок для всех агентств. |
AgencyProject | задан | задан | задан | null | 6 | Удержание по проекту для конкретного агентства. |
Project | null | задан | задан | null | 5 | Удержание по проекту для всех агентств. |
AgencyDeveloper | задан | задан | null | null | 4 | Удержание по застройщику для конкретного агентства. |
Developer | null | задан | null | null | 3 | Удержание по застройщику для всех агентств. |
Agency | задан | null | null | null | 2 | Удержание для конкретного агентства по всем застройщикам. |
Global | null | null | null | null | 1 | Глобальный дефолт — страховочный уровень. |
Ось объекта заполняется сверху вниз: ProjectId без DeveloperCompanyId или LayoutGroupId без
ProjectId/DeveloperCompanyId недопустимы (INV-PT-5).
Резолюция условий и расчёт удержания
Выполняется при создании начисления по завершённой сделке
(../consumers/crm-deal-completed.md). Вход: AgencyCompanyId
сделки и координаты объекта (DeveloperCompanyId, ProjectId, LayoutGroupId — из снимка объекта в
событии завершения, ADR-0068), применённое правило застройщика (CalculationBase, CommissionPercent —
reward-rule.md), суммы цен, момент asOf = CompletedAt сделки.
- Кандидаты. Отобрать условия:
Status = Active, период покрываетasOf(EffectiveFrom <= asOfи (EffectiveTo is nullилиEffectiveTo > asOf)), измерения совместимы со сделкой — каждое nullable-измерение либоnull(шире), либо равно значению сделки (AgencyCompanyId is nullили = агентству;DeveloperCompanyId is nullили = застройщику;ProjectId is nullили = проекту;LayoutGroupId is nullили = группе планировок). Записей единицы — выборка целиком (ADR-0056). - Наиболее специфичная комбинация. Выбрать кандидата с максимальным рангом специфичности
PlatformTermsScope(AgencyLayoutGroup→ … →Global; таблица выше). Внутри одной комбинации измерений кандидат ровно один — периоды активных условий одной специфичности не пересекаются (INV-PT-2), поэтому резолюция детерминирована. - Fallback 0 п.п. Если подошедших условий нет — начисление создаётся без удержания:
PlatformRetentionPercentagePoints = 0,PlatformFeeAmount = 0,AppliedPlatformTermsId = null, строка распределенияPlatformпри подтверждении строится с нулевой суммой. Инкрементируется warn-метрикаrewards_platform_terms_missing_total+ алерт (платформа теряет доход, пока условия не заведены; глобальная бессрочная запись — страховочный уровень). - Guard конфигурации (INV-PT-4). Если
RetentionPercentagePoints >= CommissionPercentприменённого правила (удержание съедает ставку целиком, пул ≤ 0) — начисление не создаётся, событие уходит в DLQ с кодомREWARDS_PLATFORM_RETENTION_EXCEEDS_RULE(ручной разбор Менеджером ПИН: правится либо правило застройщика, либо условия; после исправления событие переигрывается из DLQ). - Расчёт.
PlatformFeeAmount = база × RetentionPercentagePoints / 100(banker's rounding, 2 знака); база — та же, что у применённого правила (CalculationBase: базовая или конечная цена). Пул агентской стороны =CommissionAmount−PlatformFeeAmount; пул делит политика агентства (agency-commission-policy.md).
В начисление снимается снапшот: PlatformRetentionPercentagePoints, PlatformFeeAmount,
AppliedPlatformTermsId, а также применённая область (заполненные измерения → Scope) и период
(EffectiveFrom/EffectiveTo) — дом полей: commission-accrual.md. Снапшот делает
начисление объяснимым постфактум («удержание 0.50 п.п. применено — акция проекта X, период 01.04–30.06»).
При подтверждении начисления строится строка распределения Platform (RecipientUserId = null) — ровно
одна, наравне со строками Agency и Agent; строка Platform неприкосновенна для агентства (per-deal
корректировка распределения перераспределяет только пул, gross и удержание не пересчитываются).
Value objects
VO не используются: условия хранят только пункты ставки (decimal), денежные суммы появляются в
начислении (PlatformFeeAmount — commission-accrual.md).
Модели JSONB
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
CompanyEntity (агентство, identity) | reference (логическая, без FK) | AgencyCompanyId | restrict |
CompanyEntity (застройщик, identity) | reference (логическая, без FK) | DeveloperCompanyId | restrict |
Проект каталога (catalog/chessboard) | reference (логическая, без FK) | ProjectId | restrict |
Группа планировок (catalog/chessboard) | reference (логическая, без FK) | LayoutGroupId | restrict |
CommissionAccrualEntity | reference (обратная) | commission_accruals.applied_platform_terms_id → platform_commission_terms.id | restrict — условия с применениями только архивируются |
Инварианты и переходы состояний
| Инвариант | Условие | Где проверяется | Ошибка |
|---|---|---|---|
| INV-PT-1 RetentionRange | 0.25 <= RetentionPercentagePoints <= 1.50 (numeric(4,2)) | проверка входа + db check | REWARDS_PLATFORM_TERMS_RETENTION_OUT_OF_RANGE |
| INV-PT-2 NoOverlapWithinSpecificity | Периоды [EffectiveFrom, EffectiveTo|∞) активных условий с одинаковым набором заполненных измерений (AgencyCompanyId, DeveloperCompanyId, ProjectId, LayoutGroupId), включая null-значения, не пересекаются — на любой момент внутри одной специфичности активна не более чем одна запись, резолюция детерминирована | операция Activate, app-level: активные условия той же комбинации измерений читаются под блокировкой, пересечение периодов отклоняется | REWARDS_PLATFORM_TERMS_OVERLAP |
| INV-PT-3 ImmutableWhenActive | В Active меняется только EffectiveTo — досрочное завершение, только сокращение периода (не раньше текущего момента; сокращение пересечений не создаёт); измерения и RetentionPercentagePoints — только в Draft (сохраняет воспроизводимость снапшотов начислений) | guard по Status в операции изменения | REWARDS_PLATFORM_TERMS_ACTIVE_IMMUTABLE |
| INV-PT-4 RetentionBelowRulePercent | RetentionPercentagePoints < CommissionPercent применённого правила застройщика — пул агентской стороны строго положителен | создание начисления (consumer завершения сделки, шаг 4 резолюции) | REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE → DLQ, ручной разбор |
| INV-PT-5 ObjectAxisTopDown | Ось объекта заполняется сверху вниз: ProjectId требует DeveloperCompanyId, LayoutGroupId требует ProjectId (иначе комбинация измерений некорректна) | проверка входа при Create/Activate | REWARDS_PLATFORM_TERMS_SCOPE_INVALID |
Коды ошибок
| Код | HTTP | Условие |
|---|---|---|
REWARDS_PLATFORM_TERMS_RETENTION_OUT_OF_RANGE | 400 | Пункты удержания вне диапазона 0.25–1.50 (INV-PT-1). |
REWARDS_PLATFORM_TERMS_PERIOD_INVALID | 400 | EffectiveTo <= EffectiveFrom. |
REWARDS_PLATFORM_TERMS_SCOPE_INVALID | 400 | Ось объекта заполнена не сверху вниз (INV-PT-5). |
REWARDS_PLATFORM_TERMS_OVERLAP | 409 | Пересечение периодов активных условий одной специфичности при активации (INV-PT-2). |
REWARDS_PLATFORM_TERMS_ACTIVE_IMMUTABLE | 409 | Попытка изменить Active-условия сверх сокращения EffectiveTo (INV-PT-3). |
REWARDS_PLATFORM_TERMS_NOT_FOUND | 404 | Условия не найдены (операции platform-API). |
REWARDS_PLATFORM_TERMS_INVALID_STATUS_TRANSITION | 409 | Недопустимый переход статуса (например, Activate архивной записи). |
REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE | — (DLQ) | Удержание ≥ ставки применённого правила при создании начисления (INV-PT-4); событие в DLQ, начисление не создано. |
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
PlatformTermsCreatedEvent | доменное | Успешный Create | Id, AgencyCompanyId?, DeveloperCompanyId?, ProjectId?, LayoutGroupId? |
PlatformTermsActivatedEvent | доменное | Draft → Active | Id, Scope, RetentionPercentagePoints, EffectiveFrom/EffectiveTo |
PlatformTermsArchivedEvent | доменное | * → Archived | Id, actor |
Интеграционные Kafka-события не публикуются — условия применяются только внутри контекста rewards
(при создании начисления); внешним потребителям их изменения не нужны.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_platform_terms_status btree; ix_platform_terms_agency btree (agency_company_id) where not null; ix_platform_terms_developer btree (developer_company_id) where not null; ix_platform_terms_project btree (project_id) where not null; ix_platform_terms_layout_group btree (layout_group_id) where not null; ix_platform_terms_created_at btree. Непересечение периодов (INV-PT-2) — app-level при Activate (записей единицы, ADR-0056) |
| Партиционирование / retention | none — единицы записей на комбинацию измерений + глобальный дефолт; retention бессрочный (asOf-резолюция по историческим периодам, исторические снапшоты ссылаются) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) + блокировка активных условий той же комбинации измерений при Activate (INV-PT-2) |
| Permissions / PII | Управление — rewards.platform-terms.manage, чтение реестра — rewards.platform-terms.view (платформенный scope, Менеджер ПИН). Агентству сами условия не отдаются — применённое удержание видно в снапшоте начисления (PlatformRetentionPercentagePoints, PlatformFeeAmount). PII нет |
| Audit | Version + actor-stamping; история условий = записи со своими периодами EffectiveFrom/EffectiveTo и специфичностью, применённая версия зафиксирована AppliedPlatformTermsId (+ снапшот области/периода) в начислении |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: AgencyCommissionPolicyEntity, Entity / Aggregate: CommissionAccrualEntity, Entity / Aggregate: PlatformReceiptEntity
- API (10): GET /api/v1/platform/commission-terms — Реестр условий платформенной комиссии, POST /api/v1/agency/commission-accruals/{accrualId}/adjust-distribution — Per-deal корректировка долей внутри пула, POST /api/v1/developer/commission-accruals/{accrualId}/confirm — Подтверждение начисления застройщиком, POST /api/v1/platform/commission-accruals/{accrualId}/adjust — Корректировка суммы начисления (gross), POST /api/v1/platform/commission-terms — Создание условий платформенной комиссии (черновик), POST /api/v1/platform/commission-terms/{termsId}/activate — Активация условий платформенной комиссии, POST /api/v1/platform/commission-terms/{termsId}/archive — Архивирование условий платформенной комиссии, POST /internal/v1/commission/calculate-property-object — Расчёт комиссии по объекту недвижимости (internal, s2s для catalog), PUT /api/v1/platform/commission-terms/{termsId} — Изменение условий платформенной комиссии, Rewards API — реестр операций и общие контрактные модели
- Use Cases (6): UC-RWD-002. Агент видит комиссию на объекте недвижимости в каталоге, UC-RWD-003. Сделка завершена — начисление комиссии (system), UC-RWD-004. Застройщик подтверждает начисление комиссии, UC-RWD-007. Платформа настраивает условия удержания ПИН, UC-RWD-015. Настройка индивидуальных условий вознаграждения по области и сроку (акции и boost), US-реестр: Финансовый блок ПИН (US-FIN)
- Консюмеры (1): Consumer: CRM Deal Completed
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (1): Rewards Service
Связанные документы
- commission-accrual.md — точка применения (снапшот-поля
PlatformRetentionPercentagePoints/PlatformFeeAmount/AppliedPlatformTermsId, строка распределенияPlatform). - agency-commission-policy.md — политика агентства делит пул (gross − удержание ПИН).
- reward-rule.md — ставка застройщика (
CommissionPercent,CalculationBase) — источник guard-а INV-PT-4. - Consumer-точка резолюции: ../consumers/crm-deal-completed.md.
- Валюта денежных полей начисления:
CurrencyId short+CurrencySnapshot— ../../handbook/domain/snapshots.md. - ADR-0068
(единый scoped+time-bounded паттерн: ось
Developer → Project → LayoutGroup, период, непересечение, снапшот области/периода; замещает пункт ADR-0066 «per-developer override вне scope v1»), referral-rate-rule.md (эталон периодной резолюции того же паттерна). - ADR-0066
(модель удержания, три строки распределения),
ADR-0067 (гейт
MoneyReceived), ADR-0052 (tenancy — платформенная сущность), ADR-0048 (денежная арифметика, RUB-only).