UC-RWD-012. Владелец агентства выгружает отчёты по вознаграждениям в Excel
Назначение
Описывает сценарий Excel-выгрузок агентства: владелец/руководитель выбирает один из двух отчётов —
детальный «по сотрудникам и сделкам» (строка = сделка агента) или сводный «по агентам за период»
(строка = агент), задаёт период и фильтры, ставит асинхронную задачу выгрузки, дожидается генерации
XLSX-файла и скачивает его по короткоживущей подписанной ссылке (ADR-0041). Отчёты — для бухгалтерии,
сверки выплат и управленческой отчётности. Связан с контекстом rewards.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-RWD-012 |
| Контекст | rewards (см. docs/06-services/rewards/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агентство (владелец/руководитель) |
| Permissions / scope | rewards.commission.view-all — выгрузка не раскрывает больше, чем видно в списках агентства; scope — Organization(CompanyId агентства) |
| Tenant | TenantId = CompanyId агентства из claims (не из body); в файл попадают только начисления/поручения своего tenant |
| Record-level | не применяется (выборка ограничена tenant-ом) |
Предусловие
- Компания-агентство
Status = Active; актор аутентифицирован сrewards.commission.view-all. - В периоде есть начисления/поручения (пустая выборка допустима — файл с нулевыми totals, не ошибка).
- У tenant-а меньше двух активных задач выгрузки (
Queued/Running) — иначе поток A2.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button |
| Триггерящий элемент | ЛК агентства → «Комиссии» → «Выгрузки» → «Сформировать отчёт» |
Основной Поток
- Руководитель открывает «Выгрузки», выбирает тип отчёта и фильтры:
ByEmployeePerDeal— строка = сделка агента: агент, сделка, объект, застройщик, даты, комиссия gross, удержание ПИН, пул, доля агента, удержание агентства, статусы с датами, № поручения (../reports/agency-rewards-by-employee-per-deal.md);PeriodSummary— строка = агент за период: сделок, начислено, подтверждено, получено (деньги пришли — гейт ADR-0067), выплачено, ожидает, удержано агентством (../reports/agency-rewards-period-summary.md).
- Фронт вызывает
POST /api/v1/agency/rewards-exportsс телом{ reportType: ByEmployeePerDeal | PeriodSummary, periodFrom, periodTo (> periodFrom, диапазон <= 1 года), agentUserId?, status? }(../api/agency/create-rewards-export.md). - Система ставит задачу: проверка периода (иначе A1) и лимита конкурентности (иначе A2); дедупликация
двойного клика — повтор с теми же типом и фильтрами при активной задаче возвращает тот же
jobId; создаются задача (Status = Queued) и audit-записьExportSensitive(актор, тип, фильтры, время). Ответ —202+Location: /api/v1/agency/rewards-exports/{jobId}. - Фоновый исполнитель генерирует файл: читает начисления/распределения/поручения tenant-а за период
(отбор по
DealCompletedAt ∈ [periodFrom; periodTo)), обогащает ФИО агентов и названиями компаний изidentityна момент генерации, проверяет лимит 100 000 строк (превышение — A3), формирует строки и totals по контракту отчёта, сериализует XLSX и кладёт в приватное объектное хранилище (retention 7 дней); задача →Completed(частичный файл никогда не публикуется). - Фронт опрашивает статус:
GET /api/v1/agency/rewards-exports/{jobId}(../api/agency/get-rewards-export-job.md), рекомендованный интервал 5–10 с. ПриCompletedответ содержитdownloadUrl— подписанную ссылку с TTL 1 час (каждая выдача — новый signed URL + audit-запись). - Руководитель скачивает XLSX напрямую из объектного хранилища по ссылке — минуя API
rewards(SLA API не зависит от размера файла); передаёт файл бухгалтерии / использует для сверки выплат.
Альтернативные Потоки
A1. Период шире года
periodTo − periodFrom> 1 года →REWARDS_EXPORT_PERIOD_TOO_WIDE(400), задача не создаётся. Руководитель сужает период (большие горизонты — несколькими выгрузками).
A2. Лимит конкурентности
- У tenant-а уже 2 активные задачи (
Queued/Running) →REWARDS_EXPORT_CONCURRENCY_LIMIT(429); активные задачи не прерываются. Руководитель дожидается завершения и повторяет.
A3. Превышен лимит строк
- Выборка дала > 100 000 строк → задача завершается
FailedсFailureCode = REWARDS_EXPORT_ROW_LIMIT_EXCEEDED(фиксируется в задаче, не в HTTP-ответе); частичный файл не публикуется. Руководитель сужает период или фильтры (agentUserId,status) и ставит новую выгрузку.
A4. Ссылка истекла / retention истёк
- Прошло > 1 часа с выдачи ссылки, файл жив (< 7 дней) → повторный
GETвыдаёт новую ссылку. - Прошло > 7 дней — файл удалён по retention: задача отдаётся как
Expired,downloadUrl = null; руководитель ставит новую выгрузку. Завершается.
A5. Фильтр по чужому агенту
agentUserIdне принадлежит агентству — принадлежность не проверяется: данные ограничены tenant-ом, чужой id даёт пустой отчёт (нулевые строки и totals). Не ошибка.
Постусловие
- Задача выгрузки в терминальном статусе (
Completed/Failed/Expired); дляCompleted— XLSX в приватном хранилище с retention 7 дней иrowsCountв задаче. - Каждая постановка и каждая выдача ссылки зафиксированы audit-записями
ExportSensitive(актор, тип, фильтры/jobId, время) — выгрузка Sensitive-данных прослеживаема (ADR-0041). - Данные файла — снапшот на момент генерации; последующие изменения начислений в файл не попадают (актуальность — новой выгрузкой).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
403 FORBIDDEN | 403 | Нет rewards.commission.view-all (например, агент) | Скрыть раздел выгрузок |
ValidationError | 400 | Неизвестный reportType/status; periodTo <= periodFrom | Подсветить поля |
REWARDS_EXPORT_PERIOD_TOO_WIDE | 400 | Диапазон периода > 1 года (A1) | Сузить период |
REWARDS_EXPORT_CONCURRENCY_LIMIT | 429 | 2 активные задачи у tenant-а (A2) | Дождаться завершения |
REWARDS_EXPORT_ROW_LIMIT_EXCEEDED | — (статус задачи Failed) | Выборка > 100 000 строк (A3) | Сузить период/фильтры, новая выгрузка |
REWARDS_EXPORT_JOB_NOT_FOUND | 404 | Задача не существует или чужой 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 |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): US-реестр: Владелец агентства (US-OWN-AG)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: ../api/agency/create-rewards-export.md (постановка, лимиты, жизненный цикл задачи), ../api/agency/get-rewards-export-job.md (статус, signed URL).
- Контракты отчётов: ../reports/agency-rewards-by-employee-per-deal.md, ../reports/agency-rewards-period-summary.md.
- Источники данных: ../domain/commission-accrual.md (строки
распределения, статусы с
MoneyReceived), ../domain/payout-order.md (№ поручений, история выплат). - Модель задачи (
RewardsExportJobModel): ../api/README.md. - Связанные UC: UC-RWD-005 (поручения, чьи номера попадают в отчёт), UC-RWD-010 (статус «получено» — гейт денег).
- ADR: 0041 (async export pipeline, signed URL, retention, лимиты), ADR-0052 (tenancy), ADR-0056 (simplicity-first).