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

POST /api/v1/agency/rewards-exports — Постановка Excel-выгрузки вознаграждений агентства

Асинхронная выгрузка отчётов агентства по вознаграждениям в XLSX (ADR-0041): операция создаёт задачу выгрузки (RewardsExportJob), фоновый исполнитель формирует файл и кладёт его в приватное объектное хранилище; статус и короткоживущая ссылка на скачивание — get-rewards-export-job.md. Контракты самих отчётов: ../../reports/agency-rewards-by-employee-per-deal.md и ../../reports/agency-rewards-period-summary.md.

Назначение

Владелец/руководитель агентства выгружает в Excel картину вознаграждений: детально по сотрудникам и сделкам (ByEmployeePerDeal) или сводно по агентам за период (PeriodSummary) — для бухгалтерии, сверки выплат и управленческой отчётности. Генерация асинхронная — большие периоды не блокируют API и не нарушают SLA. Feature group — rewards-exports.

Metadata

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

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

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

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/agency/rewards-exports
Success status202 (+ Location: /api/v1/agency/rewards-exports/{jobId})
Idempotency headerне применяется; естественная защита от двойного клика: повтор с теми же (ReportType, фильтры), пока активна незавершённая задача, возвращает 202 с тем же JobId
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claims (tenant_id)GuidДаRequired; не из bodyTenant isolation
ActorUserIdidentity claims (sub)GuidДаRequiredaudit
ReportTypebodystring (enum RewardsReportType)ДаByEmployeePerDeal / PeriodSummary
PeriodFrombodyDateTimeOffsetДаRequired; строки отбираются по DealCompletedAt >= PeriodFrom
PeriodTobodyDateTimeOffsetДаPeriodTo > PeriodFrom (исключительно: DealCompletedAt < PeriodTo); диапазон ≤ 1 года — иначе REWARDS_EXPORT_PERIOD_TOO_WIDE
AgentUserIdbodyGuid?Нетесли задан — непустой; фильтр «только этот агент»
Statusbodystring? (enum CommissionAccrualStatus)НетPendingConfirmation/Confirmed/MoneyReceived/Paid/Cancelled; пусто = все статусы

Enum RewardsReportType

ЗначениеОтчёт
ByEmployeePerDealСтрока = сделка агента: ../../reports/agency-rewards-by-employee-per-deal.md.
PeriodSummaryСтрока = агент за период: ../../reports/agency-rewards-period-summary.md.

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

Дом модели — README.md («Общие контрактные модели»). На постановке: Status = Queued, DownloadUrl = null.

Алгоритм

  1. Контекст и доступ. Операцию выполняет владелец/руководитель агентства; scope — организация (TenantId из claims); требуется право rewards.commission.view-all.
  2. Проверка входа. ReportType — известное значение enum; PeriodTo > PeriodFrom; диапазон периода ≤ 1 года — иначе REWARDS_EXPORT_PERIOD_TOO_WIDE; Status — известное значение enum, если задан; AgentUserId непустой, если задан (принадлежность агента агентству не проверяется — чужой id даст пустой отчёт, данные ограничены tenant-ом).
  3. Дедупликация двойного клика. Если у tenant-а уже есть незавершённая задача (Queued/Running) с теми же ReportType и фильтрами — новая не создаётся, возвращается 202 с её JobId.
  4. Лимит конкурентности. Активных задач (Queued/Running) у tenant-а — не более 2; превышение → REWARDS_EXPORT_CONCURRENCY_LIMIT (уже идущие задачи не прерываются).
  5. Постановка задачи. В одном согласованном изменении создаются задача выгрузки (Status = Queued, ReportType, снапшот фильтров, актор) и audit-запись ExportSensitive (актор, фильтры, время). Ответ — 202 + Location.
  6. Фоновая генерация (вне запроса). Исполнитель читает данные (rewards.commission_accruals + распределения + поручения; ФИО агентов и названия застройщиков — обогащение из identity на момент генерации), формирует строки и итоги по контракту отчёта, проверяет лимит 100 000 строк (превышение → задача Failed с кодом REWARDS_EXPORT_ROW_LIMIT_EXCEEDED — пользователь сужает период или фильтры), сериализует XLSX и кладёт файл в приватное объектное хранилище; задача → Completed (или Failed с кодом причины — частичный файл никогда не публикуется). Retention файла — 7 дней, после — задача Expired, файл удаляется.

