UC-RWD-013. Агент предлагает корректировку своей доли, руководитель одобряет
Назначение
Описывает двухзвенный сценарий «агент запрашивает → руководитель одобряет» для пересмотра доли агента в
пуле сделки. Агент-исполнитель не правит распределение напрямую (право распоряжаться пулом остаётся у
руководителя агентства): он подаёт формальную заявку с предлагаемой суммой своей доли и обязательным
обоснованием. Руководитель либо одобряет — и тогда предложение исполняется как Distribution-корректировка
(перераспределение пула Agency ↔ Agent той же логикой, что прямая правка UC-RWD-011 вид 3), либо
отклоняет с комментарием. Строка Platform (удержание ПИН), CommissionAmount и статус начисления не
меняются ни при каком исходе. Связан с контекстом rewards.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-RWD-013 |
| Контекст | rewards (см. docs/06-services/rewards/) |
| Тип | основной (инициатива агента в финансовом контуре) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агент-исполнитель сделки (подаёт заявку); Руководитель/администратор агентства (разрешает заявку) |
| Permissions / scope | Заявка — rewards.commission.adjust-request; решение — rewards.commission.adjust-approve. Scope — Organization(CompanyId агентства) |
| Tenant | TenantId = 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. Агент подаёт заявку
- Агент в карточке своей комиссии видит распределение (
Platform,Agency,Agent) и считает свою долю заниженной (индивидуальная договорённость, вклад в сложную сделку, замеченная ошибка политики). Нажимает «Предложить пересмотр доли». - Фронт вызывает
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). - Система проверяет доступ и вход:
rewards.commission.adjust-request; начисление найдено в tenant-е и принадлежит агенту (AgentUserId = ActorUserId, иначе —REWARDS_ACCRUAL_NOT_FOUND, чужое от несуществующего не отличается);CurrencyId= валюте начисления (INV-6, иначеREWARDS_CURRENCY_MISMATCH);Reasonнепуст (иначеREWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED). - 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. - Система создаёт заявку
CommissionAdjustmentRequestEntityв статусеPending(RequestedByAgentUserId,ProposedAgentAmount,Reason); распределение, суммы иVersionначисления не меняются — заявка лишь предложение. Фиксируется факт: публикуетсяcommission-adjustment-request-status-changed(Pending) →notificationsуведомляет руководителя о заявке, требующей решения. - Агент видит заявку со
Status = Pending(«на рассмотрении»).
Фаза 2. Руководитель разрешает заявку
- Руководитель в списке входящих заявок открывает заявку (предложенная сумма, обоснование, текущее
распределение) и выбирает решение. Фронт вызывает
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). - Система проверяет доступ (
rewards.commission.adjust-approve), находит заявку в tenant-е (иначе —REWARDS_ADJUSTMENT_REQUEST_NOT_FOUND) и её статусPending(иначе —REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS, повторное решение). - Ветка
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 аудит-строки. - Ветка
Rejected. Заявка →RejectedсResolutionComment; распределение,Versionи суммы начисления не трогаются. - Фиксация факта: публикуется
commission-adjustment-request-status-changed(Approved/Rejected) →notificationsуведомляет агента о решении. При одобрении начисления вMoneyReceived(кошельки уже кредитованы по прежнему делению) дополнительно публикуетсяpin.rewards.commission-distribution-adjusted.v1(key =AccrualId) →walletавто-проводкойAdjustmentпереносит дельту долей между счетамиAgentCommissionиAgencyCommission(ADR-0065); вConfirmedкошелёк ещё не кредитован — событие не публикуется. - Агент видит решение: при одобрении — обновлённую долю и запись в истории корректировок начисления
(
Adjustments[],Kind = Distribution); при отказе — комментарий руководителя.
Альтернативные Потоки
A1. Отклонение заявки
- Руководитель считает предложение необоснованным →
decision = Rejectedс обязательнымresolutionComment(пустой →ValidationError, 400). ЗаявкаRejected, распределение не изменено, агенту уходит уведомление с комментарием. Агент может подать новую заявку (открытыхPendingбольше нет). Завершается.
A2. Пул изменился после подачи заявки
- Между подачей и одобрением gross-корректировка (UC-RWD-011 виды 1–2) уменьшила пул ниже
ProposedAgentAmount. При одобрении сверкаProposedAgentAmount <= актуальному пулуне проходит →REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID(409); заявка остаётсяPending, руководитель просит агента переподать по актуальному пулу либо отклоняет. Возвращается к фазе 1.
A3. Одобрение после получения денег (MoneyReceived)
- Начисление в
MoneyReceived,Agent-строка свободна: одобрение допустимо; строки переписаны, но кошельки уже кредитованы по прежнему делению. Публикуетсяpin.rewards.commission-distribution-adjusted.v1—walletпереносит дельту между счетами получателей авто-проводкойAdjustment(Σ балансов получателей и строкаPlatformне меняются). Объём таких правок контролирует метрикаrewards_distribution_adjust_after_receipt_total. Рекомендованный порядок — разрешать заявки до регистрации акта (UC-RWD-010).
A4. Agent-строка занята поручением
- К моменту подачи или одобрения
Agent-строка включена в неотменённое платёжное поручение (PayoutOrderItemId != null): подача →REWARDS_ADJUSTMENT_REQUEST_FORBIDDEN; одобрение →REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN(оба 409). Сначала освободить строку (Reject/Cancelпоручения или правка состава черновика, UC-RWD-005), затем повторить.
A5. Повторная заявка / повторное решение
- Вторая
Pending-заявка по тому же начислению →REWARDS_ADJUSTMENT_REQUEST_ALREADY_PENDING(409): дождаться решения по текущей. - Повторный
resolveпо уже разрешённой заявке →REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS(409) — идемпотентность через guard статуса заявки (второй вызов не двоит корректировку/событие).
A6. Гонка одобрения со сборкой поручения
- Параллельно собирается платёжное поручение, захватывающее
Agent-строку, и идёт одобрение заявки — обе мутируют начисление под блокировкой: проходит одна. Проигравшее одобрение получаетREWARDS_CONCURRENCY_CONFLICTилиREWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN(строка уже занята) и перечитывает состояние; заявка остаётсяPending.
A7. Граничные доли
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 FORBIDDEN | 403 | Нет rewards.commission.adjust-request (агент) / rewards.commission.adjust-approve (руководитель) | Скрыть действие |
REWARDS_ACCRUAL_NOT_FOUND | 404 | Начисление не существует / чужой tenant / чужая доля (при подаче) | Вернуться к списку |
REWARDS_ADJUSTMENT_REQUEST_NOT_FOUND | 404 | Заявка не существует / чужой tenant (при решении) | Обновить список заявок |
REWARDS_ACCRUAL_ADJUSTMENT_REASON_REQUIRED | 400 | Reason заявки пуст/whitespace (INV-12) | Подсветить поле обоснования |
ValidationError | 400 | Пустой ResolutionComment при Rejected; формат полей | Подсветить поля |
REWARDS_ADJUSTMENT_REQUEST_FORBIDDEN | 409 | Подача при статусе вне {Confirmed, MoneyReceived} / занятой Agent-строке (INV-12, A4) | Показать порядок разблокировки |
REWARDS_ADJUSTMENT_REQUEST_ALREADY_PENDING | 409 | По начислению уже есть Pending-заявка (A5) | Дождаться решения по текущей |
REWARDS_ADJUSTMENT_REQUEST_AMOUNT_INVALID | 409 | ProposedAgentAmount < 0 или > пула агентства при подаче (INV-10) | Пересчитать по актуальному пулу |
REWARDS_ADJUSTMENT_REQUEST_INVALID_STATUS | 409 | Решение по не-Pending-заявке (A5) | Перечитать заявку |
REWARDS_ACCRUAL_ADJUSTMENT_FORBIDDEN | 409 | Одобрение при статусе вне guard / занятой строке (INV-11, A4) | Показать порядок разблокировки |
REWARDS_ACCRUAL_DISTRIBUTION_SUM_INVALID | 409 | Одобрение: ProposedAgentAmount превышает актуальный пул (пул изменился, A2) | Переподать заявку |
REWARDS_CURRENCY_MISMATCH | 400 | Валюта ProposedAgentAmount ≠ валюте начисления (INV-6) | Исправить вход |
REWARDS_CONCURRENCY_CONFLICT | 409 | Одобрение: expectedVersion ≠ Version начисления (A6) | Перечитать и повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
CommissionAdjustmentRequestCreatedEvent | Создание заявки (шаг 5) | in-proc аудит |
CommissionAdjustmentRequestResolvedEvent | Разрешение заявки — Approved/Rejected (шаги 9–10) | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
CommissionAdjustmentRequestStatusChangedKafkaEvent.v1 | pin.rewards.commission-adjustment-request-status-changed.v1 (key = RequestId) | Подача (Pending, шаг 5), разрешение (Approved/Rejected, шаг 11) | notifications — руководителю о заявке, агенту о решении |
CommissionDistributionAdjustedKafkaEvent.v1 | pin.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 (не двоит корректировку/событие) | второй resolve → INVALID_STATUS; корректировка однократна |
| AC10 | Гонка одобрения со сборкой поручения сериализуется блокировкой; проигравший получает 409, заявка остаётся Pending | параллельное одобрение + захват строки |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: ../api/agent/request-distribution-adjustment.md (подача), ../api/agency/approve-distribution-adjustment.md (решение), ../api/agency/adjust-accrual-distribution.md (логика исполнения корректировки).
- Domain entity: ../domain/commission-accrual.md
(
CommissionAdjustmentRequestEntity,CommissionAdjustmentRequestStatus,CommissionAccrualAdjustmentEntityKind = Distribution, INV-5/6/10/11/12). - События: ../events/commission-distribution-adjusted.md;
выравнивание кошелька — ../../wallet/README.md (проводка
Adjustment). - Связанные UC: UC-RWD-011 (три вида корректировок, вид 3 — прямая правка долей руководителем), UC-RWD-005 (освобождение строк поручения), UC-RWD-004 (появление строк распределения), UC-RWD-010 (гейт денег).
- ADR: 0067 п.4 (first-class
корректировки, аудит), 0066
(неприкосновенность строки
Platform, пул), ADR-0065 (кошелёк), ADR-0052 (tenancy).