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

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) приводятся к общему паттерну из трёх составляющих:

  1. Измерения специфичности (scope). Базовая ось общая для всех: DeveloperProjectLayoutGroup (от наименее к наиболее специфичному). Каждое измерение — nullable-поле (DeveloperCompanyId?, ProjectId?, LayoutGroupId?); все null = глобальная запись слоя. Дополнительные измерения по слоям:
    • referral-rate-rule добавляет PartnerId? (партнёр реферальной программы);
    • platform-commission-terms добавляет AgencyCompanyId? (удержание ПИН настраивается и по агентству — как в ADR-0066);
    • agency-commission-policy якорится на AgencyCompanyId (обязателен — политика принадлежит агентству) и AgentUserId?, а осью специфичности пользуется той же Developer → Project → LayoutGroup.
  2. Период действия. Поля EffectiveFrom (обязателен) / EffectiveTo? (nullable = бессрочно). Акция — это обычная запись той же специфичности с ограниченным периодом; никаких отдельных «промо-флагов» и таблиц.
  3. Резолюция. На момент завершения сделки берётся наиболее специфичная активная запись на 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 (модели)1additive: nullable-измерения ProjectId/LayoutGroupId + периодсуществующие записи трактуются как специфичность Developer/Global, EffectiveFrom = дата заведения, EffectiveTo = null
pin.rewards.commission-rules.v11новый топикadditive
pin.rewards.commission-distribution-adjusted.v11новый топикadditive
pin.crm.deal-completed.v11additive: координаты объекта + CompletedAtadditive-поля, ContractVersion не бампится

Связанные ADR

  • ADR-0066 — удержание ПИН и реферальные ставки; настоящий ADR реализует и замещает его пункт «per-developer override вне scope v1» и распространяет паттерн на все четыре слоя.
  • ADR-0048 — денежная арифметика, RUB-only (расчёты ставок/удержаний/долей).
  • ADR-0056 — simplicity-first: единый паттерн и период как атрибут записи вместо отдельных промо-таблиц.
  • ADR-0067 — гейт MoneyReceived: снапшот ставок и запрет ретро-пересчёта согласованы с моментом подтверждения денег.

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

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