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

UC-RWD-013. Агент предлагает корректировку своей доли, руководитель одобряет

Назначение

Описывает двухзвенный сценарий «агент запрашивает → руководитель одобряет» для пересмотра доли агента в пуле сделки. Агент-исполнитель не правит распределение напрямую (право распоряжаться пулом остаётся у руководителя агентства): он подаёт формальную заявку с предлагаемой суммой своей доли и обязательным обоснованием. Руководитель либо одобряет — и тогда предложение исполняется как Distribution-корректировка (перераспределение пула AgencyAgent той же логикой, что прямая правка UC-RWD-011 вид 3), либо отклоняет с комментарием. Строка Platform (удержание ПИН), CommissionAmount и статус начисления не меняются ни при каком исходе. Связан с контекстом rewards.

Metadata

ПолеЗначение
IDUC-RWD-013
Контекстrewards (см. docs/06-services/rewards/)
Типосновной (инициатива агента в финансовом контуре)
Статусdraft

Актор

ПолеЗначение
РольАгент-исполнитель сделки (подаёт заявку); Руководитель/администратор агентства (разрешает заявку)
Permissions / scopeЗаявка — rewards.commission.adjust-request; решение — rewards.commission.adjust-approve. Scope — Organization(CompanyId агентства)
TenantTenantId = CompanyId агентства (Type ∈ {Agency, Broker, Pin}, ADR-0052); заявка и начисление — в этом tenant
Record-levelАгент подаёт заявку только по своему начислению (AgentUserId = ActorUserId); руководитель разрешает заявки своего агентства (tenant-scope)

Предусловие

  • Начисление существует в tenant-е агентства, распределение построено (три строки — Platform/Agency/Agent, появляются при подтверждении, UC-RWD-004): статус ∈ {Confirmed, MoneyReceived}.
  • Собственная Agent-строка не включена в неотменённое платёжное поручение (PayoutOrderItemId is null).
  • По начислению нет другой заявки в статусе Pending (одна открытая заявка на начисление, INV-12).
  • Обоснование сформулировано — без непустой Reason заявка отклоняется.

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК агента → карточка начисления → «Предложить пересмотр доли»; ЛК агентства (руководитель) → входящие заявки → «Одобрить» / «Отклонить»

Основной Поток

Фаза 1. Агент подаёт заявку

  1. Агент в карточке своей комиссии видит распределение (Platform, Agency, Agent) и считает свою долю заниженной (индивидуальная договорённость, вклад в сложную сделку, замеченная ошибка политики). Нажимает «Предложить пересмотр доли».
  2. Фронт вызывает POST /api/v1/my/commission-accruals/{accrualId}/adjustment-requests с телом { proposedAgentAmount: MoneyModel (Value >= 0, scale 2, CurrencyId = валюте начисления), reason: string (1..1000, непусто после trim) } (../api/agent/request-distribution-adjustment.md).
  3. Система проверяет доступ и вход: rewards.commission.adjust-request; начисление найдено в tenant-е и принадлежит агенту (AgentUserId = ActorUserId, иначе — REWARDS_ACCRUAL_NOT_FOUND, чужое от несуществующего не отличается); CurrencyId = валюте начисления (INV-6, иначе REWARDS_CURRENCY_MISMATCH); Reason непуст (иначе REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED).
  4. Guard заявки (INV-12): статус ∈ {Confirmed, MoneyReceived} и Agent-строка свободна (иначе — REWARDS_ADJUSTMENT_REQUEST_FORBIDDEN); нет уже открытой Pending-заявки (иначе — REWARDS_ADJUSTMENT_REQUEST_ALREADY_PENDING, partial-unique «одна Pending на начисление»); proposedAgentAmount <= пул агентства (CommissionAmount − PlatformFeeAmount, INV-10; строка Platform неприкосновенна — агент не может предложить долю больше пула), иначе — REWARDS_ADJUSTMENT_REQUEST_AMOUNT_INVALID.
  5. Система создаёт заявку CommissionAdjustmentRequestEntity в статусе Pending (RequestedByAgentUserId, ProposedAgentAmount, Reason); распределение, суммы и Version начисления не меняются — заявка лишь предложение. Фиксируется факт: публикуется commission-adjustment-request-status-changed (Pending) → notifications уведомляет руководителя о заявке, требующей решения.
  6. Агент видит заявку со Status = Pending («на рассмотрении»).