Диаграмма

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

ТипДетали
Интеграционные событияnone (задача выгрузки — внутренняя сущность rewards)
Внешние вызовыфоновый исполнитель: identity (ФИО агентов, названия компаний — enrichment при генерации, недоступность → retry задачи, не HTTP-запроса); объектное хранилище (файл)
Уведомленияnone (клиент опрашивает статус задачи)

Ошибки и failure modes

Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED) — дом README.md «Общие ошибки». Дельта операции (HTTP-коды — на постановке; REWARDS_EXPORT_ROW_LIMIT_EXCEEDED фиксируется в задаче, не в HTTP-ответе):

КодHTTP statusУсловиеСообщениеRetry
REWARDS_EXPORT_PERIOD_TOO_WIDE400PeriodTo − PeriodFrom > 1 года«Период выгрузки не может превышать один год»no retry (сузить период)
REWARDS_EXPORT_CONCURRENCY_LIMIT429У tenant-а уже 2 активные задачи (Queued/Running)«Дождитесь завершения текущих выгрузок»retry после завершения
REWARDS_EXPORT_ROW_LIMIT_EXCEEDED— (статус задачи Failed)Выборка превысила 100 000 строк«Слишком большая выборка — сузьте период или фильтры»no retry (сузить фильтры)

Acceptance criteria

  • AC-1 (постановка и генерация). Given актор с rewards.commission.view-all, валидные ReportType = ByEmployeePerDeal и период ≤ 1 года; When POST; Then 202 + Location с JobId, задача Queued, записан audit ExportSensitive; фоновый исполнитель доводит задачу до Completed, файл XLSX доступен через get-rewards-export-job.md.
  • AC-2 (двойной клик). Given активная задача с теми же ReportType и фильтрами; When повторный POST; Then 202 с тем же JobId — вторая задача не создаётся.
  • AC-3 (лимит конкурентности). Given у tenant-а уже 2 активные задачи; When POST с другими фильтрами; Then REWARDS_EXPORT_CONCURRENCY_LIMIT (429); активные задачи не прерываются.
  • AC-4 (период шире года). Given PeriodTo − PeriodFrom = 18 месяцев; When POST; Then REWARDS_EXPORT_PERIOD_TOO_WIDE (400), задача не создаётся.
  • AC-5 (превышение лимита строк). Given выборка за период даёт > 100 000 строк; When фоновая генерация; Then задача → Failed с REWARDS_EXPORT_ROW_LIMIT_EXCEEDED, частичный файл не публикуется; аудит фиксирует исход.
  • AC-6 (изоляция tenant). Given актор агентства A; When выгрузка; Then в файле только начисления/поручения агентства A (TenantId из claims); AgentUserId чужого агентства даёт пустой отчёт (строки нулевые, totals нулевые) — не ошибка.
  • AC-7 (доступ). Given актор без rewards.commission.view-all (например, агент); When POST; Then FORBIDDEN (403), задача и аудит-запись выгрузки не создаются (пишется запись отказа доступа).

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

  • Контракт additive-only: новые optional фильтры и новые значения RewardsReportType — additive (unknown value от старого клиента невозможен — enum валидируется); смена структуры файла — версия контракта соответствующего отчёта.
  • Logs: JobId, TenantId, ReportType, result (фильтры — в audit, суммы не логируются); metrics: rewards_export_jobs_total{report_type,result}, rewards_export_duration_seconds, rewards_export_rows (гистограмма); traces: OTel, correlation через traceparent; dashboard rewards-overview.
  • НФТ (ADR-0056): единицы выгрузок на агентство в месяц; генерация 100k строк — минуты (фон, не SLA API).

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

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

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