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

ADR-0066 — удержание ПИН и реферальные ставки

Статус

Accepted

Дата

2026-07-15

Контекст

В финансовом контуре rewards посчитана только агентская сторона комиссии, а доля самой платформы и реферальные деньги не формализованы:

  • Доли ПИН нет вовсе. Начисление (rewards/domain/commission-accrual.md) считает gross-комиссию по правилу застройщика (CommissionAmount = база × CommissionPercent / 100, reward-rule.md) и при подтверждении распределяет её двумя строками Agent + Agency по политике агентства (agency-commission-policy.md) — от gross. Вознаграждение ПИН как площадки нигде не фиксируется, не начисляется и не отчитывается.
  • Реферальные деньги повисли между контекстами. referral владеет фактом связи «кто кого привёл» (ReferralBond) и явно декларирует границу: деньги — зона rewards, начисления строятся по pin.referral.bonds.v1 (referral/README.md, решение №7). Но в rewards нет ни ставок реферального вознаграждения, ни реферальных начислений, ни обещанного consumer-а связей — обязательство «основание будущих выплат» ничем не подкреплено.
  • Решение владельца по модели монетизации. ПИН удерживает часть ставки застройщика в процентных пунктах (0.25–1.50 п.п.), остальная ставка достаётся агентской стороне; реферальные партнёры получают процент от конечной цены сделки, ставки различаются по партнёрам и застройщикам и могут временно повышаться (акции).

Решение

1. Удержание ПИН — только процентные пункты от ставки застройщика

  • Вводится сущность PlatformCommissionTerms (контекст rewards, платформенный scope): AgencyCompanyId? (nullглобальный дефолт для всех агентств), RetentionPercentagePoints — жёсткий диапазон 0.25–1.50 п.п., статусы Draft / Active / Archived.
  • Модель расчёта: PlatformFeeAmount = база × RetentionPercentagePoints / 100, где база — та же, что у применённого правила застройщика (CalculationBase: базовая или конечная цена). Пул агентской стороны = CommissionAmount (gross) − PlatformFeeAmount. Политика агентства делит пул, а не gross (вход алгоритма распределения в agency-commission-policy.md меняется соответственно).
  • Резолюция условий на момент завершения сделки: активные per-agency условия → глобальный дефолт → fallback 0 п.п. (начисление создаётся без удержания, warn-метрика rewards_platform_terms_missing_total + алерт).
  • В начисление снимается снапшот на момент DealCompletedAt: PlatformRetentionPercentagePoints, PlatformFeeAmount, AppliedPlatformTermsId; смена условий не пересчитывает существующие начисления.
  • Удержание — процентные пункты цены, а не доля комиссии. PlatformFeeAmount считается от базы начисления (CalculationBase), а не от CommissionAmount. Как следствие, при последующей корректировке gross-комиссии (компенсирующее начисление, ADR-0067) удержание ПИН не пересчитывается — оно зафиксировано в пунктах цены и остаётся привязанным к базе, а не плавает вслед за суммой комиссии.
  • Guard корректности конфигурации: удержание не может съесть ставку целиком — RetentionPercentagePoints < CommissionPercent применённого правила; нарушение при создании начисления → DLQ REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE (ручной разбор).
  • Многомерная scoped+time-bounded модель условий удержания (измерения DeveloperProjectLayoutGroup c периодами EffectiveFrom/EffectiveTo и акциями, резолюция «наиболее специфичная активная на asOf = CompletedAt») вынесена в ADR-0068; настоящее решение фиксирует базовый инвариант удержания (пункты цены, снапшот, guard), поверх которого ADR-0068 добавляет измерения специфичности additive.

Сквозной пример: цена сделки 10 000 000 ₽, ставка застройщика 10% → gross-комиссия 1 000 000 ₽. Удержание ПИН 1.00 п.п. → PlatformFeeAmount = 10 000 000 × 1 / 100 = 100 000 ₽; пул агентской стороны — 900 000 ₽, то есть 9% от цены сделки. Внутри агентства пул делит политика с per-agent оверрайдами: агенту А настроена доля 3% от цены (300 000 ₽ агенту, 600 000 ₽ — удержание агентства), агенту Б — 5% (500 000 ₽ агенту, 400 000 ₽ агентству). Доля ПИН при этом одинакова и не зависит от внутренних настроек агентства.

