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

POST /api/v1/agency/commission-adjustment-requests/{requestId}/resolve — Решение по заявке агента на корректировку доли

Руководитель агентства разрешает заявку агента (CommissionAdjustmentRequestEntity, Pending) на корректировку его доли: одобряет — и тогда предложение исполняется как Distribution-корректировка (перераспределение пула AgencyAgent той же логикой, что прямая правка adjust-accrual-distribution.md), либо отклоняет — распределение не меняется. Строка Platform неприкосновенна, сумма и валюта начисления не меняются (INV-11/INV-12 domain/commission-accrual.md). Завершающее звено схемы «агент запрашивает → руководитель одобряет».

Назначение

Дать руководителю/администратору агентства решение по инициативе агента: одобрить пересмотр доли (исполнив корректировку с обоснованием из заявки) или отклонить с комментарием. Одобрение — единственный путь, которым предложение агента превращается в фактическое изменение строк распределения: право распоряжаться пулом остаётся у руководителя. Feature group — agency-commission.

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agency/commission-adjustment-requests/{requestId}/resolve
Success status200
Idempotency headerне применяется; повтор по уже разрешённой заявке — REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS (заявка не в Pending); защита от гонок — ExpectedVersion начисления при одобрении
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
ActorUserIdidentity claims (sub)GuidДаRequiredactor-stamping (резолвер)
RequestIdroute {requestId}GuidДаRequired, непустой
DecisionbodyAdjustmentRequestDecisionДаenum-строка Approved/Rejected
ExpectedVersionbodylongПри Decision = Approved>= 1; сверка с Version начисления (optimistic concurrency)
ResolutionCommentbodystring?При Decision = Rejected — обязателен; при Approved — опционален1..1000, не пусто после trim (для Rejected) — обоснование отказа

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

Проекция разрешённой заявки — дом модели: README.md. Возвращается заявка с новым Status (Approved/Rejected), ResolvedByUserId, ResolvedAt, ResolutionComment; при ApprovedAppliedAdjustmentId (id созданной аудит-строки Distribution). Обновлённое распределение начисления (при одобрении) читается через adjust-accrual-distribution.md-модель CommissionAccrualModel / list-agency-accruals.md.

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

  • Предусловия. Заявка существует в tenant-е агентства и находится в статусе Pending. Актор — руководитель/администратор с rewards.commission.adjust-approve. Для Approved: связанное начисление ∈ {Confirmed, MoneyReceived}, Agent-строка свободна (PayoutOrderItemId is null), ProposedAgentAmount заявки <= актуальному пулу (пул мог измениться gross-корректировкой после подачи), ExpectedVersion = Version начисления.
  • Постусловия (одобрение). Строки Agency/Agent начисления заменены: Agent = ProposedAgentAmount, Agency = пул − ProposedAgentAmount; строка Platform, CommissionAmount, PlatformFeeAmount и статус не изменились; Σ трёх строк = CommissionAmount (INV-5). Создана аудит-строка Kind = Distribution (OldAmount = прежняя доля агента, NewAmount = ProposedAgentAmount, Reason из заявки); Version + 1; заявка → Approved, AppliedAdjustmentId заполнен. При MoneyReceived — публикуется компенсирующее pin.rewards.commission-distribution-adjusted.v1 (перенос дельты между счетами wallet, см. «Побочные эффекты»).
  • Постусловия (отклонение). Заявка → Rejected с ResolutionComment; распределение, Version и суммы начисления не изменены.
  • Постусловия (отказ). Заявка и начисление не изменены.

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/администратор агентства; scope — организация (TenantId из claims); требуется право rewards.commission.adjust-approve.
  2. Проверка входа. RequestId непустой; Decision ∈ {Approved, Rejected}. Для RejectedResolutionComment непустой после trim, <= 1000 (иначе ValidationError). Заявка существует в tenant-е агентства — иначе REWARDS_ADJUSTMENT_REQUEST_NOT_FOUND. Заявка в статусе Pending — иначе REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS (уже разрешена/повторное решение).
  3. Ветка Rejected. Заявка переводится в Rejected (ResolvedByUserId, ResolvedAt, ResolutionComment). Распределение начисления не трогается; событие статуса начисления не публикуется. Публикуется commission-adjustment-request-status-changed (Rejected) — уведомление агенту.
  4. Ветка Approved — исполнение корректировки. Связанное начисление читается под блокировкой на запись. Guard INV-11/INV-12: статус ∈ {Confirmed, MoneyReceived} и Agent-строка свободна (PayoutOrderItemId is null) — иначе REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN; ExpectedVersion = Version — иначе REWARDS_CONCURRENCY_CONFLICT; ProposedAgentAmount заявки <= актуальному пулу (CommissionAmount − PlatformFeeAmount, INV-10) — иначе REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID (пул изменился с момента подачи; заявку следует переподать). В одном согласованном изменении: строка AgentProposedAgentAmount, строка Agencyпул − ProposedAgentAmount (строка Platform не меняется); создаётся аудит-строка CommissionAccrualAdjustmentEntity (Kind = Distribution, OldAmount = прежняя доля агента, NewAmount = ProposedAgentAmount, Reason из заявки, ActorUserId = руководитель); Version + 1; заявка → Approved, AppliedAdjustmentId ← id строки. Статус начисления не меняется.
  5. Компенсация кошелька. Если начисление в MoneyReceived (счета wallet кредитованы по прежнему делению), в том же изменении публикуется pin.rewards.commission-distribution-adjusted.v1 с дельтой долей Agency/Agentwallet авто-проводкой Adjustment переносит разницу между счетами получателей (идентично adjust-accrual-distribution.md). В Confirmed кошелёк ещё не кредитован — событие не публикуется. Публикуется commission-adjustment-request-status-changed (Approved) — уведомление агенту.
  6. Результат. Проекция разрешённой заявки (CommissionAdjustmentRequestModel) → 200.

