Перейти к содержанию

Платежи и леджер

Как устроен платёжный поток OneWallet: единый вход POST /intents в Payment Manager (PM), state-машина intent'а, операции (operationType) и двухфазная бухгалтерия в TigerBeetle (TB).

На какие вопросы отвечает

  • Какие статусы проходит платёж и чем синхронный P2P отличается от внешнего PSP?
  • Какие типы операций (operationType) поддерживает платформа?
  • Почему деньги «не теряются»: что такое two-phase и инвариант transit.balance = 0?
  • Как клиент узнаёт финальный статус асинхронного платежа (вывод через IPPS)?
  • Как связать логи платежа и записи в леджере? (trace_id = intent_id)

Intent state machine (9 статусов)

Источник: projects/payment-manager/src/intent/handler.ts (enum IntentStatus).

stateDiagram-v2
  [*] --> CREATED
  CREATED --> VALIDATED: rule-engine, fee, limits OK
  CREATED --> CANCELED: cancel (только из CREATED)
  CREATED --> EXPIRED: invoice-expiry / TTL
  VALIDATED --> AUTHORIZED: TB pending создан
  VALIDATED --> FAILED: отказ
  VALIDATED --> MANUAL_REVIEW: подозрительно
  AUTHORIZED --> SETTLED: TB post (синхронно P2P / async PSP)
  AUTHORIZED --> FAILED: PSP отклонил / void
  SETTLING --> SETTLED
  SETTLED --> [*]
  FAILED --> [*]
  CANCELED --> [*]
  EXPIRED --> [*]

SETTLING — промежуточный статус для multi-step саг; для большинства каналов переход идёт сразу AUTHORIZED → SETTLED.

operationType (11 типов)

Каждый тип — отдельный модуль в projects/payment-manager/src/operation-types/.

operationType Назначение Канал/исполнение
P2P_TRANSFER перевод между кошельками INTERNAL, синхронно
INVOICE_PAYMENT оплата merchant-инвойса (reserve→confirm/cancel) INTERNAL, двухфазно
NFC_CHARGE оплата по NFC-метке мерчанта INTERNAL, синхронно
MINIAPP_CHARGE списание в мини-аппе INTERNAL
MINIAPP_CREDIT зачисление из мини-аппа INTERNAL
ADMIN_TRANSFER операторская корректировка INTERNAL
SERVICE_DEPOSIT служебное пополнение INTERNAL
IPPS_WITHDRAWAL вывод в банк через IPPS внешний PSP, async
THAI_QR_PAY оплата по Thai QR через IPPS внешний PSP, async
WITHDRAWAL вывод средств внешний PSP
QP_TOPUP пополнение через QP внешний PSP

TigerBeetle: two-phase и инварианты

PM — единственный write-клиент TB (admin-panel читает read-only; Auth Center и PSP прямого доступа не имеют). См. adr/0001-tigerbeetle-ledger.md.

  • Two-phase (pending → post). На AUTHORIZED создаётся pending-перевод (резерв средств через transit-счёт). На SETTLED он подтверждается (post), при отказе — void. Источник: src/ledger/transfers.ts.
  • transit.balance = 0 — главный финансовый инвариант. Транзитный счёт в норме обнулён: всё, что зашло, ушло. Гарантируется TB атомарностью связанных (linked) переводов.
  • Детерминированные id. TB account id = uuidv5(name, TB_NS) (src/ledger/accounts.ts); transfer id = uuidv5(intentId:index, TRANSFER_NS), а post/void id = pendingId ^ 1 / pendingId ^ 2 (src/ledger/id-gen.ts, transfers.ts). Поэтому id воспроизводимы при retry и проверяемы без доп. state.
  • trace_id = intent_id — обязателен в каждом платёжном событии; связывает логи PM, записи pm.intent_event и переводы TB.

Подробнее: projects/payment-manager/docs/dev/architecture/ (two-phase, TB accounts, deterministic IDs).

Синхронный P2P vs асинхронный PSP

  • INTERNAL (P2P, invoice, NFC, mini-app). Весь путь CREATED → SETTLED проходит синхронно внутри одного HTTP-запроса (settleSync()), ответ сразу финальный, requiresMonitoring: false.
  • Внешний PSP (IPPS/QP). Ответ возвращается на AUTHORIZED с requiresMonitoring: true — клиент обязан подписаться на Redis pub/sub-канал intent.{id}. Финал (SETTLED/FAILED) приходит асинхронно после ответа PSP. Phase 1: PSP-логика IPPS работает in-process (psp-worker, очередь pm.psp_tx_map, FOR UPDATE SKIP LOCKED); Redis-Streams адаптеры (Phase 2B) ещё не реализованы.

См. единый вход и контракт API: adr/0004-single-intent-api.md, projects/payment-manager/docs/dev/api/.

Пример: асинхронный вывод через IPPS

sequenceDiagram
  participant App as Flutter
  participant AC as Auth Center
  participant PM as Payment Manager
  participant TB as TigerBeetle
  participant PSP as IPPS (psp-worker)
  App->>AC: вывод 500 THB
  AC->>PM: POST /intents (HMAC, IPPS_WITHDRAWAL)
  PM->>TB: pending-перевод (резерв)
  PM-->>AC: 200 AUTHORIZED, requiresMonitoring=true
  AC-->>App: AUTHORIZED + подпишись на intent.{id}
  PSP->>PSP: отправка в IPPS, inquiry статуса
  PSP->>TB: post (успех) / void (отказ)
  PM->>AC: Redis PUBLISH intent.{id} = SETTLED
  AC-->>App: stream → SETTLED

Смежные документы