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

GET /api/v1/my/commission-summary — Моя сводка комиссий (агент)

Назначение

Отдаёт агенту в личном кабинете денежную сводку по его завершённым сделкам: сколько он получит и получил, в разрезе подтверждения и выплаты. По каждому статусу — одна сумма и количество начислений. База суммирования — доля агента (а не полная комиссия по сделке): PendingConfirmation («получу, ожидает подтверждения»), Confirmed («получу, подтверждено, деньги от застройщика ещё не получены»), MoneyReceived («деньги получены, ожидает выплаты» — гейт ADR-0067 пройден), Paid («получил») и Cancelled (справочно). Для Confirmed/MoneyReceived/Paid доля агента уже зафиксирована строкой распределения (commission-accrual.md, INV-4); для PendingConfirmation строк распределения ещё нет, поэтому доля прогнозируется по активной политике агентства (agency-commission-policy.md). Экран-компаньон списка list-my-accruals.md; агрегатная параллель agency-сводки get-agency-commission-summary.md. Feature group — my-commission.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (агент)
Auth policy / Permissionsrewards.commission.view-own (op29 — реестр README.md)
Scope (RBAC)организация: компания Type ∈ {Agency, Broker, Pin} (агентство агента)
Record-levelAgentUserId актора из токена, не из query — агент видит только свои начисления
Tenant isolationTenantId = CompanyId агентства из токена; изоляция по tenant применяется на уровне хранилища, доступ к своим начислениям — явным фильтром по AgentUserId
Auditread-sensitive (суммы комиссий): в trace — actor + период; суммы не логируются

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/my/commission-summary
Success status200
Idempotency headerнет (безопасное чтение)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdтокен (tenant_id)GuidДаRequired; не из queryTenant isolation
AgentUserIdтокен (sub)GuidДаRequired; не из queryRecord-level (own)
DealCompletedFromqueryDateTimeOffset?Нетнижняя граница периода по DealCompletedAt (включительно)
DealCompletedToqueryDateTimeOffset?Нетверхняя граница (исключительно); > DealCompletedFrom, если обе заданы

Периода нет → сводка по всем начислениям агента. Paging не применяется — ответ — единичный агрегат, не список.

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

Дом модели — README.md (10 полей: Pending/Confirmed/MoneyReceived/Paid/Cancelled × *Total : MoneyModel + *Count : int). Здесь — дельта агентской области: база суммирования и формула прогноза Pending.

АспектЗначение (agent-area)
База Confirmed/MoneyReceived/PaidAmount.Value строки CommissionDistributionEntity с RecipientType = Agent, RecipientUserId = AgentUserId (точная доля агента; строки существуют с момента Confirm)
База PendingConfirmationПрогноз ForecastAgentShare (строк распределения ещё нет) — формула ниже
База Cancelled (справочно)COALESCE(Amount строки Agent, ForecastAgentShare) — отменённые из Confirmed берут точную долю, отменённые из PendingConfirmation — прогноз
Currency всех *TotalRUB (платформа — single-currency, INV-6 REWARDS_CURRENCY_MISMATCH); пустой бакет → { Value: 0, Currency: RUB }, Count = 0
Distributions / gross-комиссия / AgencyRetainedне отдаются — агентская проекция несёт только собственную долю (Masking-правила README.md)

Формула прогноза доли агента ForecastAgentShare (дельта, только PendingConfirmation)

Для начисления без строк распределения доля агента прогнозируется по правилу удержания активной политики агентства (agency-commission-policy.md):

