POST /api/v1/agency/payout-orders — Создание платёжного поручения
Создание сводного платёжного поручения агентства (Kind = AgencyCommission) на выплату вознаграждений
агентам за период (матрица «Агенство» 5.4, domain/payout-order.md).
Поручение создаётся в статусе Draft и в этой же команде автособирает свободные Agent-строки
распределения начислений в статусе MoneyReceived — деньги от застройщика получены, гейт ADR-0067
пройден (domain/platform-receipt.md) — группируя их по агентам в
позиции. Далее состав можно править (update-payout-order-items.md) и
утвердить (approve-payout-order.md).
Назначение
Руководитель/бухгалтер агентства формирует поручение «за июнь 2026»: система захватывает все ещё не
выплаченные доли агентов по начислениям с полученными деньгами (MoneyReceived) с DealCompletedAt в
периоде, группирует по агенту и считает итог для передачи в бухгалтерию. Подтверждённые, но не оплаченные
застройщиком начисления (Confirmed) в поручение не попадают — выплаты до получения денег запрещены
(ADR-0067). Исполнение платежа отложено (решение №5/№9 ТЗ) — поручение лишь фиксирует состав.
Feature group — payouts.
| Поле | Значение |
|---|
| Сервис/контекст | rewards |
| API area | agency |
| Feature group | payouts |
| Статус | draft |
| Документ | docs/06-services/rewards/api/agency/create-payout-order.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (руководитель/бухгалтер агентства) |
| Auth policy / Permissions | rewards.payouts.manage (op21 реестра README.md) |
| Scope (RBAC) | организация: компания Type ∈ {Agency, Broker, Pin} |
| Record-level | не применяется (агрегатов ещё нет; состав ограничен tenant-ом) |
| Tenant isolation | TenantId = CompanyId агентства из claims; строки распределения захватываются только в этом tenant (INV-9) |
| Audit | create: CreatedByUserId = ActorUserId, Version = 1; финансовый документ, полный audit-trail через Version + события статусов |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/agency/payout-orders |
| Success status | 201 (+ Location: /api/v1/agency/payout-orders/{id}) |
| Idempotency header | не применяется (повтор создаёт новый черновик; уже захваченные строки во второй вызов не попадут — вернётся REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims (tenant_id) | Guid | Да | Required; не из body | Tenant isolation |
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
PeriodFrom | body | DateTimeOffset | Да | Required; включается DealCompletedAt >= PeriodFrom | — |
PeriodTo | body | DateTimeOffset | Да | Required; PeriodTo > PeriodFrom (check period_to > period_from); исключительно: DealCompletedAt < PeriodTo | — |
Title | body | string? | Нет | <= 256; назначение поручения («Выплаты за июнь 2026») | — |
Модель ответа PayoutOrderModel
Полная проекция — PayoutOrderModel, дом README.md (проекция
domain/payout-order.md). Особенности этого ответа:
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | PayoutOrderEntity.Id | — | Id созданного поручения. |
Number | string | Да | Number | — | PO-{yyyy}-{seq} — присвоен в команде (шаг 3). |
Status | string (enum PayoutOrderStatus) | Да | Status | — | Всегда Draft. |
TotalAmount | MoneyModel | Да | TotalAmount | Sensitive | Итог = Σ позиций (INV-3). |
ItemsCount | int | Да | ItemsCount | — | Число агентов; >= 1 (иначе создание не состоится). |
Items | IReadOnlyList<PayoutOrderItemModel> | Да | child-коллекция | Sensitive-суммы | Позиции по агентам (не пустой). |
Version | long | Да | Version | — | 1. |
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/бухгалтер агентства (тип компании
Agency/Broker/Pin); scope — организация (TenantId из claims); требуется право
rewards.payouts.manage.
- Проверка входа. Формат:
PeriodFrom и PeriodTo заданы, PeriodTo > PeriodFrom,
Title.Length <= 256; иначе ошибка валидации. TenantId берётся только из claims (не из body); период —
граница выборки, не сущность.
- Захват состава за период. Захватить свободные доли агентов за период (INV-1):
Agent-строки
распределения начислений в статусе MoneyReceived (гейт получения денег пройден, ADR-0067), у
которых DealCompletedAt в [PeriodFrom, PeriodTo) и PayoutOrderItemId = null (ещё не в поручении),
в tenant-е агентства (INV-9). Строки захватываются под блокировкой с пропуском уже занятых параллельным
поручением. Если свободных строк нет, но за период есть начисления в Confirmed с невыплаченными
Agent-строками → REWARDS_PAYOUT_MONEY_NOT_RECEIVED (422 — деньги от застройщика ещё не получены,
акт не зарегистрирован; выплата станет доступна после MoneyReceived). Если нет вообще ничего →
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE (собирать нечего). Номер поручения Number присваивается из
последовательности per (tenant, year): Number = "PO-{yyyy}-{seq}".
- Группировка и расчёт. Создаётся поручение
Status = Draft, Version = 1, CreatedByUserId = актор;
состав собирается из захваченных строк:
- валюта (INV-4): все суммы строк в одной валюте; иначе
REWARDS_CURRENCY_MISMATCH;
- группировка по агенту (INV-2 «один агент = одна позиция»): на каждого агента — позиция с суммой
Σ
его строк и числом строк; каждая строка помечается ссылкой на позицию (PayoutOrderItemId, INV-1);
- пересчёт (INV-3):
Item.Amount = Σ строк, TotalAmount = Σ Items[].Amount, ItemsCount = count;
требуется ItemsCount >= 1.
- Фиксация факта. Создание сопровождается доменным событием
PayoutOrderCreatedEvent внутри
rewards. Интеграционное событие статуса PayoutOrderStatusChangedKafkaEvent
(pin.rewards.payout-order-status-changed.v1) на создании не публикуется — оно шлётся только на
переходах Approve/MarkPaid/Reject (domain/payout-order.md);
черновик наружу не анонсируется. Поручение, позиции и захват строк распределения фиксируются в одном
согласованном изменении; захват под блокировкой с пропуском занятых исключает двойной захват тех же
строк параллельным поручением, сверка версии начислений — гонку с per-deal корректировкой распределения
(adjust-accrual-distribution.md, INV-11) и MarkPaid; отмена из
MoneyReceived невозможна вовсе (INV-8 начисления); коллизия Number разрешается повтором с новым
номером.
- Результат. Полная проекция созданного поручения с позициями (
Status = Draft, Number,
Version = 1) → HTTP 201 + Location.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none на создании (Kafka payout-order-status-changed — только на Approve/MarkPaid/Reject); доменное PayoutOrderCreatedEvent — in-process |
| External calls | none |
| Cache / projections / search | none |
| Notifications | none (уведомления агентам — по payout-order-status-changed на утверждении/выплате) |
Ошибки и failure modes
Общие коды (ValidationError при PeriodTo <= PeriodFrom/Title.Length > 256, FORBIDDEN,
UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT) — дом README.md «Общие ошибки», здесь не
дублируются. REWARDS_CONCURRENCY_CONFLICT покрывает конфликт версии начисления (параллельный
Cancel/MarkPaid) и гонку по Number (уникальность номера в tenant-е) — повтор с новым номером.
Специфичные для операции:
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
REWARDS_PAYOUT_MONEY_NOT_RECEIVED | 422 | Свободных строк в MoneyReceived за период нет, но есть Confirmed-начисления с невыплаченными долями — деньги от застройщика ещё не получены (гейт ADR-0067) | «Деньги от застройщика ещё не получены — выплата будет доступна после регистрации акта поступления» | retry после регистрации акта |
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE | 409 | Нет свободных Agent-строк MoneyReceived за период (все выплачены/захвачены другим поручением) и нет ожидающих Confirmed — INV-1 | «За период нет комиссий, готовых к выплате» | no retry (сменить период) |
REWARDS_CURRENCY_MISMATCH | 409 | Валюты захваченных долей различаются (INV-4) | «Строки выплаты в разных валютах» | no retry |
Acceptance criteria
- AC-1 (автосборка за период). Given есть свободные
Agent-доли начислений, прошедших гейт денег
(Status = MoneyReceived, PayoutOrderItemId = null) с DealCompletedAt в [PeriodFrom, PeriodTo);
When руководитель создаёт поручение за период; Then заводится Draft с присвоенным Number
(PO-{yyyy}-{seq}), доли сгруппированы по агенту в позиции (INV-2), захваченные строки помечены
PayoutOrderItemId (INV-1), TotalAmount = Σ Items, ItemsCount >= 1, Version = 1; ответ 201 +
Location. Постусловие: захваченные доли исключены из свободного пула — повторный вызов за тот же период
их не подберёт.
- AC-2 (нет свободных долей). Given за период нет свободных
MoneyReceived Agent-строк (все уже
выплачены/захвачены другим поручением) и нет ожидающих Confirmed; When создание; Then
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE (409) — собирать нечего, поручение не создаётся; клиент меняет
период.
- AC-2a (деньги ещё не получены). Given за период есть только
Confirmed-начисления с
невыплаченными Agent-долями (акт поступления средств не зарегистрирован); When создание; Then
REWARDS_PAYOUT_MONEY_NOT_RECEIVED (422) — поручение не создаётся; выплата станет доступна после
перехода начислений в MoneyReceived (domain/platform-receipt.md).
- AC-3 (границы периода включ./исключ.). Given доля с
DealCompletedAt == PeriodFrom и доля с
DealCompletedAt == PeriodTo; When создание; Then первая попадает в поручение (нижняя граница
включительна), вторая — нет (верхняя исключительна).
- AC-4 (валидация периода). Given
PeriodTo <= PeriodFrom или Title.Length > 256; When
создание; Then ValidationError (400).
- AC-5 (группировка одного агента). Given у одного агента несколько свободных долей за период;
When создание; Then на агента ровно одна позиция с суммой
Σ его долей и DistributionsCount = N
(INV-2), а не N позиций.
- AC-6 (конкурентный захват). Given два параллельных создания поручений за пересекающийся период;
When оба захватывают строки под блокировкой с пропуском занятых; Then каждая свободная доля
попадает ровно в одно поручение (двойного захвата нет); при исчерпании свободных строк второй вызов
получает
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE.
- AC-7 (коллизия номера). Given гонка присвоения
Number в пределах (tenant, year); When
конфликт уникальности номера; Then REWARDS_CONCURRENCY_CONFLICT (409) — повтор с новым номером из
последовательности.
- AC-8 (разные валюты). Given захваченные доли за период в разных валютах (нарушение INV-4);
When сборка; Then
REWARDS_CURRENCY_MISMATCH (409).
- AC-9 (доступ). Given актор без
rewards.payouts.manage или не роль агентства; When создание;
Then FORBIDDEN (403); строки захватываются только в tenant-е актора (INV-9).
Совместимость и наблюдаемость
- Контракт additive-only (
Title — optional). PayoutOrderModel/PayoutOrderItemModel — дом README.md;
breaking-изменение проекции — версия route.
- Logs:
PayoutOrderId, Number, TenantId, ItemsCount, DistributionsCount (суммы не логируются —
Sensitive); metrics: rewards_payout_order_create_total{result}, rewards_payout_order_items_count
(гистограмма); traces: OTel, correlation через traceparent; dashboard rewards-overview; runbook —
runbooks/rewards-payout-compose.md (по конвенции).
- НФТ (ADR-0056): захват идёт по
ix_commission_distributions_recipient/ix_commission_accruals_deal_completed_at
под блокировкой с пропуском занятых строк; объём периода — сотни-тысячи долей на агентство; целевая
латентность p95 < 500 мс; частота — единицы поручений на агентство в месяц (back-office бухгалтерии).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (13): GET /api/v1/agency/commission-accruals — Список начислений агентства, GET /api/v1/agency/payout-orders — Список платёжных поручений (история выплат), GET /api/v1/my/payouts — Мои выплаты (агент), POST /api/v1/agency/commission-accruals/{accrualId}/adjust-distribution — Per-deal корректировка долей внутри пула, POST /api/v1/agency/payout-orders/{orderId}/approve — Утверждение платёжного поручения, POST /api/v1/agency/payout-orders/{orderId}/cancel — Отмена черновика поручения, POST /api/v1/agency/payout-orders/{orderId}/mark-paid — Ручная отметка о выплате, POST /api/v1/agency/payout-orders/{orderId}/reject — Отклонение поручения, POST /api/v1/developer/commission-accruals/{accrualId}/confirm — Подтверждение начисления застройщиком, POST /api/v1/platform/receipts/{receiptId}/register — Регистрация акта поступления средств, POST /api/v1/platform/referral-payout-orders — Создание реферального платёжного поручения, PUT /api/v1/agency/payout-orders/{orderId}/items — Правка состава поручения (только Draft) (+1)
- Use Cases (2): UC-RWD-005. Агентство настраивает распределение и формирует платёжное поручение, US-реестр: Владелец агентства (US-OWN-AG)
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- registries (1): Реестр API-методов PIN
Связанные документы