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

GET /api/v1/agency/commission-accruals — Список начислений агентства

Постраничный список всех начислений комиссий tenant-агентства с полными строками распределения — экран «Комиссии агентства» ЛК руководителя (матрица «Агенство» 5.1, 5.3: «какую комиссию получим/получили — подтверждено / ожидает подтверждения»).

Назначение

Руководитель/администратор агентства видит начисления по завершённым сделкам своих агентов: суммы, статусы подтверждения/выплаты и — в отличие от застройщика (list-developer-accruals.md) — полное распределение комиссии на долю агента и удержание агентства (Distributions отдаются только agency-area — Masking-правила README.md). Служит источником данных для формирования платёжных поручений (create-payout-order.md) и сверки со сводкой (get-agency-commission-summary.md). Feature group — agency-commission.

Metadata

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

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

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

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequired; не принимается из queryTenant isolation
StatusqueryCommissionAccrualStatus?Нетenum-строка PendingConfirmation/Confirmed/Paid/Cancelled
AgentUserIdqueryGuid?Нетесли задан — != Guid.Emptyсужение по агенту
DeveloperCompanyIdqueryGuid?Нетесли задан — != Guid.Emptyфильтр по застройщику-плательщику
DealCompletedFromqueryDateTimeOffset?Нетнижняя граница DealCompletedAt (включительно)
DealCompletedToqueryDateTimeOffset?Нетверхняя граница DealCompletedAt (исключительно); > DealCompletedFrom, если обе заданы
Page / PageSizequeryintНетpaging-конвенция — README.md (Page >= 1, PageSize 1..100, default 20)

Модель ответа — постраничный список CommissionAccrualModel

Постраничный ответ: Items (список CommissionAccrualModel), Page, PageSize, TotalCount (paging-конвенция — README.md). Дом проекции — CommissionAccrualModel (README.md, полная проекция агрегата). Отдельная list-item-модель не вводится: agency-area возвращает полный CommissionAccrualModel как есть (в отличие от developer-area, где Distributions/AppliedPolicyId вырезаются). Дельта area:

АспектЗначение
DistributionsОтдаются полностью (IReadOnlyList<CommissionDistributionModel>): доли Agent/Agency с суммами и PayoutOrderItemId. Пусто до статуса Confirmed (строки создаются при Confirm, INV-4). Правило area — README.md «Masking-правила»
InvestorUserIdМаскируется — в CommissionAccrualModel не проецируется (трассировка инвестора только через DealId в crm)
Суммы (*Amount)Sensitive-класс: в теле ответа отдаются, в logs не пишутся

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/администратор агентства (тип компании Agency/Broker/Pin); scope — организация (TenantId из claims, не из query); требуется право rewards.commission.view-all. Иначе — FORBIDDEN.
  2. Проверка входа. Формат/диапазоны: Status — допустимое значение enum CommissionAccrualStatus; AgentUserId/DeveloperCompanyId непусты, если заданы; DealCompletedTo > DealCompletedFrom, если обе заданы; Page/PageSize в границах конвенции; иначе ошибка валидации. AgentUserId/DeveloperCompanyId — фильтры выборки, их существование не проверяется.
  3. Что читаем. Страница начислений агентства-tenant-а по фильтрам: опц. Status, AgentUserId, DeveloperCompanyId, границы DealCompletedAt; сортировка — по DealCompletedAt DESC (свежие сделки первыми); пагинация — по конвенции. Ведущий индекс — ix_commission_accruals_tenant_status; при заданном DeveloperCompanyId без Status может использоваться ix_commission_accruals_developer. Вместе с начислениями загружаются их строки распределения (Distributions) — agency-area отдаёт распределение полностью.
  4. Проекция. Каждое начисление разворачивается в CommissionAccrualModel поле-в-поле (дом README.md): AgencyCompanyId ← TenantId, AppliedRule.RuleName ← AppliedRuleSnapshot.RuleName ("manual" для ручных); суммы (CommissionAmount, BasePriceAmount, FinalPriceAmount) → MoneyModel; Distributions — доли Agent/Agency целиком (agency-area); InvestorUserId не проецируется (masking).
  5. Результат. Постраничный список (Items, Page, PageSize, TotalCount) → HTTP 200; суммы в логах маскируются, в теле доступны роли с permission.

Диаграмма

