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

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.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (руководитель / администратор агентства)
Auth policy / Permissionsrewards.commission.view-all (op20)
Scope (RBAC)организация: компания Type ∈ {Agency, Broker, Pin}
Record-levelне применяется — view-all разрешает сводку по любому агенту своего агентства; изоляция — tenant + проверка членства (шаг 2)
Tenant isolationTenantId из claims; выборка начислений и распределения ограничена tenant-ом по TenantId; agentUserId чужого агентства данных не даёт
Auditread-sensitive (суммы комиссий): в trace — actor + agentUserId + период; суммы не логируются

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/agency/agents/{agentUserId}/commission-summary
Success status200
Idempotency headerне применяется (read)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
AgentUserIdroute {agentUserId}GuidДаRequired, непустой Guid; активный член агентства-TenantId (шаг 2)
DealCompletedFromqueryDateTimeOffset?Нетпериод по DealCompletedAt
DealCompletedToqueryDateTimeOffset?Нет> DealCompletedFrom, если обе заданы

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

Возвращается общий CommissionSummaryModel (дом — README.md): по паре Total(MoneyModel)+Count для каждого статуса PendingConfirmation/Confirmed/MoneyReceived/Paid/Cancelled. База суммирования — доля агента, дельта идентична get-my-commission-summary.md:

СтатусБаза TotalCountИсточник
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).

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/администратор агентства (тип компании Agency/Broker/Pin); scope — организация (TenantId из claims); требуется право rewards.commission.view-all. Иначе — FORBIDDEN.
  2. Проверка входа и предусловий.
    • формат: AgentUserId непустой; если заданы обе границы периода — DealCompletedTo > DealCompletedFrom; иначе ошибка валидации;
    • членство: AgentUserId — активный сотрудник агентства-TenantId (проверка членства во внешнем identity); не член → REWARDS_AGENT_NOT_IN_AGENCY (не раскрываем, есть ли такой пользователь в других агентствах); если identity недоступен → IDENTITY_UNAVAILABLE.
  3. Что читаем (три агрегации, все ограничены 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, вся комиссия агенту).
  4. Сборка модели 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.
  5. Результат. CommissionSummaryModel → HTTP 200; членство не прошло → 404.

Диаграмма

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

ТипДетали
Integration eventsnone
External callsidentity internal «is-company-member(agentUserId, TenantId)» (timeout 2 c, кэш членства)
Cache / projections / searchnone (агрегации SQL по индексам, ADR-0056)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
REWARDS_AGENT_NOT_IN_AGENCY404agentUserId не является активным членом агентства-TenantId«Агент не найден в агентстве»no retry
IDENTITY_UNAVAILABLE503identity недоступен на шаге 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 <= DealCompletedFromValidationError (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).

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

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

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