Диаграмма

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

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

Ошибки и failure modes

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

КодHTTP statusУсловиеСообщениеRetry
REWARDS_ADJUSTMENT_REQUEST_NOT_FOUND404Заявка не существует или принадлежит чужому tenant«Заявка не найдена»no retry
REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS409Заявка не в статусе Pending (уже одобрена/отклонена)«Заявка уже разрешена»no retry
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN409При одобрении: статус начисления вне {Confirmed, MoneyReceived} или Agent-строка в неотменённом поручении (INV-11)«Корректировка недоступна в текущем состоянии начисления»no retry
REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID409При одобрении: ProposedAgentAmount заявки превышает актуальный пул (пул изменился после подачи, INV-5/10)«Предлагаемая доля превышает пул агентства»no retry (переподать заявку)
REWARDS_CONCURRENCY_CONFLICT409При одобрении: ExpectedVersion не совпал с Version начисления«Начисление изменено — обновите данные»retry после refresh

Acceptance criteria

  • AC-1 (одобрение исполняет корректировку). Given заявка Pending (ProposedAgentAmount = 70 000) по начислению Confirmed с Platform = 10 000, Agency = 27 000, Agent = 63 000 (пул 90 000); When руководитель Decision = Approved, ExpectedVersion совпадает; Then 200: Agent = 70 000, Agency = 20 000, Platform не изменён, Σ = CommissionAmount (INV-5); аудит-строка Distribution (OldAmount = 63 000, NewAmount = 70 000, Reason из заявки, ActorUserId = руководитель); Version + 1; заявка Approved, AppliedAdjustmentId заполнен.
  • AC-2 (отклонение не меняет распределение). Given заявка Pending; When Decision = Rejected, ResolutionComment; Then 200: заявка Rejected с комментарием; строки распределения, Version и суммы начисления не изменены; уведомление агенту.
  • AC-3 (отказ без комментария). Given Decision = Rejected, пустой ResolutionComment; When решение; Then 400 ValidationError — комментарий обязателен при отклонении.
  • AC-4 (повторное решение). Given заявка уже Approved/Rejected; When повторный resolve; Then REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS (409).
  • AC-5 (пул изменился после подачи). Given заявка ProposedAgentAmount = 70 000, но пул уменьшился gross-корректировкой ниже 70 000; When одобрение; Then REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID (409) — заявка не исполняется, требуется переподача.
  • AC-6 (строка в поручении). Given Agent-строка начисления включена в неотменённое поручение; When одобрение; Then REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (409).
  • AC-7 (гонка одобрения со сборкой поручения). Given параллельно собирается поручение, захватывающее Agent-строку; When обе операции; Then обе мутируют начисление под блокировкой — проходит одна; проигравшее одобрение получает REWARDS_CONCURRENCY_CONFLICT или REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN и перечитывает состояние (заявка остаётся Pending).
  • AC-8 (одобрение после получения денег). Given заявка по начислению MoneyReceived, строка свободна, счета wallet кредитованы по прежнему делению; When одобрение; Then 200, инкремент rewards_distribution_adjust_after_receipt_total, публикуется pin.rewards.commission-distribution-adjusted.v1; wallet авто-проводкой переносит дельту (см. «Побочные эффекты»).
  • AC-9 (нет права). Given у актора нет rewards.commission.adjust-approve; When решение; Then 403 FORBIDDEN.

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

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

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

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

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