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

UC-RWD-012. Владелец агентства выгружает отчёты по вознаграждениям в Excel

Назначение

Описывает сценарий Excel-выгрузок агентства: владелец/руководитель выбирает один из двух отчётов — детальный «по сотрудникам и сделкам» (строка = сделка агента) или сводный «по агентам за период» (строка = агент), задаёт период и фильтры, ставит асинхронную задачу выгрузки, дожидается генерации XLSX-файла и скачивает его по короткоживущей подписанной ссылке (ADR-0041). Отчёты — для бухгалтерии, сверки выплат и управленческой отчётности. Связан с контекстом rewards.

Metadata

ПолеЗначение
IDUC-RWD-012
Контекстrewards (см. docs/06-services/rewards/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгентство (владелец/руководитель)
Permissions / scoperewards.commission.view-all — выгрузка не раскрывает больше, чем видно в списках агентства; scope — Organization(CompanyId агентства)
TenantTenantId = CompanyId агентства из claims (не из body); в файл попадают только начисления/поручения своего tenant
Record-levelне применяется (выборка ограничена tenant-ом)

Предусловие

  • Компания-агентство Status = Active; актор аутентифицирован с rewards.commission.view-all.
  • В периоде есть начисления/поручения (пустая выборка допустима — файл с нулевыми totals, не ошибка).
  • У tenant-а меньше двух активных задач выгрузки (Queued/Running) — иначе поток A2.

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК агентства → «Комиссии» → «Выгрузки» → «Сформировать отчёт»

Основной Поток

  1. Руководитель открывает «Выгрузки», выбирает тип отчёта и фильтры:
    • ByEmployeePerDeal — строка = сделка агента: агент, сделка, объект, застройщик, даты, комиссия gross, удержание ПИН, пул, доля агента, удержание агентства, статусы с датами, № поручения (../reports/agency-rewards-by-employee-per-deal.md);
    • PeriodSummary — строка = агент за период: сделок, начислено, подтверждено, получено (деньги пришли — гейт ADR-0067), выплачено, ожидает, удержано агентством (../reports/agency-rewards-period-summary.md).
  2. Фронт вызывает POST /api/v1/agency/rewards-exports с телом { reportType: ByEmployeePerDeal | PeriodSummary, periodFrom, periodTo (> periodFrom, диапазон <= 1 года), agentUserId?, status? } (../api/agency/create-rewards-export.md).
  3. Система ставит задачу: проверка периода (иначе A1) и лимита конкурентности (иначе A2); дедупликация двойного клика — повтор с теми же типом и фильтрами при активной задаче возвращает тот же jobId; создаются задача (Status = Queued) и audit-запись ExportSensitive (актор, тип, фильтры, время). Ответ — 202 + Location: /api/v1/agency/rewards-exports/{jobId}.
  4. Фоновый исполнитель генерирует файл: читает начисления/распределения/поручения tenant-а за период (отбор по DealCompletedAt ∈ [periodFrom; periodTo)), обогащает ФИО агентов и названиями компаний из identity на момент генерации, проверяет лимит 100 000 строк (превышение — A3), формирует строки и totals по контракту отчёта, сериализует XLSX и кладёт в приватное объектное хранилище (retention 7 дней); задача → Completed (частичный файл никогда не публикуется).
  5. Фронт опрашивает статус: GET /api/v1/agency/rewards-exports/{jobId} (../api/agency/get-rewards-export-job.md), рекомендованный интервал 5–10 с. При Completed ответ содержит downloadUrl — подписанную ссылку с TTL 1 час (каждая выдача — новый signed URL + audit-запись).
  6. Руководитель скачивает XLSX напрямую из объектного хранилища по ссылке — минуя API rewards (SLA API не зависит от размера файла); передаёт файл бухгалтерии / использует для сверки выплат.

Альтернативные Потоки

A1. Период шире года

  1. periodTo − periodFrom > 1 года → REWARDS_EXPORT_PERIOD_TOO_WIDE (400), задача не создаётся. Руководитель сужает период (большие горизонты — несколькими выгрузками).

A2. Лимит конкурентности

  1. У tenant-а уже 2 активные задачи (Queued/Running) → REWARDS_EXPORT_CONCURRENCY_LIMIT (429); активные задачи не прерываются. Руководитель дожидается завершения и повторяет.

A3. Превышен лимит строк

  1. Выборка дала > 100 000 строк → задача завершается Failed с FailureCode = REWARDS_EXPORT_ROW_LIMIT_EXCEEDED (фиксируется в задаче, не в HTTP-ответе); частичный файл не публикуется. Руководитель сужает период или фильтры (agentUserId, status) и ставит новую выгрузку.

A4. Ссылка истекла / retention истёк

  1. Прошло > 1 часа с выдачи ссылки, файл жив (< 7 дней) → повторный GET выдаёт новую ссылку.
  2. Прошло > 7 дней — файл удалён по retention: задача отдаётся как Expired, downloadUrl = null; руководитель ставит новую выгрузку. Завершается.

A5. Фильтр по чужому агенту

  1. agentUserId не принадлежит агентству — принадлежность не проверяется: данные ограничены tenant-ом, чужой id даёт пустой отчёт (нулевые строки и totals). Не ошибка.

Постусловие

  • Задача выгрузки в терминальном статусе (Completed/Failed/Expired); для Completed — XLSX в приватном хранилище с retention 7 дней и rowsCount в задаче.
  • Каждая постановка и каждая выдача ссылки зафиксированы audit-записями ExportSensitive (актор, тип, фильтры/jobId, время) — выгрузка Sensitive-данных прослеживаема (ADR-0041).
  • Данные файла — снапшот на момент генерации; последующие изменения начислений в файл не попадают (актуальность — новой выгрузкой).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
403 FORBIDDEN403Нет rewards.commission.view-all (например, агент)Скрыть раздел выгрузок
ValidationError400Неизвестный reportType/status; periodTo <= periodFromПодсветить поля
REWARDS_EXPORT_PERIOD_TOO_WIDE400Диапазон периода > 1 года (A1)Сузить период
REWARDS_EXPORT_CONCURRENCY_LIMIT4292 активные задачи у tenant-а (A2)Дождаться завершения
REWARDS_EXPORT_ROW_LIMIT_EXCEEDED— (статус задачи Failed)Выборка > 100 000 строк (A3)Сузить период/фильтры, новая выгрузка
REWARDS_EXPORT_JOB_NOT_FOUND404Задача не существует или чужой tenantВернуться к списку выгрузок

Acceptance Criteria

#КритерийПроверка
AC1Постановка → фоновая генерация → Completed → скачивание по signed URL (TTL 1 час) — сквозной путь работает для обоих типов отчётовe2e-тест обоих reportType
AC2Детальный отчёт содержит раскладку каждой сделки: gross, удержание ПИН, пул, доля агента, удержание агентства, статусы, № поручения; totals сходятся с суммой строксверка файла с контрактом отчёта
AC3Сводный отчёт агрегирует по агентам: начислено/подтверждено/получено/выплачено/ожидает/удержано; totals по агентствусверка с get-agency-commission-summary за тот же период
AC4Двойной клик не создаёт вторую задачу — возвращается тот же jobIdповторный POST с теми же фильтрами
AC5В файле только данные своего tenant; чужой agentUserId даёт пустой отчёт (A5)тест изоляции двух агентств
AC6Период > 1 года и третья активная задача отклоняются (A1/A2)граничные тесты 400/429
AC7Превышение 100k строк → Failed без частичного файла (A3)тест большого объёма
AC8Каждая постановка и выдача ссылки — audit ExportSensitive; сам URL не логируетсяпроверка audit-журнала
AC9После 7 дней файл удалён, задача Expired (A4)тест retention

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

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

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