POST /api/v1/referral-rewards-exports — Постановка Excel-выгрузки реферальных вознаграждений
Асинхронная выгрузка отчётов по реферальным вознаграждениям в XLSX
(ADR-0041): операция создаёт
задачу выгрузки (ReferralRewardsExportJob), фоновый исполнитель формирует файл и кладёт его в приватное
объектное хранилище; статус и короткоживущая ссылка на скачивание —
get-referral-rewards-export-job.md. Контракты самих отчётов:
../../reports/referral-rewards-by-partner.md и
../../reports/referral-rewards-period-summary.md.
Назначение
Реферальный партнёр выгружает в Excel свои реферальные вознаграждения; Менеджер ПИН — все партнёрские
вознаграждения (для бухгалтерии, сверки реферальных выплат и оценки партнёрской программы). Два формата:
детально по партнёрам и сделкам (ByPartner) или сводно по партнёрам за период (PeriodSummary). Область
выборки (own/all) разрешается по permission актора. Генерация асинхронная — большие периоды не блокируют
API и не нарушают SLA. Feature group — referral-rewards-exports.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards |
| API area | partner |
| Feature group | referral-rewards-exports |
| Статус | draft |
| Документ | docs/06-services/rewards/api/partner/create-referral-rewards-export.md |
Актор и доступ
Реферальные начисления вне tenant-модели (../../domain/referral-accrual.md) — область выборки определяется permission актора.
| Проверка | Значение |
|---|---|
| Actor type | User (реферальный партнёр — свои начисления; Менеджер ПИН — все) |
| Auth policy / Permissions | rewards.referral.view-own (партнёр, Scope = own) или rewards.commission.view-all (Менеджер ПИН, Scope = all) |
| Scope (RBAC) | партнёр — платформа, record-level PartnerUserId = ActorUserId; Менеджер ПИН — платформенный scope (все партнёры) |
| Record-level | при rewards.referral.view-own выборка жёстко ограничена PartnerUserId = ActorUserId (из claims); заданный в body PartnerUserId игнорируется |
| Tenant isolation | не применяется (платформенная сущность вне tenant-модели, ADR-0052) |
| Audit | ExportSensitive: обязательная запись на каждую постановку (актор, Scope, ReportType, фильтры, время); финальный аудит (число строк, исход) — при завершении задачи |
Разрешение Scope: наличие rewards.commission.view-all → all (все партнёры, опциональный фильтр
PartnerUserId); иначе наличие rewards.referral.view-own → own (только свои). Нет ни одного права →
FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/referral-rewards-exports |
| Success status | 202 (+ Location: /api/v1/referral-rewards-exports/{jobId}) |
| Idempotency header | не применяется; естественная защита от двойного клика: повтор с теми же (ReportType, фильтры, Scope), пока активна незавершённая задача, возвращает 202 с тем же JobId |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | Required; при Scope = own — PartnerUserId выборки | record-level / audit |
ReportType | body | string (enum ReferralRewardsReportType) | Да | ByPartner / PeriodSummary | — |
PeriodFrom | body | DateTimeOffset | Да | Required; строки отбираются по DealCompletedAt >= PeriodFrom | — |
PeriodTo | body | DateTimeOffset | Да | PeriodTo > PeriodFrom (исключительно: DealCompletedAt < PeriodTo); диапазон ≤ 1 года — иначе REWARDS_EXPORT_PERIOD_TOO_WIDE | — |
PartnerUserId | body | Guid? | Нет | учитывается только при Scope = all; если задан — непустой, фильтр «только этот партнёр». При Scope = own игнорируется (жёстко замещается ActorUserId) | record-level |
Status | body | string? (enum ReferralAccrualStatus) | Нет | Pending/Approved/Paid/Cancelled; пусто = все статусы; для PeriodSummary игнорируется (warn-лог) | — |
Enum ReferralRewardsReportType
| Значение | Отчёт |
|---|---|
ByPartner | Строка = реферальное начисление: ../../reports/referral-rewards-by-partner.md. |
PeriodSummary | Строка = партнёр за период: ../../reports/referral-rewards-period-summary.md. |
Модель ответа ReferralRewardsExportJobModel
Дом модели — README.md («Общие контрактные модели»). На постановке: Status = Queued,
DownloadUrl = null.
Алгоритм
- Контекст и доступ. Разрешить
Scopeпо permission:rewards.commission.view-all→all; иначеrewards.referral.view-own→own; нет прав →FORBIDDEN. - Проверка входа.
ReportType— известное значение enum;PeriodTo > PeriodFrom; диапазон периода ≤ 1 года — иначеREWARDS_EXPORT_PERIOD_TOO_WIDE;Status— известное значение enum, если задан;PartnerUserIdнепустой, если задан. ПриScope = ownзаданныйPartnerUserId≠ActorUserIdигнорируется (не ошибка) и замещаетсяActorUserId. - Дедупликация двойного клика. Если у актора уже есть незавершённая задача (
Queued/Running) с теми жеScope,ReportTypeи фильтрами — новая не создаётся, возвращается202с еёJobId. - Лимит конкурентности. Активных задач (
Queued/Running) у актора — не более 2; превышение →REWARDS_EXPORT_CONCURRENCY_LIMIT(уже идущие задачи не прерываются). - Постановка задачи. В одном согласованном изменении создаются задача выгрузки (
Status = Queued,Scope,ReportType, снапшот фильтров, актор) и audit-записьExportSensitive(актор,Scope, фильтры, время). Ответ —202+Location. - Фоновая генерация (вне запроса). Исполнитель читает данные (
rewards.referral_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_WIDE | 400 | PeriodTo − PeriodFrom > 1 года | «Период выгрузки не может превышать один год» | no retry (сузить период) |
REWARDS_EXPORT_CONCURRENCY_LIMIT | 429 | У актора уже 2 активные задачи (Queued/Running) | «Дождитесь завершения текущих выгрузок» | retry после завершения |
REWARDS_EXPORT_ROW_LIMIT_EXCEEDED | — (статус задачи Failed) | Выборка превысила 100 000 строк | «Слишком большая выборка — сузьте период или фильтры» | no retry (сузить фильтры) |
Acceptance criteria
- AC-1 (партнёр — свои). Given актор с
rewards.referral.view-own(безview-all), валидныеReportType = ByPartnerи период ≤ 1 года; WhenPOST; Then202+LocationсJobId, задачаQueuedсоScope = own, записан auditExportSensitive; в файле только начисления сPartnerUserId = актор; фоновый исполнитель доводит задачу доCompleted. - AC-2 (Менеджер ПИН — все). Given актор с
rewards.commission.view-all; WhenPOSTбезPartnerUserId; Then задача соScope = all, в файле — начисления всех партнёров за период. - AC-3 (ПИН — фильтр по партнёру). Given актор с
view-all; WhenPOSTсPartnerUserId = X; Then в файле — только начисления партнёра X. - AC-4 (партнёр не может смотреть чужое). Given актор с
view-own; WhenPOSTсPartnerUserId = чужой; Then202, ноScope = own— фильтр замещёнActorUserId, в файле только свои начисления (не ошибка). - AC-5 (двойной клик). Given активная задача с теми же
Scope,ReportTypeи фильтрами; When повторныйPOST; Then202с тем жеJobId— вторая задача не создаётся. - AC-6 (лимит конкурентности). Given у актора уже 2 активные задачи; When
POSTс другими фильтрами; ThenREWARDS_EXPORT_CONCURRENCY_LIMIT(429); активные задачи не прерываются. - AC-7 (период шире года). Given
PeriodTo − PeriodFrom= 18 месяцев; WhenPOST; ThenREWARDS_EXPORT_PERIOD_TOO_WIDE(400), задача не создаётся. - AC-8 (превышение лимита строк). Given выборка за период даёт > 100 000 строк; When фоновая
генерация; Then задача →
FailedсREWARDS_EXPORT_ROW_LIMIT_EXCEEDED, частичный файл не публикуется; аудит фиксирует исход. - AC-9 (доступ). Given актор без
rewards.referral.view-ownи безrewards.commission.view-all; WhenPOST; ThenFORBIDDEN(403), задача и аудит-запись выгрузки не создаются (пишется запись отказа доступа).
Совместимость и наблюдаемость
- Контракт additive-only: новые optional фильтры и новые значения
ReferralRewardsReportType— additive (unknown value от старого клиента невозможен — enum валидируется); смена структуры файла — версия контракта соответствующего отчёта. - Logs:
JobId,ActorUserId,Scope,ReportType,result(фильтры — в audit, суммы не логируются); metrics:rewards_export_jobs_total{report_type,result},rewards_export_duration_seconds,rewards_export_rows(гистограмма); traces: OTel, correlation черезtraceparent; dashboardrewards-overview. - НФТ (ADR-0056): единицы выгрузок в месяц; генерация 100k строк — минуты (фон, не SLA API).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (3): GET /api/v1/my/referral-accruals — Мои реферальные начисления (партнёр), GET /api/v1/referral-rewards-exports/{jobId} — Статус выгрузки и ссылка на скачивание, Rewards API — реестр операций и общие контрактные модели
- Use Cases (1): US-реестр: Реферальный партнёр (US-PRT)
- Отчёты (2): Отчет/Export: Реферальные вознаграждения по партнёрам (referral-rewards-by-partner), Отчет/Export: Сводка реферальных вознаграждений за период (referral-rewards-period-summary)
- registries (1): Реестр API-методов PIN
Связанные документы
- Статус и скачивание: get-referral-rewards-export-job.md.
- Контракты отчётов: ../../reports/referral-rewards-by-partner.md, ../../reports/referral-rewards-period-summary.md.
- Источники данных: ../../domain/referral-accrual.md, ../../domain/referral-rate-rule.md, ../../domain/payout-order.md.
- Онлайн-аналог для партнёра: list-my-referral-accruals.md.
- Модель задачи и общие ошибки: README.md.
- ADR-0041 (async export pipeline, signed URL, лимиты), ADR-0052 (tenancy — платформенная сущность), ADR-0056 (simplicity-first).