2. Распределение начисления — три строки: Platform + Agency + Agent

  • Enum CommissionRecipientType расширяется значением Platform (строка удержания ПИН, RecipientUserId = null — получатель не персона, а платформа).
  • При подтверждении начисления строится ровно три строки распределения: Platform (= PlatformFeeAmount), Agency (удержание агентства из пула), Agent (остаток пула). Инварианты: по одной строке каждого типа; Σ строк = CommissionAmount (инварианты INV-5/INV-9 начисления переоформляются с «двух строк» на «три»).
  • Строка Platform неприкосновенна для агентства: per-deal корректировка распределения руководителем агентства перераспределяет только Agency/Agent внутри пула.

3. Реферальные ставки — сущность rewards, 4 уровня специфичности

  • Ставки живут в rewards (владелец денег), а не в referral: referral владеет только фактом связи «кто кого привёл» и денег не считает (его README, решение №7).
  • Вводится сущность ReferralRateRule: PartnerUserId?, DeveloperCompanyId? (обе null = глобальная ставка), Percent — процент от FinalPrice (конечной цены) сделки, EffectiveFrom / EffectiveTo? — сроки действия (в т.ч. временно повышенные ставки на период акции), статусы Draft / Active / Archived.
  • Резолюция «специфичное побеждает»: Partner×Developer > Partner > Developer > Global. Внутри одной специфичности периоды активных правил не пересекаются — проверка при активации (REWARDS_REFERRAL_RATE_OVERLAP), поэтому резолв на момент сделки всегда детерминирован.
  • Нет подошедшего правила на момент завершения сделки → реферальное начисление не создаётся (warn-метрика; глобальная ставка выступает страховочным уровнем).

4. Реферальное начисление — отдельная сущность ReferralAccrual

  • Реферальное вознаграждение — отдельная сущность, а не kind-дискриминатор на CommissionAccrual: другой получатель (партнёр-пользователь, не агент/агентство), нет распределения по политике агентства, собственный жизненный цикл.
  • Ключевые поля: DealId (unique — одно реферальное начисление на сделку), BondId (основание — связь из referral), PartnerUserId, CommissionAccrualId? (родительское агентское начисление), снапшот применённой ставки, Amount = FinalPrice × Percent / 100.
  • Статусы: Pending → Approved → Paid | Cancelled. Approved проставляется автоматически при переходе родительского начисления в MoneyReceived (гейт «ПИН подтвердил получение денег», ADR-0067); Paid — по платёжному поручению вида ReferralReward (tenant — ПИН); Cancelled — отмена сделки / отзыв связи / ручная коррекция.
  • Guard самореферала: если реферер связи совпадает с агентом-исполнителем сделки (ReferrerUserId == AgentUserId) — начисление не создаётся (skip + метрика), самому себе партнёрка не платится.
  • Основание — реплика связей: rewards консюмит pin.referral.bonds.v1 (закрывается задекларированный, но отсутствовавший consumer); закрытие связи (bond-closed) прекращает создание новых начислений, уже созданные остаются.

5. Управление — платформенный scope

  • Условиями платформы и реферальными ставками управляет Менеджер ПИН (платформенный scope); в rewards вводится новая API-область platform (CRUD/activate/archive условий и ставок, реестр реферальных начислений).
  • Permissions: rewards.platform-terms.view / rewards.platform-terms.manage, rewards.referral-rates.manage; партнёр видит свои реферальные начисления по rewards.referral.view-own (record-level по PartnerUserId).

Область Действия

ОбластьВходитНе входит
Сервисыrewards (условия/ставки/начисления/API platform), referral (только ссылки на rewards-артефакты вместо деклараций), wallet (счета ReferralCommission/PlatformRevenue, ADR-0065), identity (permissions), notificationscrm (статусы/операции сделки не меняются), chessboard, catalog
КонтрактыCommissionRecipientType + Platform; новый топик pin.rewards.referral-accruals.v1; consumer pin.referral.bonds.v1 в rewardsконтракт pin.referral.bonds.v1 не меняется; scoped-измерения удержания (Developer/Project/LayoutGroup + периоды) — ADR-0068
ДанныеPlatformCommissionTerms, ReferralRateRule, ReferralAccrual, реплика связей в rewards; снапшот-поля платформы в начислениипересчёт исторических начислений; частичные удержания
Эксплуатацияwarn-метрики fallback-условий и «нет ставки»; DLQ REWARDS_PLATFORM_RETENTION_EXCEEDS_RULEбанковская интеграция выплат (решение №9 ТЗ)

