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

POST /api/v1/platform/referral-payout-orders/{orderId}/approve — Утверждение реферального поручения

Переход реферального платёжного поручения (Kind = ReferralReward, tenant — компания ПИН) Draft → PendingPayment: оператор ПИН утверждает собранный черновик, состав позиций замораживается и поручение передаётся в бухгалтерию ПИН как «ожидается» (статусная модель и INV-6 ApproveGuard — domain/payout-order.md). Агентский аналог — ../agency/approve-payout-order.md.

Назначение

Утвердить черновик, сформированный командой создания (create-referral-payout-order.md): проверить готовность (есть хотя бы одна позиция), заморозить состав (после PendingPayment изменение позиций запрещено — INV-5 ComposeOnlyInDraft) и известить партнёров событием статуса. После утверждения поручение доступно к отметке фактической выплаты. Feature group — referral-payouts.

Metadata

ПолеЗначение
Сервис/контекстrewards
API areaplatform
Feature groupreferral-payouts
Статусdraft
Документdocs/06-services/rewards/api/platform/approve-referral-payout-order.md

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

ПроверкаЗначение
Actor typeUser (оператор ПИН)
Auth policy / Permissionsrewards.payouts.approve (платформенный scope)
Scope (RBAC)платформа
Tenant isolationпоручение принадлежит tenant-у компании ПИН и имеет Kind = ReferralReward; агентское поручение по этому route → REWARDS_PAYOUT_ORDER_NOT_FOUND (без раскрытия)
Auditupdate: ApprovedAt, ApprovedByUserId = ActorUserId, UpdatedByUserId, Version + 1; финансовый переход фиксируется PayoutOrderStatusChangedKafkaEvent

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/platform/referral-payout-orders/{orderId}/approve
Success status200
Idempotency headerне применяется (естественная идемпотентность: повтор из PendingPaymentREWARDS_PAYOUT_INVALID_STATUS_TRANSITION)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequiredactor-stamping
OrderIdroute {orderId}GuidДаRequired, непустой
ExpectedVersionbodylongДа>= 1; сверка с Version поручения (конкурент — Cancel)optimistic concurrency

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

Полная проекция агрегата — дом README.mdPayoutOrderModel»). Дельта операции: Status = PendingPayment, заполнен ApprovedAt, Items — замороженный состав позиций по партнёрам, новая Version. ApprovedByUserId наружу не выводится (внутренний audit-stamp).

Алгоритм

  1. Контекст и доступ. Операцию выполняет оператор ПИН в платформенном scope; требуется право rewards.payouts.approve.
  2. Проверка входа и предусловий.
    • формат: OrderId непустой, ExpectedVersion >= 1; иначе ValidationError;
    • существование: поручение с OrderId в tenant-е ПИН с Kind = ReferralReward существует; иначе (нет, агентское поручение, чужой tenant) → REWARDS_PAYOUT_ORDER_NOT_FOUND;
    • допустимость состояния (INV-6): статус — Draft; иначе (в т.ч. повторное утверждение) → REWARDS_PAYOUT_INVALID_STATUS_TRANSITION;
    • готовность (INV-6): ItemsCount >= 1; пустой черновик → REWARDS_PAYOUT_EMPTY;
    • конкуренция: версия совпадает с ExpectedVersion; иначе REWARDS_CONCURRENCY_CONFLICT. Поручение читается под блокировкой с позициями (сериализация с конкурентной отменой).
  3. Переход и заморозка состава. Поручение переходит Draft → PendingPayment (INV-6); состав замораживается (INV-5): добавление/исключение начислений после утверждения запрещено; привязанные рефералки остаются Approved и защищены от отмены захватом (PayoutOrderItemId != null, INV-RA-5). Проставляются ApprovedAt и внутренний ApprovedByUserId, растёт версия; TotalAmount/ItemsCount не пересчитываются.
  4. Фиксация факта. Переход и публикация события — в одном согласованном изменении: PayoutOrderStatusChangedKafkaEvent (топик pin.rewards.payout-order-status-changed.v1, ключ — OrderId, Kind = ReferralReward, FromStatus = Draft, ToStatus = PendingPayment, итог и снимок позиций) — контракт ../../events/payout-order-status-changed.md; потребитель — notifications (партнёрам позиций «вам направлена выплата», оператору — сводно).
  5. Результат. Обновлённая проекция (Status = PendingPayment, ApprovedAt, замороженные позиции, новая версия) → HTTP 200.

