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

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

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

Назначение

Руководитель агентства выгружает банковскую выписку по одному счёту своего периметра: либо счёт самого агентства (AgencyCommission), либо AgentCommission-счёт агента текущего состава — хронология проводок с дебетом/кредитом и нарастающим сальдо. Нужна для сверки выплат агентам и управленческого контроля. Feature group — agency-wallet.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (руководитель агентства / роль с доступом к командным данным)
Auth policy / Permissionswallet.team.view
Scope (RBAC)организация (только своё агентство)
Record-levelсчёт AgencyCommission компании актора (AgentUserId не задан) либо AgentCommission-счёт агента текущего состава компании актора (AgentUserId задан)
Tenant isolationAgencyCompanyId из claims (не из body); состав агентов резолвится по данным членства identity (fail closed)
AuditExportSensitive: запись на каждую постановку (актор, счёт/агент, период, время); финальный аудит — при завершении задачи

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
AgencyCompanyIdidentity claims (tenant_id)GuidДаRequired; не из body — граница периметраtenant isolation
ActorUserIdidentity claims (sub)GuidДаRequiredaudit
AgentUserIdbodyGuid?Нетне задан → счёт AgencyCommission компании актора; задан → AgentCommission-счёт этого агента (агент обязан входить в текущий состав компании — иначе FORBIDDEN)выбор счёта в периметре
PeriodFrombodyDateTimeOffsetДаRequired; строки: OccurredAt >= PeriodFrom
PeriodTobodyDateTimeOffsetДаPeriodTo > PeriodFrom (исключительно); диапазон ≤ 1 года — иначе WALLET_STATEMENT_PERIOD_TOO_WIDE
OperationKindbodystring? (enum WalletOperationKind)Нетпусто = все виды; влияет только на строки

Выбор счёта: AgentUserId = null — счёт AgencyCommission (HolderCompanyId = компания актора); AgentUserId задан — AgentCommission-счёт агента. Руководитель видит весь остаток AgentCommission-счёта агента текущего состава, включая накопленное в прежних агентствах (счета user-centric — ../../domain/wallet-account.md, list-agency-agent-balances.md). Отсутствие счёта (кредитов ещё не было) — не ошибка: пустая выписка (AC-5).

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

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

Алгоритм

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

Диаграмма

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

ТипДетали
Интеграционные событияnone (задача выгрузки — внутренняя сущность wallet)
Внешние вызовырезолюция состава агентства (identity, на постановке при заданном AgentUserId); фоновый исполнитель: identity (шапка), объектное хранилище (файл)
Уведомления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_MEMBERSHIP_UNAVAILABLE503Состав агентства недоступен (identity не отвечает) при заданном AgentUserId — fail closedretry
WALLET_STATEMENT_ROW_LIMIT_EXCEEDED— (статус задачи Failed)Выборка превысила 100 000 строк«Слишком большой период — сузьте выборку»no retry (сузить период)

FORBIDDEN (403) — нет wallet.team.view / AgentUserId вне текущего состава компании актора.

Acceptance criteria

  • AC-1 (счёт агентства). Given актор с wallet.team.view, AgentUserId не задан, период ≤ 1 года; When POST; Then 202 + Location с JobId, задача по счёту AgencyCommission компании актора доводится до Completed, файл доступен через get-wallet-statement-export-job.md.
  • AC-2 (счёт агента состава). Given AgentUserId агента текущего состава; When POST; Then 202, выписка по AgentCommission-счёту агента (весь остаток, включая прежние агентства).
  • AC-3 (агент вне состава). Given AgentUserId пользователя, не входящего в текущий состав компании актора; When POST; Then FORBIDDEN (403), задача не создаётся.
  • AC-4 (состав недоступен). Given AgentUserId задан, identity не отвечает; When POST; Then WALLET_MEMBERSHIP_UNAVAILABLE (503, fail closed) — задача не создаётся.
  • AC-5 (счёт ещё не создан). Given у выбранного холдера не было кредитов (счёт lazy не создан); When POST и генерация; Then задача Completed, выписка пустая (OpeningBalance = 0, строк нет).
  • AC-6 (период шире года). Given PeriodTo − PeriodFrom = 18 месяцев; When POST; Then WALLET_STATEMENT_PERIOD_TOO_WIDE (400), задача не создаётся.
  • AC-7 (двойной клик). Given активная задача с тем же счётом, периодом и OperationKind; When повторный POST; Then 202 с тем же JobId.

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

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

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

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

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