Последствия

КонтекстПоследствие
rewards+3 агрегата (PlatformCommissionTerms, ReferralRateRule, ReferralAccrual) и реплика связей; начисление получает снапшот удержания и три строки распределения (INV-5/INV-9 переоформляются); вход политики агентства — пул, не gross; платёжные поручения получают вид ReferralReward; новая API-область platform.
referralНичего не считает по деньгам — в README/событиях декларации заменяются конкретными ссылками на ставки/начисления/consumer в rewards.
walletКредиты по строке Platform — на счёт PlatformRevenue; по реферальным начислениям — на счёт ReferralCommission партнёра (ADR-0065).
identityКаталог permissions пополняется rewards.platform-terms.*, rewards.referral-rates.manage, rewards.referral.view-own.
notificationsУведомления партнёру о создании/подтверждении/выплате реферального начисления (топик pin.rewards.referral-accruals.v1).

Затронутые контракты и совместимость

КонтрактВерсияСовместимостьМиграция
CommissionRecipientType (+Platform) в распределении начисления1greenfield, in-place (ContractVersion=1)потребители Distributions обязаны обрабатывать три типа
pin.rewards.referral-accruals.v1 (created / status-changed)1новый топикadditive
pin.referral.bonds.v11не меняется+новый потребитель rewards (реплика связей)
pin.rewards.commission-money-received.v1 (ADR-0067)1новый топикнесёт Distributions[] уже с Platform-строкой

Альтернативы (рассмотрено и отклонено)

ВариантОписаниеПричина отклонения
Удержание ПИН — процент от суммы комиссииPlatformFee = CommissionAmount × n%Отклонено решением владельца: модель монетизации сформулирована в пунктах ставки — доля ПИН фиксирована относительно цены сделки и не плавает вслед за ставкой застройщика; «1 п.п. из 10%» прозрачно объясняется агентствам.
Договорная ставка агентства, остаток — ПИНАгентству гарантируется x% от цены, всё сверх x% уходит платформеОтклонено решением владельца: остаточная модель непрозрачна (доход ПИН скачет от правила к правилу), требует пер-агентских переговоров по каждому застройщику и стимулирует конфликты вокруг ставок.
Реферальные ставки в контексте referralreferral хранит ставки и считает суммы вознагражденийОтклонено: referral не считает деньги (его собственная граница, README решение №7) — появилась бы вторая денежная зона истины, дублирование Money-правил/валютных инвариантов и связка ставок с политиками/гейтом выплат через два контекста.
Kind-дискриминатор на CommissionAccrualРеферальное начисление — та же сущность с Kind = ReferralОтклонено: ломает инварианты и индексы — unique DealId уже занят агентским начислением; инварианты распределения (три строки, политика агентства) и confirm-актор (застройщик) к рефералке неприменимы; статус-машина обросла бы условными ветками. Отдельная сущность проще и честнее.

Риски

РискМитигация
Fallback 0 п.п.: платформа молча теряет доход, пока условия не заведеныwarn-метрика rewards_platform_terms_missing_total + алерт; глобальный дефолт заводится первым шагом внедрения
Ошибка конфигурации: удержание ≥ ставки правила застройщика (пул ≤ 0)guard при создании начисления → DLQ REWARDS_PLATFORM_RETENTION_EXCEEDS_RULE, ручной разбор Менеджером ПИН
Гонка активации двух ставок одной специфичности с пересекающимися периодамипроверка непересечения при Activate под блокировкой + REWARDS_REFERRAL_RATE_OVERLAP
Самореферал через вторую учётку (guard ловит только прямое совпадение UserId)осознанное ограничение v1; admin-коррекции связей (rebind/revoke в referral) остаются инструментом фрод-разбора
Ретро-снапшот: смена условий/ставок ожидаемо не влияет на созданные начислениязафиксировано в модели (снапшот на DealCompletedAt); корректировки — только явными операциями ADR-0067

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

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

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