Диаграмма

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

ТипДетали
Integration eventsPayoutOrderStatusChangedKafkaEventpin.rewards.payout-order-status-changed.v1 (key = OrderId); Kind = ReferralReward, FromStatus = Draft, ToStatus = PendingPayment
External callsnone
Cache / projections / searchnone
Notificationsопосредованно: notifications — консюмер payout-order-status-changed: партнёрам позиций «вам направлена выплата», оператору «поручение {Number} утверждено»

Ошибки и failure modes

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

КодHTTP statusУсловиеСообщениеRetry
REWARDS_PAYOUT_ORDER_NOT_FOUND404Поручение не существует / не ReferralReward / чужой tenant«Платёжное поручение не найдено»no retry
REWARDS_PAYOUT_INVALID_STATUS_TRANSITION409Status != Draft (INV-6; в т.ч. повторное утверждение)«Поручение нельзя утвердить в текущем статусе»no retry
REWARDS_PAYOUT_EMPTY409ItemsCount < 1 — черновик без позиций (INV-6)«Нельзя утвердить поручение без позиций»no retry

Acceptance criteria

  • AC-1 (утверждение готового черновика). Given реферальное поручение Draft с ItemsCount >= 1 (Version = N); When оператор утверждает с ExpectedVersion = N; Then поручение переходит Draft → PendingPayment, состав заморожен (INV-5), ApprovedAt заполнен, Version = N+1; опубликовано PayoutOrderStatusChangedKafkaEvent (Draft → PendingPayment, Kind = ReferralReward); ответ 200. Постусловие: notifications асинхронно уведомляет партнёров позиций (окно доставки — минуты); рефералки состава остаются Approved и не могут быть отменены, пока привязаны.
  • AC-2 (пустой черновик). Given поручение Draft с ItemsCount = 0 (все начисления исключены правкой до заморозки); When утверждение; Then REWARDS_PAYOUT_EMPTY (409); событие не публикуется.
  • AC-3 (недопустимый статус / повтор). Given поручение уже PendingPayment/Paid/Rejected/Cancelled; When утверждение (в т.ч. повторное); Then REWARDS_PAYOUT_INVALID_STATUS_TRANSITION (409).
  • AC-4 (гонка approve/cancel). Given одновременные утверждение и отмена того же черновика; When обе операции; Then одна побеждает по блокировке и сверке версии, проигравшая — REWARDS_CONCURRENCY_CONFLICT (409); итоговое состояние согласовано (либо утверждено, либо отменено с освобождением рефералок).
  • AC-5 (не реферальное / чужое). Given OrderId агентского поручения (Kind = AgencyCommission) или несуществующий; When утверждение по этому route; Then REWARDS_PAYOUT_ORDER_NOT_FOUND (404, без раскрытия).
  • AC-6 (доступ). Given актор с rewards.payouts.approve только в scope организации-агентства (не платформы); When утверждение; Then FORBIDDEN (403).

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

  • Payload события — additive-only; breaking → pin.rewards.payout-order-status-changed.v2 (dual-publish
    • sunset).
  • Logs: OrderId, Number, Kind, Draft→PendingPayment, ItemsCount (суммы не логируются — Sensitive); metrics: rewards_payout_approve_total{result, kind}; traces: OTel, correlation через traceparent; dashboard rewards-overview.
  • НФТ (ADR-0056): переход одного агрегата под блокировкой + одно событие; целевая латентность p95 < 400 мс; частота — единичные утверждения в месяц.

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

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

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