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

GET /api/v1/my/commission-accruals — Мои начисления (агент)

Назначение

Отдаёт агенту в личном кабинете постраничный список начислений по его завершённым сделкам: статус подтверждения и выплаты, сумму сделки и свою долю комиссии. Агент видит только свои начисления и только собственную долю: полное распределение и удержание агентства ему не раскрываются — проекция редуцируется до MyAccrualModel (только собственная Agent-строка), в отличие от agency-версии (list-agency-accruals.md, view-all). Служит источником для сверки со сводкой (get-my-commission-summary.md) и историей выплат (list-my-payouts.md). Feature group — my-commission.

Metadata

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

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

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

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdтокен (sub)GuidДаRequired; не принимается из queryrecord-level фильтр AgentUserId
TenantIdтокен (tenant_id)GuidДаRequired; не принимается из queryTenant isolation
StatusqueryCommissionAccrualStatus?Нетenum-строка PendingConfirmation/Confirmed/Paid/Cancelled
Page / PageSizequeryintНетpaging-конвенция — README.md (Page >= 1, PageSize 1..100, default 20)

Модель ответа — страница MyAccrualModel

Ответ — страница элементов MyAccrualModel с полями Items, Page, PageSize, TotalCount (paging-конвенция — README.md). Дом базовой проекции — CommissionAccrualModel (README.md).

MyAccrualModel — дельта от CommissionAccrualModel

Редуцированная агентская проекция. Наследует все поля CommissionAccrualModel, кроме маскируемых, и добавляет собственную долю агента:

ИзменениеПолеТипПравило
Убрано (masking)DistributionsПолный список долей (в т.ч. Agency-строка удержания) агенту не отдаётся — Masking-правила README.md
Убрано (masking)AppliedPolicyIdВнутренняя ссылка на политику агентства; документ политики агенту не отдаётся
ДобавленоMyShareAmountMoneyModel?Сумма собственной Agent-строки распределения (CommissionDistributionEntity где RecipientType == Agent и RecipientUserId == AgentUserId, Amount). null до статуса Confirmed (строк распределения ещё нет; прогноз — в get-my-commission-summary.md)
Не проецируетсяInvestorUserIdНикогда не отдаётся в API rewards (как и в базовой модели)

Сохраняются как есть: Id, DealId, DeveloperCompanyId, AgencyCompanyId, AgentUserId (= ActorUserId), PropertyObjectId, DealCompletedAt, CalculationBase, CommissionPercent, BasePriceAmount, FinalPriceAmount, CommissionAmount (gross сделки; фиксируется как «комиссия по сделке»), AppliedRule (ref-модель id+имя), Status, ConfirmedAt, PaidAt, CancelledAt, CancelReason, CreatedAt, Version. Суммы — Sensitive-класс: в теле ответа отдаются, в logs не пишутся.

Алгоритм

  1. Контекст и доступ. Актор — агент; tenant — его агентство (Type ∈ {Agency, Broker, Pin}); требуется право rewards.commission.view-own. Агент видит только свои начисления: AgentUserId берётся из токена, не из запроса.
  2. Проверка входа. Значение enum Status (если задано) допустимо; paging в границах конвенции (Page >= 1, PageSize 1..100) — иначе ValidationError. Тип компании актора вне {Agency, Broker, Pin}FORBIDDEN. Входных id, требующих существования, нет (агент гарантирован токеном).
  3. Что читаем. Страница начислений агента, отобранных по обязательному record-level условию AgentUserId = ActorUserId и опциональному фильтру Status, отсортированных по DealCompletedAt по убыванию (свежие сделки первыми). Вместе с начислениями загружаются их строки распределения — они нужны для расчёта MyShareAmount. Ведущий индекс — ix_commission_accruals_agent (tenant_id, agent_user_id).
  4. Бизнес-правила и маппинг. Проекция начисления в MyAccrualModel поле-в-поле: AgencyCompanyId берётся из tenant начисления; AppliedRule.RuleName — из снапшота применённого правила ("manual" для ручных начислений); MyShareAmount — сумма единственной Agent-строки распределения агента (INV-9), null пока начисление не подтверждено. Поля Distributions, AppliedPolicyId, InvestorUserId в проекцию не попадают (masking). Переходов состояний нет.
  5. Результат. Страница MyAccrualModel (Items, Page, PageSize, TotalCount) → HTTP 200.

Диаграмма

Ошибки и failure modes