Фаза 2. Руководитель разрешает заявку

  1. Руководитель в списке входящих заявок открывает заявку (предложенная сумма, обоснование, текущее распределение) и выбирает решение. Фронт вызывает POST /api/v1/agency/commission-adjustment-requests/{requestId}/resolve с телом { decision: Approved | Rejected, expectedVersion?: long (обязателен при Approved — версия начисления), resolutionComment?: string (обязателен при Rejected, 1..1000) } (../api/agency/approve-distribution-adjustment.md).
  2. Система проверяет доступ (rewards.commission.adjust-approve), находит заявку в tenant-е (иначе — REWARDS_ADJUSTMENT_REQUEST_NOT_FOUND) и её статус Pending (иначе — REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS, повторное решение).
  3. Ветка Approved. Начисление читается под блокировкой. Guard INV-11/INV-12: статус ∈ {Confirmed, MoneyReceived} и Agent-строка свободна (иначе — REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN); expectedVersion = Version (иначе — REWARDS_CONCURRENCY_CONFLICT); ProposedAgentAmount <= актуальному пулу (пул мог измениться gross-корректировкой после подачи заявки — иначе REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID, заявку переподать). В одном согласованном изменении: Agent-строка ← ProposedAgentAmount, Agency-строка ← пул − ProposedAgentAmount (строка Platform не меняется, Σ трёх строк = CommissionAmount, INV-5); создаётся append-only аудит-строка CommissionAccrualAdjustmentEntity (Kind = Distribution, OldAmount = прежняя доля агента, NewAmount = ProposedAgentAmount, Reason из заявки, ActorUserId = руководитель); Version + 1; заявка → Approved, AppliedAdjustmentId ← id аудит-строки.
  4. Ветка Rejected. Заявка → Rejected с ResolutionComment; распределение, Version и суммы начисления не трогаются.
  5. Фиксация факта: публикуется commission-adjustment-request-status-changed (Approved/Rejected) → notifications уведомляет агента о решении. При одобрении начисления в MoneyReceived (кошельки уже кредитованы по прежнему делению) дополнительно публикуется pin.rewards.commission-distribution-adjusted.v1 (key = AccrualId) → wallet авто-проводкой Adjustment переносит дельту долей между счетами AgentCommission и AgencyCommission (ADR-0065); в Confirmed кошелёк ещё не кредитован — событие не публикуется.
  6. Агент видит решение: при одобрении — обновлённую долю и запись в истории корректировок начисления (Adjustments[], Kind = Distribution); при отказе — комментарий руководителя.

Альтернативные Потоки

A1. Отклонение заявки

  1. Руководитель считает предложение необоснованным → decision = Rejected с обязательным resolutionComment (пустой → ValidationError, 400). Заявка Rejected, распределение не изменено, агенту уходит уведомление с комментарием. Агент может подать новую заявку (открытых Pending больше нет). Завершается.

A2. Пул изменился после подачи заявки

  1. Между подачей и одобрением gross-корректировка (UC-RWD-011 виды 1–2) уменьшила пул ниже ProposedAgentAmount. При одобрении сверка ProposedAgentAmount <= актуальному пулу не проходит → REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID (409); заявка остаётся Pending, руководитель просит агента переподать по актуальному пулу либо отклоняет. Возвращается к фазе 1.

A3. Одобрение после получения денег (MoneyReceived)

  1. Начисление в MoneyReceived, Agent-строка свободна: одобрение допустимо; строки переписаны, но кошельки уже кредитованы по прежнему делению. Публикуется pin.rewards.commission-distribution-adjusted.v1wallet переносит дельту между счетами получателей авто-проводкой Adjustment (Σ балансов получателей и строка Platform не меняются). Объём таких правок контролирует метрика rewards_distribution_adjust_after_receipt_total. Рекомендованный порядок — разрешать заявки до регистрации акта (UC-RWD-010).

A4. Agent-строка занята поручением

  1. К моменту подачи или одобрения Agent-строка включена в неотменённое платёжное поручение (PayoutOrderItemId != null): подача → REWARDS_ADJUSTMENT_REQUEST_FORBIDDEN; одобрение → REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (оба 409). Сначала освободить строку (Reject/Cancel поручения или правка состава черновика, UC-RWD-005), затем повторить.

A5. Повторная заявка / повторное решение

  1. Вторая Pending-заявка по тому же начислению → REWARDS_ADJUSTMENT_REQUEST_ALREADY_PENDING (409): дождаться решения по текущей.
  2. Повторный resolve по уже разрешённой заявке → REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS (409) — идемпотентность через guard статуса заявки (второй вызов не двоит корректировку/событие).

A6. Гонка одобрения со сборкой поручения

  1. Параллельно собирается платёжное поручение, захватывающее Agent-строку, и идёт одобрение заявки — обе мутируют начисление под блокировкой: проходит одна. Проигравшее одобрение получает REWARDS_CONCURRENCY_CONFLICT или REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN (строка уже занята) и перечитывает состояние; заявка остаётся Pending.

A7. Граничные доли

  1. ProposedAgentAmount = 0 (вся комиссия агентству) или = пулу (весь пул агенту, Agency = 0) — допустимо как предложение и как исполнение (нулевая доля стороны разрешена; строка Platform при этом неизменна).