rule = policy.Rules.AgentOverrides[AgentUserId] ?? policy.Rules.DefaultRule
AgencyRetention = rule.Kind == Percent ? round(CommissionAmount.Value * rule.Value / 100, 2, banker's)
: rule.Kind == FixedAmount ? min(rule.Value, CommissionAmount.Value) -- guard rule.Currency == CommissionAmount.Currency
: 0 -- нет активной политики (INV-6 fallback)
ForecastAgentShare = CommissionAmount.Value - AgencyRetention

Прогноз не авторитетен: используется текущая активная политика; фактическая доля фиксируется строкой распределения при Confirm и может отличаться, если политику сменят до подтверждения (дельта задокументирована как прогноз). Нет активной политики → удержание 0, вся комиссия агенту (INV-6).

Алгоритм

  1. Контекст и доступ. Актор — агент; tenant — его агентство (Type ∈ {Agency, Broker, Pin}); требуется право rewards.commission.view-own. Агент видит только свои начисления: AgentUserId берётся из токена, не из запроса.
  2. Проверка входа. Если заданы обе границы периода — DealCompletedTo > DealCompletedFrom, иначе ValidationError. Тип компании актора вне {Agency, Broker, Pin}FORBIDDEN. Входных id, требующих существования, нет (self-scope).
  3. Что читаем. Активная политика агентства (единственная, Status = Active; если её нет — fallback INV-6: удержание 0, вся комиссия агенту) — из неё определяется ставка удержания для агента (персональный оверрайд или правило по умолчанию). Начисления агента группируются по статусу с суммой доли и количеством в каждом бакете; при заданном периоде — с ограничением по DealCompletedAt (нижняя граница включительно, верхняя исключительно). Построчная выгрузка не выполняется — только агрегаты по статусу (ADR-0056).
  4. Бизнес-правила и маппинг. Слагаемое на начисление — доля агента: для Confirmed/MoneyReceived/Paid берётся точная сумма строки распределения Agent; для PendingConfirmation — прогноз ForecastAgentShare по активной политике; для Cancelled — точная доля, если она была зафиксирована, иначе прогноз. Суммы и количества раскладываются в CommissionSummaryModel: PendingConfirmationTotal/Count, ConfirmedTotal/Count, MoneyReceivedTotal/Count, PaidTotal/Count, CancelledTotal/Count; отсутствующий бакет → { Value: 0, Currency: RUB } / 0. Прогнозная ветвь Pending неавторитетна.
  5. Результат. Сводка CommissionSummaryModel (5 пар Total+Count) → HTTP 200.

Диаграмма

Ошибки и failure modes

Общие коды (ValidationError 400, FORBIDDEN 403, UNAUTHORIZED 401) — README.md. Специфичное для операции:

КодHTTP statusУсловиеСообщениеRetry
REWARDS_CURRENCY_MISMATCH409Прогноз Pending: валюта FixedAmount-удержания активной политики ≠ валюта CommissionAmount начисления (практически недостижимо — платформа single-currency RUB, INV-6)«Несовместимая валюта в расчёте прогноза»no retry

Пустая выборка (нет начислений в периоде) — не ошибка: 200 со всеми бакетами { Value: 0 } / Count = 0.

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

  • Предусловия. Актор — агент в агентстве (Type ∈ {Agency, Broker, Pin}) с правом rewards.commission.view-own. У агентства может быть активная политика удержания (единственная, Status = Active) — либо нет, тогда действует fallback INV-6 (удержание 0). По агенту могут существовать начисления в любых статусах.
  • Постусловия. Только чтение: состояние не меняется, событий нет. Возвращён единичный агрегат CommissionSummaryModel (5 пар *Total/*Count) по доле агента: Confirmed/MoneyReceived/Paid — точные суммы Agent-строк, PendingConfirmation — прогноз по текущей активной политике (неавторитетный), Cancelled — справочно. Gross-комиссия сделки, AgencyRetained, Distributions не раскрываются (masking).

Интеграционная последовательность. Операция синхронна и читает два источника внутри rewards: активную политику агентства (для прогноза PendingConfirmation) и начисления агента (наполняются consumer-ом завершения сделки crm-deal-completed.md; подтверждение и выплата меняют статусы). Прогноз Pending берёт текущую активную политику: если её сменят до Confirm, фактическая доля из строки распределения может отличаться от показанного прогноза — расхождение задокументировано как ожидаемое.

Acceptance-критерии

  • Happy path. Дано: агент с правом rewards.commission.view-own, начисления в статусах PendingConfirmation/Confirmed/MoneyReceived/Paid. Когда: GET /api/v1/my/commission-summary без периода. Тогда: 200 CommissionSummaryModel с 5 парами *Total/*Count; Confirmed/MoneyReceived/Paid — точная доля из Agent-строки распределения, PendingConfirmation — прогноз ForecastAgentShare по активной политике.
  • Фильтр периода. Дано: заданы DealCompletedFrom/DealCompletedTo. Когда: GET. Тогда: суммируются только начисления с DealCompletedAt в [From, To) (нижняя включительно, верхняя исключительно).
  • Некорректный период. Дано: DealCompletedTo <= DealCompletedFrom. Когда: GET. Тогда: 400 ValidationError.
  • Прогноз Pending неавторитетен. Дано: начисление PendingConfirmation, после снятия сводки политику агентства сменили. Когда: сделку подтверждают. Тогда: фактическая доля из строки распределения может отличаться от ранее показанного прогноза — расхождение задокументировано как ожидаемое.
  • Нет активной политики. Дано: у агентства нет политики в статусе Active. Когда: GET. Тогда: прогноз Pending считается с удержанием 0 — вся комиссия агенту (INV-6 fallback), ошибки нет.
  • Пустая выборка. Дано: у агента нет начислений (в периоде). Когда: GET. Тогда: 200, все бакеты { Value: 0, Currency: RUB } / Count = 0не 404.
  • Только своя доля. Дано: агент запрашивает сводку. Когда: GET. Тогда: AgentUserId берётся из токена; gross-комиссия сделки, AgencyRetained и Distributions в ответ не попадают (masking).
  • Нет права / не в агентстве. Дано: тип компании актора вне {Agency, Broker, Pin} либо нет права. Когда: GET. Тогда: 403 FORBIDDEN.

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

  • Модель — общий CommissionSummaryModel: новые поля additive nullable; удаление/переименование/смена типа — breaking (migration + rollout). Прогнозная методика Pending — часть контракта области; смена формулы удержания — breaking для клиента сводки.
  • Logs: TenantId, AgentUserId, период, TotalCount бакетов (суммы не логируются — Sensitive); metrics: rewards_api_request_duration_seconds{route}; traces: сквозной trace через traceparent; dashboard rewards-overview; runbook при деградации агрегации — docs/06-services/rewards/runbooks/ (по конвенции).

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

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

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