POST /api/v1/platform/referral-payout-orders/{orderId}/mark-paid — Отметка о выплате реферального поручения
Перевод реферального платёжного поручения (Kind = ReferralReward, tenant — компания ПИН)
PendingPayment → Paid вручную оператором ПИН после фактической выплаты вознаграждений партнёрам вне
платформы (исполнение платежей отложено — решение №9 ТЗ). Отметка каскадно переводит все привязанные
реферальные начисления Approved → Paid (INV-7 MarkPaidCascade,
domain/payout-order.md) и публикует событие статуса с позициями
Items[] — по нему контекст wallet дебетует счета партнёров (ADR-0065). Агентский аналог —
../agency/mark-payout-order-paid.md.
Назначение
Терминальный финансовый переход реферального поручения: после отметки поручение попадает в историю выплат
платформы со статусом «выплачено», партнёры получают уведомления, их начисления закрываются статусом
Paid. Feature group — referral-payouts.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards |
| API area | platform |
| Feature group | referral-payouts |
| Статус | draft |
| Документ | docs/06-services/rewards/api/platform/mark-referral-payout-order-paid.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (оператор ПИН) |
| Auth policy / Permissions | rewards.payouts.approve (платформенный scope) |
| Scope (RBAC) | платформа |
| Tenant isolation | поручение принадлежит tenant-у компании ПИН, Kind = ReferralReward; агентское поручение по этому route → REWARDS_PAYOUT_ORDER_NOT_FOUND |
| Audit | update: PaidAt, PaidByUserId = ActorUserId, UpdatedByUserId, Version + 1; каскад — PaidAt позиций и переход рефералок Approved → Paid; финансовые переходы фиксируются событиями статусов |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/platform/referral-payout-orders/{orderId}/mark-paid |
| Success status | 200 |
| Idempotency header | не применяется (повтор из Paid — терминальный статус → REWARDS_PAYOUT_INVALID_STATUS_TRANSITION; двойного дебета кошельков нет) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | Required | actor-stamping (PaidByUserId) |
OrderId | route {orderId} | Guid | Да | Required, непустой | — |
ExpectedVersion | body | long | Да | >= 1; сверка с Version поручения (конкурент — Reject) | optimistic concurrency |
Модель ответа PayoutOrderModel
Полная проекция агрегата — дом README.md («PayoutOrderModel»). Дельта операции:
Status = Paid, заполнены PaidAt и Items[].PaidAt, новая Version. PaidByUserId наружу не
выводится (внутренний audit-stamp).
Алгоритм
- Контекст и доступ. Операцию выполняет оператор ПИН в платформенном scope; требуется право
rewards.payouts.approve. - Проверка входа и предусловий.
- формат:
OrderIdнепустой,ExpectedVersion >= 1; иначеValidationError; - существование: поручение с
OrderIdв tenant-е ПИН сKind = ReferralRewardсуществует; иначе →REWARDS_PAYOUT_ORDER_NOT_FOUND; - допустимость состояния (INV-7 guard): статус —
PendingPayment; изDraft/Paid/Rejected/Cancelled→REWARDS_PAYOUT_INVALID_STATUS_TRANSITION; - конкуренция: версия совпадает с
ExpectedVersion; иначеREWARDS_CONCURRENCY_CONFLICT. Поручение читается под блокировкой (защита от параллельногоReject) вместе с позициями.
- формат:
- Что читаем для каскада. По позициям поручения определяются привязанные реферальные начисления
(
referral_accruals.payout_order_item_id); каждое читается под блокировкой и обязано быть в статусеApproved— иначе дефект целостности (REWARDS_REFERRAL_ACCRUAL_PAID_ONLY_VIA_PAYOUT, операция откатывается целиком). - Переход и каскад (INV-7). Поручение переходит
PendingPayment → Paid: проставляютсяPaidAt, внутреннийPaidByUserId = актор,PaidAtкаждой позиции, растёт версия. Каждое привязанное реферальное начисление переводитсяApproved → Paid(PaidAtначисления) — терминальный статус (INV-RA-5). Родительские комиссионные начисления не затрагиваются (ихPaid— каскад агентских поручений). - Фиксация фактов. Переход поручения, каскад по рефералкам и публикация событий — в одном
согласованном изменении (частичной выплаты не бывает):
PayoutOrderStatusChangedKafkaEvent(топикpin.rewards.payout-order-status-changed.v1, ключ —OrderId,Kind = ReferralReward,FromStatus = PendingPayment,ToStatus = Paid,PaidAt,Items[]{RecipientUserId, Amount}— по позициям выплаты) — контракт ../../events/payout-order-status-changed.md;- по каждому переведённому начислению —
ReferralAccrualStatusChangedKafkaEvent(топикpin.rewards.referral-accruals.v1, ключ — id начисления,Approved → Paid,PaidAt). Потребители:wallet— поItems[]события поручения дебетует счетаReferralCommissionпартнёров (ADR-0065; идемпотентность по операции и id источника — двойного дебета при повторной доставке нет);notifications— партнёрам «вознаграждение выплачено».
- Результат. Проекция поручения (
Status = Paid,PaidAt,Items[].PaidAt, новая версия) → HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | PayoutOrderStatusChangedKafkaEvent → pin.rewards.payout-order-status-changed.v1, key = OrderId (Kind = ReferralReward, PendingPayment → Paid, Items[] {RecipientUserId, Amount}; деньги — Value + CurrencyId + CurrencySnapshot, ../../../handbook/domain/snapshots.md); ReferralAccrualStatusChangedKafkaEvent → pin.rewards.referral-accruals.v1, key = id начисления (Approved → Paid) — по каждому привязанному начислению; контракт — ../../events/referral-accrual-status-changed.md |
| External calls | none (исполнение платежей вне платформы — решение №9 ТЗ) |
| Cache / projections / search | none |
| Notifications | notifications по событиям: партнёрам «вознаграждение выплачено»; wallet — дебет счетов ReferralCommission по Items[] (ADR-0065, асинхронно — окно секунды) |
Ошибки и failure modes
Общие коды (ValidationError, FORBIDDEN, UNAUTHORIZED, REWARDS_CONCURRENCY_CONFLICT) — дом
README.md «Общие ошибки». Специфичные для операции:
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
REWARDS_PAYOUT_ORDER_NOT_FOUND | 404 | Поручение не существует / не ReferralReward / чужой tenant | «Платёжное поручение не найдено» | no retry |
REWARDS_PAYOUT_INVALID_STATUS_TRANSITION | 409 | Status != PendingPayment (в т.ч. повтор из Paid) | «Поручение нельзя отметить выплаченным в текущем статусе» | no retry |
REWARDS_REFERRAL_ACCRUAL_PAID_ONLY_VIA_PAYOUT | 500 | Каскад: привязанное начисление не в Approved (дефект целостности INV-7/INV-RA-5) | «Внутренняя ошибка перехода начисления» | retry после fix |
Acceptance criteria
- AC-1 (отметка выплаты с каскадом). Given реферальное поручение
PendingPayment(Version = N) с позициями по трём партнёрам (5 привязанных рефералок вApproved); When оператор отмечает выплату сExpectedVersion = N; Then поручение →Paid(PaidAt,Items[].PaidAt,Version = N+1); все 5 рефералок →Paid(INV-7); опубликованыPayoutOrderStatusChangedKafkaEventсItems[]и 5×ReferralAccrualStatusChangedKafkaEvent(Approved → Paid); ответ200. Постусловие:walletасинхронно дебетует счетаReferralCommissionтрёх партнёров на суммы позиций;notificationsуведомляет партнёров (окно — минуты). - AC-2 (атомарность каскада). Given поручение с N привязанными рефералками; When отметка;
Then переход поручения, все
PaidAt, все переходы рефералок и публикация всех событий происходят в одном согласованном изменении — состояния «поручениеPaid, а рефералки ещёApproved» наружу не наблюдается. - AC-3 (недопустимый статус / повтор). Given поручение в
Draft/Paid/Rejected/Cancelled; When отметка (в т.ч. повторная изPaid); ThenREWARDS_PAYOUT_INVALID_STATUS_TRANSITION(409) — каскад не выполняется, события не публикуются, двойного дебета кошельков нет. - AC-4 (гонка mark-paid / reject). Given одновременные
MarkPaidиRejectтого же поручения; When обе операции; Then одна побеждает по блокировке и сверке версии, проигравшая —REWARDS_CONCURRENCY_CONFLICT(409); итог согласован: либо всё выплачено, либо всё отклонено с освобождением рефералок. - AC-5 (дефект целостности). Given привязанная рефералка оказалась не в
Approved; When каскад; ThenREWARDS_REFERRAL_ACCRUAL_PAID_ONLY_VIA_PAYOUT(500) — операция откатывается целиком. - AC-6 (не реферальное / чужое). Given
OrderIdагентского поручения или несуществующий; When отметка по этому route; ThenREWARDS_PAYOUT_ORDER_NOT_FOUND(404, без раскрытия). - AC-7 (доступ). Given актор с
rewards.payouts.approveтолько в scope агентства; When отметка; ThenFORBIDDEN(403).
Смежные операции (тонкая дельта): отклонение
rejectReferralPayoutOrder — POST /api/v1/platform/referral-payout-orders/{orderId}/reject
(rewards.payouts.approve, платформенный scope): PendingPayment → Rejected с обязательной причиной
RejectReason (trim, 1..1000; пустая → REWARDS_PAYOUT_REJECT_REASON_REQUIRED 400, INV-8); все
привязанные рефералки освобождаются (PayoutOrderItemId → null) и остаются в Approved — доступны
следующему поручению; публикуется PayoutOrderStatusChangedKafkaEvent
(PendingPayment → Rejected, RejectReason); статусы рефералок не меняются — событий по ним и движений
кошелька нет. Поведение зеркально агентскому аналогу —
../agency/reject-payout-order.md, с дельтой: tenant — компания ПИН,
источники строк — реферальные начисления, уведомление — только оператору ПИН.
Совместимость и наблюдаемость
- Payload событий — additive-only; breaking → новая версия топика (dual-publish + sunset). Потребители дедуплицируют повторную доставку по id агрегата + переходу (конвенция README.md «Согласованность и порядок событий»).
- Logs:
OrderId,Number,Kind,FromStatus→ToStatus, число переведённых рефералок (суммы не логируются — Sensitive); metrics:rewards_payout_mark_paid_total{result, kind},rewards_referral_accruals_paid_total; traces: OTel, correlation черезtraceparent; dashboardrewards-overview; runbook —runbooks/rewards-payout-lifecycle.md(по конвенции). - НФТ (ADR-0056): каскад — позиции + рефералки (десятки-сотни строк) в одной транзакции под блокировкой, публикация N+1 событий; целевая латентность p95 < 700 мс; частота — единичные отметки в месяц.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (6): GET /api/v1/platform/referral-accruals — Реестр реферальных начислений, GET /api/v1/platform/referral-payout-orders — Список реферальных платёжных поручений, POST /api/v1/platform/referral-accruals/{accrualId}/cancel — Ручная отмена реферального начисления, POST /api/v1/platform/referral-payout-orders — Создание реферального платёжного поручения, POST /api/v1/platform/referral-payout-orders/{orderId}/approve — Утверждение реферального поручения, Rewards API — реестр операций и общие контрактные модели
- Use Cases (2): UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата), US-реестр: Финансовый блок ПИН (US-FIN)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность и каскад: domain/payout-order.md (INV-7 MarkPaidCascade,
INV-8); статусы рефералок: domain/referral-accrual.md (INV-RA-5,
Paid— только каскадом поручения). - Контракт события поручения: ../../events/payout-order-status-changed.md.
- Смежные операции: approve-referral-payout-order.md, create-referral-payout-order.md (+ cancel), list-referral-payout-orders.md.
- Агентский аналог: ../agency/mark-payout-order-paid.md; отклонение: ../agency/reject-payout-order.md.
- Модели и общие ошибки: README.md; решение №9 ТЗ (исполнение платежей отложено), ADR-0065 (дебет кошелька), ADR-0067 (гейт), ADR-0052 (tenancy), ADR-0056 (simplicity-first).