Специфичных для операции REWARDS_*-кодов нет (read без мутаций и concurrency). Применяются общие коды — дом README.md:

КодHTTP statusУсловиеRetry
ValidationError400enum Status / paging вне диапазонаno retry
UNAUTHORIZED401нет/просрочен JWT агентаno retry
FORBIDDEN403нет rewards.commission.view-own / тип компании вне {Agency, Broker, Pin}no retry

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

  • Предусловия. Агент авторизован (JWT), состоит в агентстве (Type ∈ {Agency, Broker, Pin}) и имеет право rewards.commission.view-own. По агенту могут существовать начисления в любом из статусов PendingConfirmation/Confirmed/Paid/Cancelled — либо не существовать вовсе. Собственная Agent-строка распределения появляется только с момента Confirm начисления (INV-4 domain/commission-accrual.md) — до этого доля в строках не зафиксирована.
  • Постусловия. Операция только читает: состояние rewards не меняется, события не публикуются. Возвращена страница MyAccrualModel, отобранная по record-level условию AgentUserId = ActorUserId и опциональному Status, отсортированная по DealCompletedAt убыв. MyShareAmount заполнен для Confirmed/Paid (сумма собственной Agent-строки) и null для PendingConfirmation. Полное распределение (Distributions), удержание агентства, AppliedPolicyId, InvestorUserId в ответ не попадают (masking).

Acceptance-критерии

  • Happy path. Дано: агент с правом rewards.commission.view-own, начисления в разных статусах. Когда: GET /api/v1/my/commission-accruals. Тогда: 200, страница MyAccrualModel отсортирована по DealCompletedAt убыв.; для Confirmed/PaidMyShareAmount = сумма собственной Agent-строки, для PendingConfirmationMyShareAmount = null; Distributions/AppliedPolicyId/InvestorUserId в элементах отсутствуют.
  • Фильтр по статусу. Дано: Status = Confirmed. Когда: GET. Тогда: 200, только начисления в статусе Confirmed; TotalCount = число таких начислений агента.
  • Неизвестный статус. Дано: Status вне enum CommissionAccrualStatus. Когда: GET. Тогда: 400 ValidationError.
  • Paging вне диапазона. Дано: PageSize = 500 (или Page = 0). Когда: GET. Тогда: 400 ValidationError (PageSize 1..100, Page >= 1).
  • Пустая выборка. Дано: у агента нет начислений (в т.ч. под фильтром). Когда: GET. Тогда: 200, Items = [], TotalCount = 0не 404.
  • Доля до подтверждения. Дано: начисление PendingConfirmation (строк распределения ещё нет, INV-4). Когда: GET. Тогда: элемент присутствует, MyShareAmount = null; прогноз доли доступен в get-my-commission-summary.md. После Confirm то же начисление отдаёт непустой MyShareAmount (eventual consistency — после обработки подтверждения застройщиком).
  • Только своя доля. Дано: агент пытается передать AgentUserId/TenantId в query. Когда: GET. Тогда: параметры игнорируются — ключи берутся из токена (sub/tenant_id); чужие начисления в выборку не попадают (fail-closed record-level).
  • Нет права / не в агентстве. Дано: нет rewards.commission.view-own либо тип компании актора вне {Agency, Broker, Pin}. Когда: GET. Тогда: 403 FORBIDDEN.

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

  • MyAccrualModel — производная проекция общего CommissionAccrualModel: новые поля additive nullable; удаление/переименование/смена типа — breaking (migration + rollout). Unknown-значение enum Status в query → ValidationError.
  • Logs: TenantId, AgentUserId, применённый Status, TotalCount (суммы и доли не логируются); metrics: rewards_api_request_duration_seconds{route}; traces: сквозной trace через traceparent; dashboard rewards-overview; runbook при деградации выборки — docs/06-services/rewards/runbooks/ (по конвенции).

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

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

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

  • Сущность и инварианты распределения: domain/commission-accrual.md (индекс ix_commission_accruals_agent, Agent-строка CommissionDistributionEntity INV-9, masking InvestorUserId/Distributions — раздел «Безопасность»).
  • Базовая модель CommissionAccrualModel, MoneyModel, paging и Masking-правила — README.md.
  • Смежные agent-операции: get-my-commission-summary.md (агрегаты и прогноз доли), list-my-payouts.md (история выплат). Полный список tenant-а — list-agency-accruals.md (agency-area, view-all).
  • Permissions — каталог ../../identity/domain/permissions.md; решение №2 (tenancy) — ../README.md; ADR-0052.