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

UC-WLT-002. Выплата поручения дебетует баланс получателя

Назначение

Описывает системный сценарий: платёжное поручение отмечено выплаченным в rewards (payout-order.md) — и кошелёк списывает выплаченные суммы с балансов получателей: доли агентов — со счетов AgentCommission, реферальные вознаграждения — со счетов ReferralCommission. Баланс холдера показывает только невыплаченный остаток обязательств. Связан с контекстом wallet.

Metadata

ПолеЗначение
IDUC-WLT-002
Контекстwallet (см. docs/06-services/wallet/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (consumer событий rewards)
Permissions / scopeне применяется (m2m-обработка событий)
Tenantвне tenant-модели (wallet-account.md)

Предусловие

  • Поручение прошло PendingPayment → Paid (ручная отметка после фактической выплаты вне платформы, решение №9 ТЗ); каскад rewards выполнен (позиции, строки-источники, начисления — payout-order.md, INV-7).
  • Балансы получателей ранее кредитованы по гейту получения денег (UC-WLT-001) — кредиты предшествуют дебетам по бизнес-процессу.
  • Событие pin.rewards.payout-order-status-changed.v1 с ToStatus = Paid несёт позиции Items[] (id позиции, получатель, сумма) и вид поручения (payout-order-status-changed.md).

Триггер

ПолеЗначение
ИсточникKafka event
Триггерящий элементpin.rewards.payout-order-status-changed.v1 (ToStatus = Paid) → consumer rewards-payout-order-status-changed.md

Основной Поток

  1. Кошелёк получает событие выплаты поручения: вид поручения, позиции Items[] с получателями и суммами.
  2. Для каждой позиции определяется холдерский счёт: AgencyCommission-поручение → счёт AgentCommission агента-получателя; ReferralReward-поручение → счёт ReferralCommission партнёра.
  3. По каждой позиции проводится транзакция PayoutDebit (SourceId = id позиции): дебет холдерского счёта, кредит PlatformSettlement на сумму позиции (ledger-transaction.md).
  4. Guard no-negative: баланс холдерского счёта после дебета >= 0 (wallet-account.md, INV-WLT-3) — при выполнении транзакция и балансы фиксируются атомарно.
  5. По каждой проводке холдерского счёта публикуется pin.wallet.transaction-recorded.v1; notifications доставляет получателю «вам выплачено {Amount}».
  6. Холдер видит уменьшенный баланс и операцию списания в истории (UC-WLT-004).

Альтернативные Потоки

A1. Дебет без покрытия (рассинхрон rewards ↔ wallet)

  1. Дебет позиции увёл бы баланс холдерского счёта ниже нуля (потерян/задержан кредит, счёт не существует).
  2. Это не бизнес-отказ, а детектор рассинхрона: транзакция не проводится, сообщение уходит в DLQ + алерт (WALLET_BALANCE_NEGATIVE_FORBIDDEN), создаётся issue Reconciliation Center (ADR-0021).
  3. Разбор: недостающий кредит доводится командой владельца данных (rewards) либо компенсирующей проводкой — журнал не правится.
  4. Сообщение реплеится из DLQ: уже проведённые позиции — no-op, недостающие — проводятся.

A2. Повторная доставка события

  1. Событие Paid доставлено повторно (at-least-once) или обрабатывается параллельно вторым инстансом.
  2. Каждая позиция упирается в unique (OperationKind, SourceId) — no-op (INV-WLT-7): балансы не меняются, события повторно не публикуются.

A3. Частично обработанное событие

  1. Обработка прервалась после части позиций (сбой/рестарт) либо часть позиций ушла в DLQ (A1).
  2. Повторная доставка/replay доводит оставшиеся позиции; идемпотентность гарантирует эквивалентность однократной полной обработке.

Постусловие

  • По каждой позиции поручения существует ровно одна транзакция PayoutDebit «дебет холдерского счёта / кредит PlatformSettlement».
  • Балансы получателей уменьшены на суммы своих позиций и остались >= 0; баланс PlatformSettlement увеличен (погашение обязательств).
  • Для каждого затронутого счёта: Balance = Σ кредитов − Σ дебетов его проводок.
  • События pin.wallet.transaction-recorded.v1 опубликованы; уведомления о выплате доставлены.

Возможные Ошибки

КодHTTPУсловиеПоведение
WALLET_BALANCE_NEGATIVE_FORBIDDEN— (consumer)Счёт получателя отсутствует / дебет позиции не покрыт балансом (INV-WLT-3)Транзакция не проводится; DLQ + алерт + issue Reconciliation Center; replay после исправления
WALLET_UNKNOWN_ENUM— (consumer)Неизвестный Kind поручения / пустые Items[] при Paid — счёт списания не определитьNon-retriable → DLQ, разбор контракта с rewards
WALLET_CURRENCY_MISMATCH— (consumer)Валюта позиции ≠ валюте счёта / вне допустимых (INV-WLT-4)Non-retriable → DLQ + алерт (дефект контракта данных)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
WalletTransactionRecordedKafkaEventpin.wallet.transaction-recorded.v1Атомарно с проведением каждой PayoutDebit-транзакцииnotifications

Acceptance Criteria

#КритерийПроверка
AC1Given поручение с N позициями перешло в Paid, When кошелёк обработал событие, Then проведено N PayoutDebit-транзакций и балансы получателей уменьшены ровно на суммы позицийинтеграционный тест consumer-а
AC2Given реферальное поручение (ReferralReward), When обработано событие Paid, Then списание идёт со счетов ReferralCommission партнёров, не затрагивая их AgentCommission-балансытест раздельности балансов
AC3Given уже обработанное событие Paid, When оно доставлено повторно, Then балансы не изменились (no-op по (OperationKind, SourceId))тест двойной доставки (INV-WLT-7)
AC4Given позиция, не покрытая балансом, When проводится дебет, Then транзакция не проведена, сообщение в DLQ, алерт поднят, покрытые позиции того же события проведенынегативный тест A1
AC5Given исправленный рассинхрон, When сообщение реплеится из DLQ, Then доведены только недостающие позиции (проведённые — no-op) и балансы сходятся с журналомтест replay

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

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

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