ADR-0068 — Scoped и time-bounded конфигурация комиссионных и реферальных ставок
Статус
Accepted
Дата
2026-07-16
Контекст
Финансовый контур PIN опирается на четыре независимо настраиваемые ставки, но каждая из них сегодня описана как одна активная запись на свой ключ, без слоёв специфичности и без срока действия:
- Ставка застройщика (
reward-ruleвrewards, reward-rule.md) —CommissionPercentот базы (CalculationBase). Задаётся, по сути, на застройщика; нет способа задать другую ставку на отдельный проект или группу планировок и нет способа временно поднять её на период акции (типовой кейс: 10% → 12% на квартал). - Удержание ПИН (
platform-commission-terms, ADR-0066) —RetentionPercentagePoints(0.25–1.50 п.п.) с единственным nullable-измерениемAgencyCompanyIdи резолюцией «per-agency → глобальный дефолт». Нет срока действия и нет измерения по застройщику/проекту; акцию ПИН (временное понижение удержания) выразить нечем. - Распределение агентство↔агент (
agency-commission-policy, agency-commission-policy.md) — доля агента из пула агентской стороны, per-agent override. Нет времянок: агентство не может объявить agent-boost (временно поднять долю агента с 2–4% до 6–7% на период мотивационной кампании) без ручной правки постоянной политики. - Реферальная ставка (
referral-rate-rule, ADR-0066) — уже введена со слоямиPartner×Developer > Partner > Developer > Globalи периодамиEffectiveFrom/EffectiveTo. Это единственный слой, где паттерн «scoped + time-bounded» уже отражён.
Признаки проблемы:
- Нет измерения проекта/группы планировок. Ставка застройщика и удержание ПИН фактически заданы на застройщика целиком, хотя экономика отличается по проектам и по группам планировок (студии против видовых пентхаусов). Резолюция объекта сделки до застройщика уже теряет эту специфичность.
- Акции невыразимы. Любое временное изменение ставки (акция застройщика, промо-понижение удержания ПИН, agent-boost агентства) сейчас требует переписать постоянную запись, а затем вручную вернуть её обратно — без атомарности, без аудита периода и с риском «залипшей» акции.
- Разнобой моделей. Реферальная ставка уже scoped+time-bounded, остальные три — «одна активная запись». ADR-0066 явно оставил per-developer override удержания вне scope v1 («новое nullable-измерение + уровень специфичности поверх текущей резолюции» — additive-расширение). Настало время сделать это расширение и распространить единый паттерн на все четыре слоя.
Экономический пример объекта: объект принадлежит группе планировок LayoutGroup внутри Project внутри
портфеля застройщика Developer. Ставка на объект должна разрешаться до наиболее специфичной активной
записи на момент завершения сделки, а не до записи «на застройщика вообще».
Решение
1. Единый паттерн «scoped + time-bounded» для всех четырёх слоёв ставок
Все четыре конфигурационные сущности (reward-rule, platform-commission-terms,
agency-commission-policy, referral-rate-rule) приводятся к общему паттерну из трёх составляющих:
- Измерения специфичности (scope). Базовая ось общая для всех:
Developer→Project→LayoutGroup(от наименее к наиболее специфичному). Каждое измерение — nullable-поле (DeveloperCompanyId?,ProjectId?,LayoutGroupId?); всеnull= глобальная запись слоя. Дополнительные измерения по слоям:referral-rate-ruleдобавляетPartnerId?(партнёр реферальной программы);platform-commission-termsдобавляетAgencyCompanyId?(удержание ПИН настраивается и по агентству — как в ADR-0066);agency-commission-policyякорится наAgencyCompanyId(обязателен — политика принадлежит агентству) иAgentUserId?, а осью специфичности пользуется той жеDeveloper → Project → LayoutGroup.
- Период действия. Поля
EffectiveFrom(обязателен) /EffectiveTo?(nullable = бессрочно). Акция — это обычная запись той же специфичности с ограниченным периодом; никаких отдельных «промо-флагов» и таблиц. - Резолюция. На момент завершения сделки берётся наиболее специфичная активная запись на
asOf = CompletedAt: среди записей слоя выбираются те, чей период покрываетCompletedAt(EffectiveFrom ≤ CompletedAt < EffectiveToилиEffectiveTo = null), из них — запись с максимальной специфичностью по осям. Порядок специфичности — «наиболее конкретное измерение побеждает» (LayoutGroup > Project > Developer > Global; для рефералаPartnerуточняет ось так же, как в ADR-0066).
2. Непересечение периодов внутри одной специфичности
- Инвариант конфигурации: внутри одной и той же специфичности (одинаковый набор заполненных измерений)
периоды активных записей не пересекаются. Это делает резолв на
asOfдетерминированным — на любой момент времени для данной специфичности активна не более чем одна запись. - Проверка непересечения выполняется при активации/сохранении записи (под блокировкой на ключ
специфичности); нарушение отклоняется бизнес-ошибкой. Слои используют собственные коды несовместимости
(
REWARDS_REWARD_RULE_OVERLAP,REWARDS_PLATFORM_TERMS_OVERLAP,REWARDS_AGENCY_POLICY_OVERLAP,REWARDS_REFERRAL_RATE_OVERLAP). - Разные специфичности сосуществуют свободно: акция на
LayoutGroupможет действовать одновременно с бессрочной базовой записью наDeveloper— резолвер выберетLayoutGroupкак более специфичную.
3. Акции и boost как временные записи
Все три «временных» бизнес-кейса выражаются одинаково — записью той же сущности с ограниченным периодом, без специальных сущностей:
- Акция застройщика (
reward-rule): базовая записьDeveloper= 10% бессрочно; на квартал заводится записьProject= 12% сEffectiveFrom/EffectiveToна границы акции. Сделки внутри периода разрешаются в 12% (более специфичныйProject+ покрывающий период), вне периода — в 10%. - Промо-понижение удержания ПИН (
platform-commission-terms): базовое удержание 1.00 п.п.; временная запись с пониженнымRetentionPercentagePoints(в тех же границах 0.25–1.50 п.п.) на период промо. Удержание по-прежнему считается только в процентных пунктах от базы, диапазон и семантика ADR-0066 сохранены. - Agent-boost агентства (
agency-commission-policy): базовая доля агента 2–4% из пула; временная запись с долей 6–7% на период кампании. Boost делит пул агентской стороны (после удержания ПИН), а не gross — вход политики агентства не меняется относительно ADR-0066.
4. Снапшот применённой области и периода в начисление
- При создании начисления (
commission-accrual/referral-accrual) в него снимается снапшот применённой записи каждого слоя: идентификатор записи, её специфичность (какие измерения заполнены) и период (EffectiveFrom/EffectiveTo) — дополнительно к уже снимаемымPlatformRetentionPercentagePoints/PlatformFeeAmount/AppliedPlatformTermsId(ADR-0066). - Снапшот делает начисление самодостаточным и объяснимым постфактум («ставка 12% применена, потому что сделка попала в акцию проекта X, период 01.04–30.06»). Последующее изменение или архивация записей ставок не пересчитывает уже созданные начисления.
- Удержание ПИН при корректировке gross не пересчитывается: оно снято снапшотом и считается от исходной базы; корректировка выполняется только компенсирующим начислением (согласовано с ADR-0066/0067).
5. Резолвер ставок в crm-deal-completed
- Точка применения паттерна — обработка завершения сделки (
pin.crm.deal-completed.v1) вrewards: резолвер получаетCompletedAtи координаты объекта (DeveloperCompanyId,ProjectId,LayoutGroupId), а такжеAgencyCompanyId/AgentUserId/Partnerиз атрибуции сделки, и разрешает все четыре слоя одним проходом по правилу «наиболее специфичная активная наasOf». - Координаты специфичности объекта (
Project,LayoutGroup) — изcatalog/chessboard;rewardsполучает их в снимке объекта сделки (в событии завершения или репликой), собственной моделью каталога не владеет. - Отсутствие подошедшей записи по слою обрабатывается как в ADR-0066: удержание ПИН → fallback 0 п.п. (warn-метрика), реферальная ставка → начисление не создаётся; ставка застройщика без правила → начисление агентской стороны не создаётся (warn-метрика). Глобальная запись слоя выступает страховочным уровнем.
6. Управление
- Слоями
reward-rule,platform-commission-terms,referral-rate-ruleуправляет Менеджер ПИН (платформенный scope, API-областьplatformвrewards, ADR-0066);agency-commission-policy— в ведении руководителя агентства (organization scope). Заведение акций/boost — это создание временных записей соответствующего слоя, отдельных операций «включить/выключить акцию» не требуется. - Конфигурация ставок публикуется событием
pin.rewards.commission-rules.v1(created/updated/archived записи слоёв) для наблюдаемости и обновления кэшей резолвера; корректировки уже созданных распределений — событиемpin.rewards.commission-distribution-adjusted.v1(компенсирующее перераспределение).
Замещение ADR-0066
Пункт ADR-0066 «Per-developer override (условия «агентство × застройщик») — вне scope v1; расширение
additive: новое nullable-измерение + уровень специфичности поверх текущей резолюции» настоящим ADR
реализуется и замещается. Удержание ПИН получает полную ось Developer → Project → LayoutGroup
(в дополнение к AgencyCompanyId) и период действия; резолюция ADR-0066 «per-agency → глобальный дефолт»
становится частным случаем общего правила «наиболее специфичная активная на asOf». Диапазон
0.25–1.50 п.п., модель «только процентные пункты», разделение начисления на строки Platform/Agency/Agent
и правило «удержание не пересчитывается при корректировке gross» из ADR-0066 сохраняются без изменений.
Рассмотренные альтернативы
| Вариант | Описание | Причина отклонения |
|---|---|---|
| Single-active-per-scope (текущее) | По одной активной записи на ключ слоя, без слоёв специфичности и без периодов | Не тянет ни акции (временную ставку негде выразить, кроме перезаписи постоянной), ни специфичность проекта/группы планировок; уже потребовал исключения в самом ADR-0066 (per-developer override отложен). Не масштабируется на 4 слоя единообразно. |
| Отдельные таблицы промо/акций | Постоянная ставка в основной таблице + отдельная таблица временных промо-записей с приоритетом над базой | Дублирует резолюцию и валидацию в двух местах, плодит вторую модель истины на каждый слой (×4), усложняет снапшот и аудит («из какой таблицы пришла ставка»). Период — естественный атрибут самой записи; промо это просто запись с EffectiveTo, отдельная сущность не нужна. |
| Ставка только на застройщика (без Project/LayoutGroup) | Оставить единственное измерение «застройщик», акции — глобальным флагом на застройщика | Теряет реальную экономику: разные проекты и группы планировок одного застройщика имеют разную маржинальность; акция обычно локальна (один проект/корпус), а не на весь портфель. |
| Пересчитывать начисления при смене ставок | Не снимать снапшот, считать по актуальной конфигурации | Ломает иммутабельность финансовых оснований: смена ставки задним числом переписала бы историю комиссий и рефералки, конфликтует с гейтом MoneyReceived (ADR-0067) и правилом «удержание не пересчитывается». |
Последствия
| Контекст | Последствие |
|---|---|
rewards | Все четыре конфигурационные сущности расширяются осью Developer → Project → LayoutGroup и периодом EffectiveFrom/EffectiveTo; вводится инвариант непересечения периодов внутри специфичности (per-layer коды overlap); резолвер в обработчике pin.crm.deal-completed.v1 разрешает все слои по «наиболее специфичная активная на asOf=CompletedAt»; начисление получает снапшот применённой области и периода по каждому слою; новые топики pin.rewards.commission-rules.v1, pin.rewards.commission-distribution-adjusted.v1. |
crm | Событие завершения сделки pin.crm.deal-completed.v1 несёт координаты специфичности объекта (DeveloperCompanyId, ProjectId, LayoutGroupId) и CompletedAt как asOf; атрибуция (OriginAgencyCompanyId/OriginAgentUserId, текущие ExecutorAgentUserId/ResponsibleAgentUserId, CommissionRecipientUserId) — источник агентских/партнёрских координат резолвера. |
catalog / chessboard | Источник иерархии Developer → Project → LayoutGroup для координат объекта; rewards получает их снимком, собственной модели каталога не заводит. |
Затрагиваемые сервисы
| Сервис | Роль |
|---|---|
rewards | Владелец всех четырёх слоёв ставок, резолвера и снапшота; изменения сущностей, инвариантов и событий. |
crm | Поставщик CompletedAt и координат объекта/атрибуции в событии завершения сделки. |
catalog / chessboard | Источник иерархии специфичности объекта (Developer/Project/LayoutGroup). |
Затронутые контракты и совместимость
| Контракт | Версия | Совместимость | Миграция |
|---|---|---|---|
reward-rule, platform-commission-terms, agency-commission-policy, referral-rate-rule (модели) | 1 | additive: nullable-измерения ProjectId/LayoutGroupId + период | существующие записи трактуются как специфичность Developer/Global, EffectiveFrom = дата заведения, EffectiveTo = null |
pin.rewards.commission-rules.v1 | 1 | новый топик | additive |
pin.rewards.commission-distribution-adjusted.v1 | 1 | новый топик | additive |
pin.crm.deal-completed.v1 | 1 | additive: координаты объекта + CompletedAt | additive-поля, ContractVersion не бампится |
Связанные ADR
- ADR-0066 — удержание ПИН и реферальные ставки; настоящий ADR реализует и замещает его пункт «per-developer override вне scope v1» и распространяет паттерн на все четыре слоя.
- ADR-0048 — денежная арифметика, RUB-only (расчёты ставок/удержаний/долей).
- ADR-0056 — simplicity-first: единый паттерн и период как атрибут записи вместо отдельных промо-таблиц.
- ADR-0067 — гейт
MoneyReceived: снапшот ставок и запрет ретро-пересчёта согласованы с моментом подтверждения денег.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (4): Entity / Aggregate: AgencyCommissionPolicyEntity, Entity / Aggregate: PlatformCommissionTermsEntity, Entity / Aggregate: ReferralRateRuleEntity, Entity / Aggregate: RewardRuleEntity
- API (4): POST /api/v1/platform/commission-terms — Создание условий платформенной комиссии (черновик), POST /api/v1/platform/commission-terms/{termsId}/activate — Активация условий платформенной комиссии, POST /api/v1/platform/referral-rate-rules — Создание ставки реферального вознаграждения (черновик), POST /api/v1/platform/referral-rate-rules/{rateRuleId}/activate — Активация ставки реферального вознаграждения
- Use Cases (2): UC-RWD-015. Настройка индивидуальных условий вознаграждения по области и сроку (акции и boost), UC-RWD-016. Застройщик подтверждает начисление после смены исполнителя
- События (1): Event Contract: pin.rewards.commission-rules (CommissionRulesChangedKafkaEvent)
- Консюмеры (1): Consumer: CRM Deal Completed