Постусловие

  • Одобрение. Строки Agency/Agent начисления соответствуют предложению агента: Agent = ProposedAgentAmount, Agency = пул − ProposedAgentAmount; строка Platform, CommissionAmount, PlatformFeeAmount и статус не изменились; Σ трёх строк = CommissionAmount (INV-5). Создана ровно одна append-only аудит-строка Kind = Distribution с причиной из заявки и актором-руководителем; Version + 1; заявка Approved, AppliedAdjustmentId заполнен. При MoneyReceived кошельки выровнены проводкой Adjustment (асинхронно, окно секунды).
  • Отклонение. Заявка Rejected с комментарием; начисление не изменено.
  • История заявок и корректировок видна агентству в проекциях (Adjustments[], реестр заявок) — расчёт объясним постфактум.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
403 FORBIDDEN403Нет rewards.commission.adjust-request (агент) / rewards.commission.adjust-approve (руководитель)Скрыть действие
REWARDS_ACCRUAL_NOT_FOUND404Начисление не существует / чужой tenant / чужая доля (при подаче)Вернуться к списку
REWARDS_ADJUSTMENT_REQUEST_NOT_FOUND404Заявка не существует / чужой tenant (при решении)Обновить список заявок
REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED400Reason заявки пуст/whitespace (INV-12)Подсветить поле обоснования
ValidationError400Пустой ResolutionComment при Rejected; формат полейПодсветить поля
REWARDS_ADJUSTMENT_REQUEST_FORBIDDEN409Подача при статусе вне {Confirmed, MoneyReceived} / занятой Agent-строке (INV-12, A4)Показать порядок разблокировки
REWARDS_ADJUSTMENT_REQUEST_ALREADY_PENDING409По начислению уже есть Pending-заявка (A5)Дождаться решения по текущей
REWARDS_ADJUSTMENT_REQUEST_AMOUNT_INVALID409ProposedAgentAmount < 0 или > пула агентства при подаче (INV-10)Пересчитать по актуальному пулу
REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS409Решение по не-Pending-заявке (A5)Перечитать заявку
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN409Одобрение при статусе вне guard / занятой строке (INV-11, A4)Показать порядок разблокировки
REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID409Одобрение: ProposedAgentAmount превышает актуальный пул (пул изменился, A2)Переподать заявку
REWARDS_CURRENCY_MISMATCH400Валюта ProposedAgentAmount ≠ валюте начисления (INV-6)Исправить вход
REWARDS_CONCURRENCY_CONFLICT409Одобрение: expectedVersionVersion начисления (A6)Перечитать и повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
CommissionAdjustmentRequestCreatedEventСоздание заявки (шаг 5)in-proc аудит
CommissionAdjustmentRequestResolvedEventРазрешение заявки — Approved/Rejected (шаги 9–10)in-proc аудит

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
CommissionAdjustmentRequestStatusChangedKafkaEvent.v1pin.rewards.commission-adjustment-request-status-changed.v1 (key = RequestId)Подача (Pending, шаг 5), разрешение (Approved/Rejected, шаг 11)notifications — руководителю о заявке, агенту о решении
CommissionDistributionAdjustedKafkaEvent.v1pin.rewards.commission-distribution-adjusted.v1 (key = AccrualId)Одобрение начисления в MoneyReceived (шаг 11, A3)wallet — перенос дельты долей проводкой Adjustment (ADR-0065)

Одобрение начисления в Confirmed кошельковых событий не порождает (кредита ещё не было).

Acceptance Criteria

#КритерийПроверка
AC1Подача создаёт заявку Pending, не меняя распределение/Version; уходит уведомление руководителютест подачи по Confirmed-начислению
AC2Заявка допустима только по своему начислению (AgentUserId = ActorUserId); чужое → 404подача по чужой доле → REWARDS_ACCRUAL_NOT_FOUND
AC3Одобрение исполняет Distribution-корректировку: Agent = Proposed, Agency = пул − Proposed, Platform и Σ неизменны; аудит-строка Kind = Distribution; Version + 1; заявка Approvedтест одобрения (пул 90 000, Proposed = 70 000)
AC4Отклонение не меняет распределение/Version; требует непустой ResolutionCommentотказ без комментария → 400; с комментарием → заявка Rejected
AC5Одна открытая заявка на начисление (INV-12); вторая → 409параллельная/повторная подача → ALREADY_PENDING
AC6Предложение больше пула отклоняется при подаче и при одобрении (пул изменился)Proposed > пулAMOUNT_INVALID; пул уменьшился до одобрения → DISTRIBUTION_SUM_INVALID
AC7Одобрение в MoneyReceived публикует commission-distribution-adjusted + инкремент метрики after-receipt; в Confirmed — событий неттесты по статусам
AC8Занятая поручением Agent-строка блокирует подачу и одобрение (A4)включить в Draft-акт-поручение → подача/одобрение → 409
AC9Повторное решение по разрешённой заявке идемпотентно через guard (не двоит корректировку/событие)второй resolveINVALID_STATUS; корректировка однократна
AC10Гонка одобрения со сборкой поручения сериализуется блокировкой; проигравший получает 409, заявка остаётся Pendingпараллельное одобрение + захват строки

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

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

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