GET /api/v1/agency/agents/{agentUserId}/commission-summary — Сводка по конкретному агенту
Точечная денежная сводка комиссий одного агента внутри агентства: сколько этот агент получит/получил,
подтверждено/ожидает. Экран руководителя агентства «карточка агента» (матрица «Агенство» 5.3, 5.6).
Назначение
Руководитель/администратор агентства открывает сводку по выбранному сотруднику, чтобы увидеть его личный
заработок в разрезе статусов начислений. Это agency-двойник агентской операции
get-my-commission-summary.md: та же база суммирования (доля агента —
Agent-строка распределения) и тот же прогноз для неподтверждённых начислений, но агент задаётся route-ом
и виден любому агенту агентства (permission rewards.commission.view-all), а не только себе. Owning-контекст
— rewards; дом полей — domain/commission-accrual.md,
domain/agency-commission-policy.md; дом моделей —
README.md. Feature group — agency-commission.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | agency |
| Feature group | agency-commission |
| Статус | draft |
| Документ | docs/06-services/rewards/api/agency/get-agent-commission-summary.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (руководитель / администратор агентства) |
| Auth policy / Permissions | rewards.commission.view-all (op20) |
| Scope (RBAC) | организация: компания Type ∈ {Agency, Broker, Pin} |
| Record-level | не применяется — view-all разрешает сводку по любому агенту своего агентства; изоляция — tenant + проверка членства (шаг 2) |
| Tenant isolation | TenantId из claims; выборка начислений и распределения ограничена tenant-ом по TenantId; agentUserId чужого агентства данных не даёт |
| Audit | read-sensitive (суммы комиссий): в trace — actor + agentUserId + период; суммы не логируются |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/agency/agents/{agentUserId}/commission-summary |
| Success status | 200 |
| Idempotency header | не применяется (read) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
AgentUserId | route {agentUserId} | Guid | Да | Required, непустой Guid; активный член агентства-TenantId (шаг 2) | — |
DealCompletedFrom | query | DateTimeOffset? | Нет | период по DealCompletedAt | — |
DealCompletedTo | query | DateTimeOffset? | Нет | > DealCompletedFrom, если обе заданы | — |
Модель ответа CommissionSummaryModel
Возвращается общий CommissionSummaryModel (дом — README.md): по паре Total(MoneyModel)+Count
для каждого статуса PendingConfirmation/Confirmed/MoneyReceived/Paid/Cancelled. База суммирования — доля
агента, дельта идентична get-my-commission-summary.md:
| Статус | База Total | Count | Источник |
|---|
Confirmed / MoneyReceived / Paid | Σ CommissionDistributionEntity.Amount строк RecipientType = Agent этого агента | число таких начислений | распределение (строки материализованы при Confirm, INV-4 commission-accrual.md) |
PendingConfirmation | Σ прогнозной доли агента: AgencyCommissionPolicyEntity.ResolveRetention(AgentUserId, CommissionAmount).agentShare по активной политике | число PendingConfirmation-начислений | прогноз (строк распределения ещё нет) — как в get-my-commission-summary.md |
Cancelled | Σ Agent-доли: материализованная строка (отмена из Confirmed) либо прогноз (отмена из PendingConfirmation) — справочно | число отменённых | COALESCE(строка, прогноз) |
Прогноз использует текущую активную политику агентства; если активной политики нет —
удержание = 0, вся CommissionAmount идёт агенту (INV-6
agency-commission-policy.md). InvestorUserId и полный документ
политики не отдаются (Masking-правила README.md).
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/администратор агентства (тип компании
Agency/Broker/Pin); scope — организация (TenantId из claims); требуется право
rewards.commission.view-all. Иначе — FORBIDDEN.
- Проверка входа и предусловий.
- формат:
AgentUserId непустой; если заданы обе границы периода — DealCompletedTo > DealCompletedFrom; иначе ошибка валидации;
- членство:
AgentUserId — активный сотрудник агентства-TenantId (проверка членства во внешнем
identity); не член → REWARDS_AGENT_NOT_IN_AGENCY (не раскрываем, есть ли такой пользователь в
других агентствах); если identity недоступен → IDENTITY_UNAVAILABLE.
- Что читаем (три агрегации, все ограничены tenant-ом):
- материализованная доля агента — сумма и количество
Amount строк распределения
RecipientType = Agent этого агента, сгруппированные по статусу начисления (Confirmed,
MoneyReceived, Paid, отменённые-из-Confirmed), опц. период по DealCompletedAt. Индексы
ix_commission_distributions_recipient, ix_commission_distributions_accrual;
- непокрытые начисления — сумма и количество
CommissionAmount начислений этого агента в статусах
PendingConfirmation/Cancelled, у которых ещё нет Agent-строки распределения, опц. период.
Индекс ix_commission_accruals_agent;
- активная политика агентства — для прогноза доли по неподтверждённым начислениям; если активной
политики нет — fallback INV-6 (удержание = 0, вся комиссия агенту).
- Сборка модели
CommissionSummaryModel:
Confirmed*, MoneyReceived*, Paid* ← материализованная доля;
PendingConfirmation* ← прогноз: правило удержания агента разрешается по активной политике
(ResolveRetention); Percent p → доля = сумма × (1 − p/100); FixedAmount (нелинейно из-за min) →
доля считается по-начислениям (Σ (CommissionAmount − min(f, CommissionAmount))). Логика — дом
get-my-commission-summary.md;
Cancelled* ← материализованные Agent-строки отменённых + прогноз для отменённых-из-Pending
(справочно);
- все суммы —
MoneyModel в RUB; при отсутствии строк — 0.00.
- Результат.
CommissionSummaryModel → HTTP 200; членство не прошло → 404.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none |
| External calls | identity internal «is-company-member(agentUserId, TenantId)» (timeout 2 c, кэш членства) |
| Cache / projections / search | none (агрегации SQL по индексам, ADR-0056) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_AGENT_NOT_IN_AGENCY | 404 | agentUserId не является активным членом агентства-TenantId | «Агент не найден в агентстве» | no retry |
IDENTITY_UNAVAILABLE | 503 | identity недоступен на шаге 2 (проверка членства) | «Сервис временно недоступен» | retry |
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED) — README.md.
Acceptance criteria
- AC-1 (сводка по агенту). Given
agentUserId — активный сотрудник агентства-TenantId с
начислениями в разных статусах; When руководитель запрашивает сводку; Then 200 с парами
Total/Count по статусам; база — доля агента: Confirmed/MoneyReceived/Paid из материализованных
Agent-строк, PendingConfirmation — прогноз по активной политике (ResolveRetention).
- AC-2 (согласие с агентским двойником). Given тот же агент и период; Then результат совпадает с
get-my-commission-summary.md для этого агента (одна база
суммирования и один способ прогноза) — разница лишь в акторе/доступе.
- AC-3 (прогноз без активной политики). Given у агентства нет активной политики; When запрос;
Then прогноз
PendingConfirmation = полная CommissionAmount агента (удержание = 0, fallback INV-6).
- AC-4 (FixedAmount-прогноз нелинеен). Given активная политика с
FixedAmount-удержанием; When
прогноз PendingConfirmation; Then доля агента считается по-начислениям
Σ (CommissionAmount − min(f, CommissionAmount)), а не линейно по сумме (из-за min).
- AC-5 (не член агентства). Given
agentUserId не является активным членом агентства-TenantId;
When запрос; Then REWARDS_AGENT_NOT_IN_AGENCY (404, без раскрытия принадлежности к другим
агентствам).
- AC-6 (identity недоступен). Given проверка членства не может быть выполнена (identity недоступен по
таймауту 2 c); When запрос; Then
IDENTITY_UNAVAILABLE (503, retry) — доступ fail-closed, сводка
не отдаётся без подтверждённого членства.
- AC-7 (пустой период / валидация). Given нет начислений за период —
200 со всеми 0.00 RUB/0;
Given обе границы заданы и DealCompletedTo <= DealCompletedFrom — ValidationError (400).
- AC-8 (доступ и изоляция). Given актор без
rewards.commission.view-all или не роль агентства —
FORBIDDEN (403); чужой agentUserId из другого агентства данных не даёт (изоляция по TenantId).
Совместимость и наблюдаемость
- Модель — общий
CommissionSummaryModel: изменения только additive; смена базы прогноза — breaking
(согласуется с get-my-commission-summary.md).
- Unknown enum handling: новые
CommissionAccrualStatus попадут в Cancelled-подобную «прочую» ветвь
только после явного маппинга — до тех пор не суммируются (форвард-совместимость агрегаций).
- Logs:
TenantId, agentUserId, период, counts (суммы не логируются); metrics:
rewards_api_request_duration_seconds{route}, rewards_identity_membership_check_total{result} (проверка
членства); traces: OTel, correlation через traceparent; dashboard rewards-overview; runbook — не
применяется.
- НФТ (ADR-0056): три индексные агрегации по данным одного агента + одна проверка членства (кэшируется,
таймаут 2 c); объём — начисления одного агента (десятки-сотни); целевая латентность p95 < 300 мс
(включая обращение к identity); операция карточки агента — низкая частота (back-office).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы