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

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 areaplatform
Feature groupreferral-payouts
Статусdraft
Документdocs/06-services/rewards/api/platform/create-referral-payout-order.md

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

ПроверкаЗначение
Actor typeUser (оператор ПИН)
Auth policy / Permissionsrewards.payouts.manage (платформенный scope)
Scope (RBAC)платформа
Tenant isolationTenantId поручения = компания ПИН (владелец процесса реферальных выплат, ADR-0052; INV-9 domain/payout-order.md) — источники строк платформенные, вне tenant-модели агентств
Auditcreate: CreatedByUserId = ActorUserId, Version = 1; финансовый документ — полный след через Version + события статусов

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/platform/referral-payout-orders
Success status201 (+ Location: /api/v1/platform/referral-payout-orders/{id})
Idempotency headerне применяется (повтор создаёт новый черновик; уже захваченные начисления во второй вызов не попадут — REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequiredactor-stamping
PeriodFrombodyDateTimeOffsetДаRequired; включаются рефералки с DealCompletedAt >= PeriodFrom
PeriodTobodyDateTimeOffsetДаRequired; PeriodTo > PeriodFrom; исключительно: DealCompletedAt < PeriodTo
Titlebodystring?Нет<= 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.

Алгоритм

  1. Контекст и доступ. Операцию выполняет оператор ПИН в платформенном scope; требуется право rewards.payouts.manage; tenant создаваемого поручения — компания ПИН.
  2. Проверка входа. PeriodFrom/PeriodTo заданы, PeriodTo > PeriodFrom, Title.Length <= 256; иначе ValidationError.
  3. Захват состава за период (INV-1 по Kind). Захватить свободные реферальные начисления: Status = Approved, PayoutOrderItemId = null, DealCompletedAt ∈ [PeriodFrom, PeriodTo). Начисления захватываются под блокировкой с пропуском уже занятых параллельным поручением. Если свободных начислений за период нет → REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE. Номер поручения присваивается из последовательности per (tenant ПИН, year): Number = "PO-{yyyy}-{seq}".
  4. Группировка и расчёт. Создаётся поручение 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.
  5. Фиксация факта. Поручение, позиции и захват начислений фиксируются в одном согласованном изменении. Интеграционное событие PayoutOrderStatusChangedKafkaEvent на создании не публикуется — черновик наружу не анонсируется (событие — только на Approve/MarkPaid/Reject, domain/payout-order.md). Захват под блокировкой с пропуском занятых исключает двойной захват; гонка с отменой начисления (Cancel из Approved) — обе мутируют агрегат рефералки, проходит одна; коллизия Number разрешается повтором с новым номером (REWARDS_CONCURRENCY_CONFLICT).
  6. Результат. Полная проекция созданного поручения с позициями (Kind = ReferralReward, Status = Draft, Number, Version = 1) → HTTP 201 + Location.

Диаграмма

Ошибки и failure modes

Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT — в т.ч. гонка по Number и конфликт версии рефералки) — дом README.md «Общие ошибки». Специфичные для операции:

КодHTTP statusУсловиеСообщениеRetry
REWARDS_PAYOUT_DISTRIBUTION_UNAVAILABLE409Нет свободных Approved-рефералок за период (все выплачены/захвачены другим поручением) — INV-1«За период нет вознаграждений, готовых к выплате»no retry (сменить период)
REWARDS_CURRENCY_MISMATCH409Валюты захваченных начислений различаются (INV-4)«Начисления выплаты в разных валютах»no retry

Acceptance criteria

  • AC-1 (автосборка за период). Given есть свободные Approved-рефералки (PayoutOrderItemId = null) с DealCompletedAt в [PeriodFrom, PeriodTo); When оператор создаёт поручение за период; Then заводится Draft c Kind = 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 создание; Then REWARDS_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 <= PeriodFromValidationError (400); Given актор без rewards.payouts.manage платформенного scope (в т.ч. руководитель агентства) — FORBIDDEN (403).

Смежные операции (тонкая дельта): отмена черновика

cancelReferralPayoutOrderPOST /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; dashboard rewards-overview.
  • НФТ (ADR-0056): захват — по ix_referral_accruals_status/ix_referral_accruals_deal_completed_at под блокировкой с пропуском занятых; объём периода — десятки-сотни рефералок; целевая латентность p95 < 500 мс; частота — единицы поручений в месяц (back-office ПИН).

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

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

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