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

POST /api/v1/agency/commission-accruals/{accrualId}/adjust-distribution — Per-deal корректировка долей внутри пула

Точечная правка распределения одного начисления: руководитель агентства перераспределяет пул агентства (CommissionAmount − PlatformFeeAmount) между строками Agency и Agent без смены политики (INV-11 domain/commission-accrual.md). Строка Platform (удержание ПИН) неприкосновенна — сумма и валюта начисления не меняются, меняется только деление пула.

Назначение

Руководитель/администратор агентства корректирует долю конкретного агента по конкретной сделке (индивидуальная договорённость, бонус за сложную сделку, исправление ошибки политики) — не трогая политику агентства и другие начисления. Каждая корректировка оставляет обязательный аудит-след с причиной (CommissionAccrualAdjustmentEntity, Kind = Distribution). Feature group — agency-commission.

Metadata

ПолеЗначение
Сервис/контекстrewards
API areaagency
Feature groupagency-commission
Статусdraft
Документdocs/06-services/rewards/api/agency/adjust-accrual-distribution.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (руководитель/администратор агентства)
Auth policy / Permissionsrewards.commission-policy.manage (распоряжение пулом агентства — то же право, что и управление политикой)
Scope (RBAC)организация: компания Type ∈ {Agency, Broker, Pin}
Record-levelне применяется (доступ на уровне tenant: начисление своего агентства)
Tenant isolationIn-tenant: TenantId из claims; чужое начисление невидимо → 404
Auditupdate: UpdatedByUserId = ActorUserId, Version + 1; обязательная append-only строка корректировки CommissionAccrualAdjustmentEntity (Kind = Distribution, OldAmount/NewAmount — доля агента до/после, Reason, ActorUserId)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agency/commission-accruals/{accrualId}/adjust-distribution
Success status200
Idempotency headerне применяется (повтор с теми же суммами — no-op по значениям, но создаёт вторую аудит-строку; защита от гонок — ExpectedVersion)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ActorUserIdidentity claims (sub)GuidДаRequiredactor-stamping аудита
AccrualIdroute {accrualId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1; сверка с Version начисления (optimistic concurrency)
AgentAmountbodyMoneyModelДаValue >= 0, scale 2; CurrencyId = валюте начисления (INV-6)Sensitive — не логируется
AgencyAmountbodyMoneyModelДаValue >= 0, scale 2; CurrencyId = валюте начисления (INV-6); AgentAmount.Value + AgencyAmount.Value = пул агентстваSensitive — не логируется
ReasonbodystringДа1..1000, не пусто после trim — обязательная причина корректировки (INV-11)

Модель ответа CommissionAccrualModel

Полная проекция начисления с обновлённым распределением — дом модели: README.md («Общие контрактные модели»); Distributions[] содержит три строки (Platform — без изменений, Agency/Agent — с новыми суммами), Version — новый.

Предусловия и постусловия

  • Предусловия. Начисление существует в tenant-е агентства и находится в статусе Confirmed или MoneyReceived (распределение уже построено — три строки INV-4/9); Agent-строка не включена в неотменённое платёжное поручение (PayoutOrderItemId is null). Актор — руководитель/администратор с rewards.commission-policy.manage и актуальным Version.
  • Постусловия (успех). Суммы строк Agency и Agent заменены на переданные; строка Platform, CommissionAmount, PlatformFeeAmount и статус не изменились; Σ трёх строк по-прежнему равна CommissionAmount (INV-5); создана аудит-строка Kind = Distribution с причиной; Version + 1.
  • Постусловия (отказ). Распределение, версия и аудит-история начисления не изменились.

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/администратор агентства; scope — организация (TenantId из claims); требуется право rewards.commission-policy.manage.
  2. Проверка входа. AccrualId непустой, ExpectedVersion >= 1; AgentAmount/AgencyAmountValue >= 0, scale 2; Reason непустой после trim, <= 1000 — иначе REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED. Начисление существует в tenant-е агентства — иначе REWARDS_ACCRUAL_NOT_FOUND. CurrencyId обеих сумм совпадает с валютой начисления (INV-6) — иначе REWARDS_CURRENCY_MISMATCH.
  3. Guard корректировки (INV-11). Статус начисления ∈ {Confirmed, MoneyReceived} и Agent-строка свободна (PayoutOrderItemId is null) — иначе REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN. Version совпадает с ExpectedVersion — иначе REWARDS_CONCURRENCY_CONFLICT. Начисление читается под блокировкой на запись (гонка со сборкой поручения — create-payout-order.md, INV-1 поручения).
  4. Сверка пула. AgentAmount.Value + AgencyAmount.Value = пул агентства (CommissionAmount.Value − PlatformFeeAmount.Value, INV-10) — иначе REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID. Строка Platform в перераспределении не участвует и не меняется ни при каких входных данных (INV-11).
  5. Применение. В одном согласованном изменении: строка Agent получает AgentAmount, строка AgencyAgencyAmount (её AppliedRetentionKind сохраняется как исторический след применённой политики); создаётся аудит-строка CommissionAccrualAdjustmentEntity (Kind = Distribution, OldAmount = прежняя доля агента, NewAmount = AgentAmount, Reason, ActorUserId); Version + 1. Статус начисления не меняется — событие статуса не публикуется. Если начисление в MoneyReceived (счета получателей в wallet уже кредитованы), в том же изменении публикуется компенсирующее событие pin.rewards.commission-distribution-adjusted.v1 с дельтой долей Agency/Agentwallet переносит разницу между счетами получателей (см. «Побочные эффекты»). В Confirmed кошелёк ещё не кредитован — событие не публикуется.
  6. Результат. Полная проекция начисления с обновлённым распределением и новой версией → 200.

Диаграмма

Побочные эффекты

Событие статуса не публикуется (статус не меняется). Компенсация кошелька зависит от статуса начисления на момент корректировки:

  • Confirmed (кошелёк ещё не кредитован — деньги не получены). Никаких интеграционных событий: распределение существует только как строки начисления, балансы wallet его не отражают. Рекомендованный порядок — править доли именно здесь, до регистрации акта.
  • MoneyReceived (счета получателей wallet уже кредитованы по прежнему распределению на момент регистрации акта, domain/platform-receipt.md). В том же изменении публикуется компенсирующее событие pin.rewards.commission-distribution-adjusted.v1 (../../events/commission-distribution-adjusted.md, key = AccrualId) с долями Agency/Agent до и после правки. wallet авто-проводкой Adjustment переносит дельту между счетами AgentCommission и AgencyCommission (../../../wallet/consumers/rewards-commission-distribution-adjusted.md, ADR-0065) — балансы выравниваются под новое деление пула без ручного вмешательства оператора ПИН. Строка Platform и Σ балансов получателей не меняются. Частота таких корректировок учитывается метрикой rewards_distribution_adjust_after_receipt_total.

Ошибки и failure modes

Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED) — дом README.md «Общие ошибки». Дельта операции:

