Платежи и леджер¶
Как устроен платёжный поток 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
Смежные документы¶
- 01-overview.md — карта сервисов и потоков
- 03-data-and-schemas.md — схема
pm.*(intent, intent_event, psp_tx_map…) - 05-security-and-auth.md — HMAC и step-up auth
- ../business/04-scenarios.md — бизнес-сценарии платежей
- projects/payment-manager/docs/dev/modules/ — intent, channels, operation-types, ledger, psp