Ошибки и failure modes

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

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Status вне enum; AgentUserId/DeveloperCompanyId = Guid.Empty; DealCompletedTo <= DealCompletedFrom; PageSize вне 1..100«Проверьте параметры фильтра»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
FORBIDDEN403нет rewards.commission.view-all / тип компании вне {Agency, Broker, Pin} / чужой tenant«Недостаточно прав»no retry

Кросс-сервисная свежесть

Начисления создаются асинхронно консюмером pin.crm.deal-completed.v1 (crm-deal-completed.md): между фактом завершения сделки в crm и появлением начисления в этом списке есть окно обработки события (обычно секунды, при отставании консюмера — дольше). Поэтому список отражает все уже созданные начисления агентства строго согласованно (OLTP-чтение), но только что завершённая в CRM сделка может ещё не иметь начисления. Смена статуса начисления (Confirm/Cancel/MarkPaid) видна в списке немедленно после соответствующего коммита — статусные переходы происходят внутри rewards, не через реплики.

Acceptance criteria

  • AC-1 (список начислений с распределением). Given у агентства есть начисления в разных статусах; When руководитель запрашивает список без фильтра; Then 200, начисления по DealCompletedAt DESC, у Confirmed/Paid — полные Distributions (доли Agent/Agency, PayoutOrderItemId), у PendingConfirmationDistributions пусты (строки создаются при Confirm, INV-4); InvestorUserId не проецируется.
  • AC-2 (фильтры). Given те же данные; When заданы Status, AgentUserId, DeveloperCompanyId и/или период по DealCompletedAt; Then остаются только удовлетворяющие всем условиям записи; фильтры — сужение выборки, существование AgentUserId/DeveloperCompanyId не проверяется (пустой результат — не ошибка).
  • AC-3 (источник для поручения). Given фильтр Status = Confirmed; When запрос; Then выдача совпадает с множеством, из которого create-payout-order.md собирает свободные Agent-доли (свободные — те, у чьих строк PayoutOrderItemId = null).
  • AC-4 (кросс-сервисное окно). Given сделка завершена в CRM только что; When список запрошен до обработки события консюмером; Then начисления по ней ещё нет — оно появится после обработки deal-completed (окно — секунды); это ожидаемое поведение, не потеря данных.
  • AC-5 (пусто / за пределами страницы). Given нет начислений под фильтр или Page за последней; Then 200, Items = [], корректный TotalCount (не 404).
  • AC-6 (валидация). Given Status вне enum, AgentUserId/DeveloperCompanyId = Guid.Empty, DealCompletedTo <= DealCompletedFrom или PageSize вне 1..100; Then ValidationError (400).
  • AC-7 (доступ, изоляция, masking). Given актор без rewards.commission.view-all или не роль агентства — FORBIDDEN (403); Given актор агентства A — только начисления A (TenantId из claims); суммы и распределение в теле доступны роли, но в логи не пишутся; developer-area той же сущности (list-developer-accruals.md) распределение не отдаёт.

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

  • Модель — общий CommissionAccrualModel: новые поля additive nullable; удаление/переименование/смена типа — breaking (migration + rollout). Unknown-значение enum Status в query → ValidationError (unknown-enum-правило cross-cutting).
  • Logs: TenantId, применённые фильтры (Status, AgentUserId, DeveloperCompanyId, границы дат), TotalCount (суммы и распределение не логируются); metrics: rewards_api_request_duration_seconds{route}; traces: OTel, correlation через traceparent; dashboard rewards-overview; runbook при деградации выборки — docs/06-services/rewards/runbooks/ (по конвенции).
  • НФТ (ADR-0056): объём начислений = число завершённых сделок (десятки тысяч в год на платформу, на одно агентство меньше); выборка страницами по ix_commission_accruals_tenant_status с догрузкой Distributions по ix_commission_distributions_accrual; целевая латентность p95 < 300 мс на странице PageSize <= 100.

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

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

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

  • Сущность и инварианты распределения: domain/commission-accrual.md (индекс ix_commission_accruals_tenant_status; Distributions создаются при Confirm — INV-4; masking InvestorUserId/Distributions — раздел «Безопасность»).
  • Модели CommissionAccrualModel/CommissionDistributionModel/MoneyModel, paging и Masking-правила — README.md.
  • Смежные операции: list-developer-accruals.md (та же сущность, developer-area без распределения), get-agency-commission-summary.md (агрегаты по тем же данным), create-payout-order.md (потребитель Agent-строк начислений в MoneyReceived).
  • Permissions — каталог ../../identity/domain/permission.md; решение №2 (tenancy) — ../README.md; ADR-0052.