POST /api/v1/platform/referral-payout-orders — Создание реферального платёжного поручения
Создание сводного платёжного поручения платформы (PayoutOrderKind = ReferralReward, tenant —
компания ПИН) на выплату реферальных вознаграждений партнёрам за период
(domain/payout-order.md). Поручение создаётся в статусе Draft и в этой
же команде автособирает свободные реферальные начисления в статусе Approved
(domain/referral-accrual.md) — деньги от застройщика получены, гейт
ADR-0067 пройден — группируя их по партнёрам в позиции. Агентский аналог —
../agency/create-payout-order.md.
Назначение
Оператор ПИН формирует поручение «партнёрские выплаты за июнь 2026»: система захватывает все ещё не
выплаченные Approved-рефералки с DealCompletedAt в периоде, группирует по партнёру и считает итог для
передачи в бухгалтерию ПИН. Исполнение платежа отложено (решение №9 ТЗ) — поручение фиксирует состав.
Feature group — referral-payouts.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards |
| API area | platform |
| Feature group | referral-payouts |
| Статус | draft |
| Документ | docs/06-services/rewards/api/platform/create-referral-payout-order.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (оператор ПИН) |
| Auth policy / Permissions | rewards.payouts.manage (платформенный scope) |
| Scope (RBAC) | платформа |
| Tenant isolation | TenantId поручения = компания ПИН (владелец процесса реферальных выплат, ADR-0052; INV-9 domain/payout-order.md) — источники строк платформенные, вне tenant-модели агентств |
| Audit | create: CreatedByUserId = ActorUserId, Version = 1; финансовый документ — полный след через Version + события статусов |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/platform/referral-payout-orders |
| Success status | 201 (+ Location: /api/v1/platform/referral-payout-orders/{id}) |
| Idempotency header | не применяется (повтор создаёт новый черновик; уже захваченные начисления во второй вызов не попадут — REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | Required | actor-stamping |
PeriodFrom | body | DateTimeOffset | Да | Required; включаются рефералки с DealCompletedAt >= PeriodFrom | — |
PeriodTo | body | DateTimeOffset | Да | Required; PeriodTo > PeriodFrom; исключительно: DealCompletedAt < PeriodTo | — |
Title | body | string? | Нет | <= 256; назначение («Партнёрские выплаты за июнь 2026») | — |
Модель ответа PayoutOrderModel
Полная проекция — PayoutOrderModel, дом README.md (проекция
domain/payout-order.md). Дельта этого ответа: Kind = ReferralReward;
Status = Draft; Number (PO-{yyyy}-{seq} из последовательности per (tenant ПИН, year)); позиции
Items[] — по партнёрам (RecipientUserId = PartnerUserId рефералок,
DistributionsCount = число привязанных начислений); TotalAmount = Σ Items (INV-3); Version = 1.
Алгоритм
- Контекст и доступ. Операцию выполняет оператор ПИН в платформенном scope; требуется право
rewards.payouts.manage; tenant создаваемого поручения — компания ПИН. - Проверка входа.
PeriodFrom/PeriodToзаданы,PeriodTo > PeriodFrom,Title.Length <= 256; иначеValidationError. - Захват состава за период (INV-1 по
Kind). Захватить свободные реферальные начисления:Status = Approved,PayoutOrderItemId = null,DealCompletedAt ∈ [PeriodFrom, PeriodTo). Начисления захватываются под блокировкой с пропуском уже занятых параллельным поручением. Если свободных начислений за период нет →REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE. Номер поручения присваивается из последовательности per (tenant ПИН, year):Number = "PO-{yyyy}-{seq}". - Группировка и расчёт. Создаётся поручение
Kind = ReferralReward(задаётся при создании, неизменяем),Status = Draft,Version = 1,CreatedByUserId = актор; состав:- однородность вида (INV-10): только реферальные начисления — смешение с
Agent-строками распределения запрещено структурно; - валюта (INV-4): суммы всех начислений в одной валюте; иначе
REWARDS_CURRENCY_MISMATCH; - группировка по партнёру (INV-2): на каждого партнёра — одна позиция с суммой
Σего начислений иDistributionsCount = N; каждое начисление помечается ссылкой на позицию (PayoutOrderItemId); - пересчёт (INV-3):
Item.Amount = Σначислений,TotalAmount = Σ Items[].Amount,ItemsCount = count; требуетсяItemsCount >= 1.
- однородность вида (INV-10): только реферальные начисления — смешение с
- Фиксация факта. Поручение, позиции и захват начислений фиксируются в одном согласованном
изменении. Интеграционное событие
PayoutOrderStatusChangedKafkaEventна создании не публикуется — черновик наружу не анонсируется (событие — только наApprove/MarkPaid/Reject, domain/payout-order.md). Захват под блокировкой с пропуском занятых исключает двойной захват; гонка с отменой начисления (CancelизApproved) — обе мутируют агрегат рефералки, проходит одна; коллизияNumberразрешается повтором с новым номером (REWARDS_CONCURRENCY_CONFLICT). - Результат. Полная проекция созданного поручения с позициями (
Kind = ReferralReward,Status = Draft,Number,Version = 1) → HTTP201+Location.
Диаграмма
Ошибки и failure modes
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT — в т.ч.
гонка по Number и конфликт версии рефералки) — дом README.md «Общие ошибки». Специфичные
для операции:
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE | 409 | Нет свободных Approved-рефералок за период (все выплачены/захвачены другим поручением) — INV-1 | «За период нет вознаграждений, готовых к выплате» | no retry (сменить период) |
REWARDS_CURRENCY_MISMATCH | 409 | Валюты захваченных начислений различаются (INV-4) | «Начисления выплаты в разных валютах» | no retry |
Acceptance criteria
- AC-1 (автосборка за период). Given есть свободные
Approved-рефералки (PayoutOrderItemId = null) сDealCompletedAtв[PeriodFrom, PeriodTo); When оператор создаёт поручение за период; Then заводитсяDraftcKind = ReferralRewardиNumber, рефералки сгруппированы по партнёру в позиции (INV-2), захваченные начисления помеченыPayoutOrderItemId(INV-1),TotalAmount = Σ Items,ItemsCount >= 1,Version = 1; ответ201+Location. Постусловие: захваченные рефералки исключены из свободного пула и защищены от отмены (REWARDS_REFERRAL_ACCRUAL_CANCEL_FORBIDDEN, INV-RA-5). - AC-2 (Pending не попадает). Given рефералки в
Pending(деньги от застройщика не получены) за тот же период; When создание; Then они в поручение не включаются — источник толькоApproved(гейт ADR-0067). - AC-3 (нет свободных начислений). Given за период нет свободных
Approved-рефералок; When создание; ThenREWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE(409) — поручение не создаётся. - AC-4 (границы периода). Given рефералка с
DealCompletedAt == PeriodFromи рефералка сDealCompletedAt == PeriodTo; When создание; Then первая включена (нижняя граница включительна), вторая — нет (верхняя исключительна). - AC-5 (группировка одного партнёра). Given у одного партнёра несколько свободных рефералок за
период; When создание; Then на партнёра ровно одна позиция с суммой
ΣиDistributionsCount = N(INV-2), а не N позиций. - AC-6 (конкурентный захват). Given два параллельных создания реферальных поручений за
пересекающийся период; When оба захватывают начисления под блокировкой с пропуском занятых; Then
каждая свободная рефералка попадает ровно в одно поручение; при исчерпании свободных второй вызов
получает
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE. - AC-7 (однородность вида). Given свободные
Agent-строки распределения агентских начислений за тот же период; When создание реферального поручения; Then они не затрагиваются — источники строк поKindне смешиваются (INV-10). - AC-8 (валидация периода / доступ). Given
PeriodTo <= PeriodFrom—ValidationError(400); Given актор безrewards.payouts.manageплатформенного scope (в т.ч. руководитель агентства) —FORBIDDEN(403).
Смежные операции (тонкая дельта): отмена черновика
cancelReferralPayoutOrder — POST /api/v1/platform/referral-payout-orders/{orderId}/cancel
(rewards.payouts.manage, платформенный scope): Draft → Cancelled; все привязанные рефералки
освобождаются (PayoutOrderItemId → null, INV-8) и остаются в Approved — доступны следующему
поручению; события не публикуются (черновик наружу не анонсировался); повтор из терминального статуса →
REWARDS_PAYOUT_INVALID_STATUS_TRANSITION (409). Поведение зеркально агентскому аналогу —
../agency/cancel-payout-order.md, с дельтой: tenant — компания ПИН,
источники строк — реферальные начисления.
Совместимость и наблюдаемость
- Контракт additive-only (
Title— optional);PayoutOrderModel/PayoutOrderItemModel— дом README.md; breaking-изменение проекции — версия route. - Logs:
PayoutOrderId,Number,Kind,ItemsCount,DistributionsCount(суммы не логируются — Sensitive); metrics:rewards_payout_order_create_total{result, kind}; traces: OTel, correlation черезtraceparent; dashboardrewards-overview. - НФТ (ADR-0056): захват — по
ix_referral_accruals_status/ix_referral_accruals_deal_completed_atпод блокировкой с пропуском занятых; объём периода — десятки-сотни рефералок; целевая латентность p95 < 500 мс; частота — единицы поручений в месяц (back-office ПИН).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (8): GET /api/v1/platform/referral-accruals — Реестр реферальных начислений, GET /api/v1/platform/referral-payout-orders — Список реферальных платёжных поручений, POST /api/v1/platform/receipts/{receiptId}/register — Регистрация акта поступления средств, POST /api/v1/platform/referral-accruals/{accrualId}/adjust — Корректировка суммы реферального начисления, POST /api/v1/platform/referral-accruals/{accrualId}/cancel — Ручная отмена реферального начисления, POST /api/v1/platform/referral-payout-orders/{orderId}/approve — Утверждение реферального поручения, POST /api/v1/platform/referral-payout-orders/{orderId}/mark-paid — Отметка о выплате реферального поручения, Rewards API — реестр операций и общие контрактные модели
- Use Cases (2): UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата), US-реестр: Финансовый блок ПИН (US-FIN)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность и инварианты (
PayoutOrderKind, INV-1..4/9/10): domain/payout-order.md. - Источник строк: domain/referral-accrual.md (
Approved,PayoutOrderItemId, INV-RA-5). - Жизненный цикл: list-referral-payout-orders.md, approve-referral-payout-order.md, mark-referral-payout-order-paid.md (+ «Смежные операции»: reject).
- Гейт получения денег: register-platform-receipt.md (
Pending → Approved). - Агентский аналог: ../agency/create-payout-order.md.
- Общие модели/ошибки/paging: README.md; ADR-0067, ADR-0052 (tenancy), ADR-0056 (simplicity-first), решение №9 ТЗ (исполнение платежей отложено).