КодHTTP statusУсловиеСообщениеRetry
REWARDS_ACCRUAL_NOT_FOUND404Начисление не существует или принадлежит чужому tenant«Начисление не найдено»no retry
REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED400Reason пустой/отсутствует (INV-11)«Укажите причину корректировки»no retry
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN409Статус вне {Confirmed, MoneyReceived} или Agent-строка уже в неотменённом поручении (INV-11)«Корректировка распределения недоступна в текущем состоянии начисления»no retry
REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID409AgentAmount + AgencyAmount ≠ пулу агентства (INV-5/10)«Сумма долей не равна пулу агентства»no retry (пересчитать по актуальному пулу)
REWARDS_CURRENCY_MISMATCH400CurrencyId сумм не совпадает с валютой начисления (INV-6)«Валюта долей не совпадает с валютой начисления»no retry
REWARDS_CONCURRENCY_CONFLICT409ExpectedVersion не совпал (параллельная сборка поручения / другая корректировка)«Начисление изменено — обновите данные»retry после refresh

Acceptance criteria

  • AC-1 (перераспределение пула). Given начисление Confirmed с распределением Platform = 10 000, Agency = 27 000, Agent = 63 000 (пул 90 000); When руководитель передаёт AgentAmount = 70 000, AgencyAmount = 20 000, Reason; Then 200: строки Agency/Agent заменены, Platform и CommissionAmount не изменились, Σ трёх строк = CommissionAmount (INV-5); создана аудит-строка Distribution (OldAmount = 63 000, NewAmount = 70 000); Version + 1.
  • AC-2 (Platform неприкосновенна). Given любые входные суммы; When корректировка; Then строка Platform не меняется никогда — перераспределяется только пул; вход, где AgentAmount + AgencyAmount ≠ пул, отклоняется REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID (409).
  • AC-3 (граничные доли). Given AgentAmount = 0, AgencyAmount = пул (или наоборот); When корректировка; Then 200 — нулевая доля допустима (строка остаётся с Amount = 0, >= 0 по дому).
  • AC-4 (без причины). Given пустой Reason; When корректировка; Then REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED (400), распределение не изменено.
  • AC-5 (строка в поручении). Given Agent-строка начисления уже включена в неотменённое платёжное поручение; When корректировка; Then REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (409) — сначала исключить строку из черновика поручения (update-payout-order-items.md) либо дождаться терминации поручения.
  • AC-6 (недопустимый статус). Given начисление в PendingConfirmation (строк ещё нет) / Paid / Cancelled; When корректировка; Then REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (409).
  • AC-7 (гонка со сборкой поручения). Given параллельно создаётся поручение, захватывающее Agent-строку; When обе операции; Then обе мутируют начисление под блокировкой — проходит одна; проигравшая корректировка получает REWARDS_CONCURRENCY_CONFLICT или REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (строка уже занята) и перечитывает состояние.
  • AC-8 (корректировка после получения денег). Given начисление MoneyReceived, строка свободна, счета получателей в wallet кредитованы по прежнему распределению; When корректировка (AgentAmount уменьшен на δ, AgencyAmount увеличен на δ); Then 200, инкремент rewards_distribution_adjust_after_receipt_total, публикуется pin.rewards.commission-distribution-adjusted.v1 (OldAgentAmount/NewAgentAmount, OldAgencyAmount/NewAgencyAmount); wallet авто-проводкой Adjustment переносит δ со счёта агента на счёт агентства — балансы выравниваются без ручного вмешательства (см. «Побочные эффекты»).
  • AC-9 (корректировка до получения денег — без события). Given начисление Confirmed; When корректировка; Then 200, компенсирующее событие не публикуется (кошелёк ещё не кредитован).

Совместимость и наблюдаемость

  • Контракт additive-only; CommissionAccrualModel — дом README.md.
  • Logs: AccrualId, TenantId, Status, result (суммы не логируются — Sensitive); metrics: rewards_accrual_adjust_distribution_total{result}, rewards_distribution_adjust_after_receipt_total; traces: OTel, correlation через traceparent; dashboard rewards-overview.
  • НФТ (ADR-0056): мутация одного агрегата; целевая латентность p95 < 300 мс; частота — единичные корректировки (back-office руководителя агентства).

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

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

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