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

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 areapartner
Feature groupreferral-rewards-exports
Статусdraft
Документdocs/06-services/rewards/api/partner/create-referral-rewards-export.md

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

Реферальные начисления вне tenant-модели (../../domain/referral-accrual.md) — область выборки определяется permission актора.

ПроверкаЗначение
Actor typeUser (реферальный партнёр — свои начисления; Менеджер ПИН — все)
Auth policy / Permissionsrewards.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)
AuditExportSensitive: обязательная запись на каждую постановку (актор, Scope, ReportType, фильтры, время); финальный аудит (число строк, исход) — при завершении задачи

Разрешение Scope: наличие rewards.commission.view-allall (все партнёры, опциональный фильтр PartnerUserId); иначе наличие rewards.referral.view-ownown (только свои). Нет ни одного права → FORBIDDEN.

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequired; при Scope = ownPartnerUserId выборкиrecord-level / audit
ReportTypebodystring (enum ReferralRewardsReportType)ДаByPartner / PeriodSummary
PeriodFrombodyDateTimeOffsetДаRequired; строки отбираются по DealCompletedAt >= PeriodFrom
PeriodTobodyDateTimeOffsetДаPeriodTo > PeriodFrom (исключительно: DealCompletedAt < PeriodTo); диапазон ≤ 1 года — иначе REWARDS_EXPORT_PERIOD_TOO_WIDE
PartnerUserIdbodyGuid?Нетучитывается только при Scope = all; если задан — непустой, фильтр «только этот партнёр». При Scope = own игнорируется (жёстко замещается ActorUserId)record-level
Statusbodystring? (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.

Алгоритм

  1. Контекст и доступ. Разрешить Scope по permission: rewards.commission.view-allall; иначе rewards.referral.view-ownown; нет прав → FORBIDDEN.
  2. Проверка входа. ReportType — известное значение enum; PeriodTo > PeriodFrom; диапазон периода ≤ 1 года — иначе REWARDS_EXPORT_PERIOD_TOO_WIDE; Status — известное значение enum, если задан; PartnerUserId непустой, если задан. При Scope = own заданный PartnerUserIdActorUserId игнорируется (не ошибка) и замещается ActorUserId.
  3. Дедупликация двойного клика. Если у актора уже есть незавершённая задача (Queued/Running) с теми же Scope, ReportType и фильтрами — новая не создаётся, возвращается 202 с её JobId.
  4. Лимит конкурентности. Активных задач (Queued/Running) у актора — не более 2; превышение → REWARDS_EXPORT_CONCURRENCY_LIMIT (уже идущие задачи не прерываются).
  5. Постановка задачи. В одном согласованном изменении создаются задача выгрузки (Status = Queued, Scope, ReportType, снапшот фильтров, актор) и audit-запись ExportSensitive (актор, Scope, фильтры, время). Ответ — 202 + Location.
  6. Фоновая генерация (вне запроса). Исполнитель читает данные (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_WIDE400PeriodTo − PeriodFrom > 1 года«Период выгрузки не может превышать один год»no retry (сузить период)
REWARDS_EXPORT_CONCURRENCY_LIMIT429У актора уже 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 года; When POST; Then 202 + Location с JobId, задача Queued со Scope = own, записан audit ExportSensitive; в файле только начисления с PartnerUserId = актор; фоновый исполнитель доводит задачу до Completed.
  • AC-2 (Менеджер ПИН — все). Given актор с rewards.commission.view-all; When POST без PartnerUserId; Then задача со Scope = all, в файле — начисления всех партнёров за период.
  • AC-3 (ПИН — фильтр по партнёру). Given актор с view-all; When POST с PartnerUserId = X; Then в файле — только начисления партнёра X.
  • AC-4 (партнёр не может смотреть чужое). Given актор с view-own; When POST с PartnerUserId = чужой; Then 202, но Scope = own — фильтр замещён ActorUserId, в файле только свои начисления (не ошибка).
  • AC-5 (двойной клик). Given активная задача с теми же Scope, ReportType и фильтрами; When повторный POST; Then 202 с тем же JobId — вторая задача не создаётся.
  • AC-6 (лимит конкурентности). Given у актора уже 2 активные задачи; When POST с другими фильтрами; Then REWARDS_EXPORT_CONCURRENCY_LIMIT (429); активные задачи не прерываются.
  • AC-7 (период шире года). Given PeriodTo − PeriodFrom = 18 месяцев; When POST; Then REWARDS_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; When POST; Then FORBIDDEN (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; dashboard rewards-overview.
  • НФТ (ADR-0056): единицы выгрузок в месяц; генерация 100k строк — минуты (фон, не SLA API).

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

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

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