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

POST /api/v1/my/wallet/statement-exports — Постановка выписки по моему счёту (XLSX)

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

Назначение

Холдер счёта (агент — AgentCommission, партнёр — ReferralCommission) выгружает банковскую выписку по одному своему счёту за период: хронология проводок с дебетом/кредитом и нарастающим сальдо, сальдо на начало и обороты. Нужна для сверки с бухгалтерией и разбора «из чего сложился остаток». Генерация асинхронная — большие периоды не блокируют API. Feature group — my-wallet.

Metadata

ПолеЗначение
Сервис/контекстwallet
API areauser
Feature groupmy-wallet
Статусdraft
Документdocs/06-services/wallet/api/user/create-wallet-statement-export.md

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

ПроверкаЗначение
Actor typeUser (агент / партнёр — холдер счёта)
Auth policy / Permissionswallet.transactions.view-own
Scope (RBAC)платформа (личные данные пользователя)
Record-levelтолько счёт с HolderUserId = текущий пользователь; вид — AgentCommission/ReferralCommission
Tenant isolationвне tenant-модели (счета user-centric); граница — HolderUserId из claims (не из body)
AuditExportSensitive: запись на каждую постановку (актор, AccountKind, период, время); финальный аудит (число строк, исход) — при завершении задачи

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequired; не из body — record-level границаrecord-level
AccountKindbodystring (enum WalletAccountKind)Датолько AgentCommission / ReferralCommission; иной вид → ValidationError (системные/агентские компанейские счета пользователю недоступны)выбор своего счёта
PeriodFrombodyDateTimeOffsetДаRequired; строки: OccurredAt >= PeriodFrom
PeriodTobodyDateTimeOffsetДаPeriodTo > PeriodFrom (исключительно); диапазон ≤ 1 года — иначе WALLET_STATEMENT_PERIOD_TOO_WIDE
OperationKindbodystring? (enum WalletOperationKind)Нетпусто = все виды; влияет только на строки (сальдо на начало — по всем проводкам до PeriodFrom)

Счёт резолвится по паре (HolderUserId = UserId, AccountKind) — селектор всегда указывает на свой счёт. Отсутствие счёта (кредитов ещё не было) — не ошибка: пустая выписка с нулевыми OpeningBalance и оборотами (см. AC-5).

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

Дом модели — ../../reports/wallet-statement.md. На постановке: Status = Queued, DownloadUrl = null, FileName = null.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь-холдер; scope — платформа; требуется право wallet.transactions.view-own.
  2. Проверка входа. AccountKind ∈ {AgentCommission, ReferralCommission}; PeriodTo > PeriodFrom; диапазон ≤ 1 года — иначе WALLET_STATEMENT_PERIOD_TOO_WIDE; OperationKind — известное значение enum, если задан.
  3. Дедупликация двойного клика. Если у пользователя уже есть незавершённая задача (Queued/ Running) с теми же AccountKind, периодом и OperationKind — новая не создаётся, возвращается 202 с её JobId.
  4. Лимит конкурентности. Активных задач (Queued/Running) у пользователя — не более 2; превышение → WALLET_STATEMENT_CONCURRENCY_LIMIT (уже идущие задачи не прерываются).
  5. Постановка задачи. В одном согласованном изменении создаются задача выгрузки (Status = Queued, резолвленный AccountId или пометка «счёт ещё не создан», снапшот фильтров, актор) и audit-запись ExportSensitive. Ответ — 202 + Location.
  6. Фоновая генерация (вне запроса). Исполнитель формирует выписку по контракту ../../reports/wallet-statement.md (сальдо на начало, строки, обороты, сальдо на конец), проверяет лимит 100 000 строк (превышение → задача Failed с WALLET_STATEMENT_ROW_LIMIT_EXCEEDED), сериализует XLSX и кладёт файл в приватное хранилище; задача → Completed (или Failed — частичный файл не публикуется). Retention файла — 7 дней, после — Expired.

Диаграмма

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

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

Ошибки и failure modes

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

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

Acceptance criteria

  • AC-1 (постановка и генерация). Given актор с wallet.transactions.view-own, AccountKind = AgentCommission и период ≤ 1 года; When POST; Then 202 + Location с JobId, задача Queued, записан audit ExportSensitive; фоновый исполнитель доводит задачу до Completed, файл XLSX доступен через get-wallet-statement-export-job.md.
  • AC-2 (двойной клик). Given активная задача с теми же AccountKind, периодом и OperationKind; When повторный POST; Then 202 с тем же JobId — вторая задача не создаётся.
  • AC-3 (лимит конкурентности). Given у пользователя уже 2 активные задачи; When POST с другими фильтрами; Then WALLET_STATEMENT_CONCURRENCY_LIMIT (429); активные задачи не прерываются.
  • AC-4 (период шире года). Given PeriodTo − PeriodFrom = 18 месяцев; When POST; Then WALLET_STATEMENT_PERIOD_TOO_WIDE (400), задача не создаётся.
  • AC-5 (счёт ещё не создан). Given у пользователя не было кредитов выбранного вида (счёт lazy не создан); When POST и генерация; Then задача Completed, выписка пустая: OpeningBalance = 0, обороты нулевые, строк нет — не ошибка.
  • AC-6 (недоступный вид счёта). Given AccountKind = PlatformRevenue (или AgencyCommission); When POST; Then ValidationError (400) — пользователю доступны только AgentCommission / ReferralCommission.
  • AC-7 (доступ). Given актор без wallet.transactions.view-own; When POST; Then FORBIDDEN (403), задача и аудит-запись выгрузки не создаются (пишется запись отказа доступа).

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

  • Контракт additive-only: новые optional фильтры и новые значения enum — additive (unknown value от старого клиента невозможен — enum валидируется); смена структуры файла — версия контракта отчёта.
  • Logs: JobId, UserId, AccountKind, период, result (суммы/балансы не логируются); metrics: wallet_statement_export_jobs_total{area=user,result}, wallet_statement_export_duration_seconds, wallet_statement_export_rows; traces: OTel, correlation через traceparent.
  • НФТ (ADR-0056): единицы выгрузок на пользователя в месяц; генерация — минуты (фон, не SLA API).

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

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

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