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

Платёжные сценарии

Простыми словами: что происходит, когда пользователь переводит деньги, пополняет кошелёк, выводит средства, платит мерчанту или мини-аппу. Все деньги — в THB (в системе хранятся в satang: 1 THB = 100 satang).

Все платежи проходят через единый endpoint Payment Manager: POST /intents (подписывается HMAC). У каждого «намерения» (intent) есть статус-машина из 9 состояний: CREATED → VALIDATED → AUTHORIZED → SETTLING → SETTLED, плюс ответвления FAILED / MANUAL_REVIEW / CANCELED / EXPIRED.

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

  • Что значит «перевод прошёл мгновенно», а что — «ожидает подтверждения»?
  • Чем top-up отличается от вывода средств, и почему вывод может занять время?
  • Как устроена оплата по счёту мерчанта (invoice) и оплата на кассе (POS)?
  • Что такое charge/credit для мини-аппа и оплата NFC-меткой?
  • Что из этого уже работает, а что запланировано на Phase 2B?

Сценарии и статусы

Сценарий operationType Что происходит Статус
P2P-перевод между пользователями P2P_TRANSFER Канал INTERNAL_P2P — деньги списываются и зачисляются синхронно в одном запросе. Готово
Пополнение кошелька QP_TOPUP Внешний PSP-канал зачисляет средства; финал приходит асинхронно. Готово (PSP Phase 1 in-process)
Вывод на банк/счёт (IPPS) IPPS_WITHDRAWAL Списание в кошельке, перевод через IPPS; финал асинхронно. Готово (PSP Phase 1 in-process)
Вывод по Thai QR THAI_QR_PAY Оплата по QR во внешнюю сеть; финал асинхронно. Готово (PSP Phase 1 in-process)
Прочий вывод WITHDRAWAL Обобщённый вывод, требует externalRef. Готово
Счёт мерчанта (invoice) INVOICE_PAYMENT Двухфазно: резерв счёта → подтверждение/отмена (см. ниже). Готово
Оплата на кассе (POS) INVOICE_PAYMENT / NFC_CHARGE Мерчант формирует QR-инвойс или принимает NFC; QR-инвойс = тот же двухфазный invoice. Готово
Списание у мини-аппа MINIAPP_CHARGE Мини-апп списывает с кошелька покупателя в пользу мерчанта. Готово
Зачисление от мини-аппа MINIAPP_CREDIT Мини-апп зачисляет средства пользователю (возврат/бонус). Готово
Оплата NFC-меткой NFC_CHARGE Pull-charge: списание с кошелька клиента, зачисление мерчанту (metadata.nfcTagId). Готово
Сервисное зачисление SERVICE_DEPOSIT Зачисление пользователю от сервиса (metadata.recipientUserId). Готово
Перевод оператором ADMIN_TRANSFER Ручная операция из admin-panel. Готово

Внешние Redis-Streams PSP-адаптеры (stream.ipps.*, stream.qp.*, webhook gateway) — Phase 2B, не реализовано. Сейчас PSP-логика IPPS работает in-process внутри PM (psp-worker, очередь в таблице psp_tx_map).

Синхронный vs асинхронный финал

  • INTERNAL P2P (channel=INTERNAL_P2P) проходит CREATED → SETTLED синхронно в одном HTTP-запросе. Ответ сразу содержит status: SETTLED, requiresMonitoring: false.
  • Внешние PSP-каналы (top-up, вывод) возвращаются на AUTHORIZED с requiresMonitoring: true; клиент подписывается на Redis-канал intent.{id} и получает финальный статус позже (PM → Auth Center → Flutter).

Пример P2P-запроса (значения иллюстративные):

POST /intents
{ "idempotencyKey": "…uuid…", "operationType": "P2P_TRANSFER",
  "amount": 15000, "currency": "THB", "recipientUserId": 4821,
  "comment": "обед" }
→ 200 { "status": "SETTLED", "requiresMonitoring": false }
(amount: 15000 satang = 150.00 THB.)

P2P-перевод (синхронно)

sequenceDiagram
    participant App as Flutter (one_loop_app)
    participant AC as Auth Center
    participant PM as Payment Manager
    participant TB as TigerBeetle
    App->>AC: перевод 150 THB пользователю
    AC->>PM: POST /intents (P2P_TRANSFER, HMAC)
    PM->>TB: списание + зачисление (синхронно)
    TB-->>PM: ok
    PM-->>AC: 200 status=SETTLED
    AC-->>App: перевод выполнен

Merchant invoice — двухфазный поток

Счёт мерчанта оплачивается в две фазы:

  1. reservePOST /intents с INVOICE_PAYMENT создаёт счёт без обращения к TigerBeetle, сохраняет expiresAt и подпись qrSignature (для QR на кассе). Статус инвойса — CREATED.
  2. confirm/cancel — плательщик подтверждает (/intents/:id/confirm) — деньги реально движутся в TigerBeetle, статус → SETTLED; либо отмена (/intents/:id/cancel, только из CREATED).
  3. expire — фоновая задача invoice-expiry переводит просроченные счёта в EXPIRED (атомарно, только из CREATED).
sequenceDiagram
    participant M as Мерчант (POS / merchant_app)
    participant PM as Payment Manager
    participant P as Плательщик (Flutter)
    participant TB as TigerBeetle
    M->>PM: POST /intents INVOICE_PAYMENT (reserve)
    PM-->>M: счёт CREATED + qrSignature + expiresAt
    M-->>P: показывает QR / NFC
    P->>PM: POST /intents/:id/confirm
    PM->>TB: списание + зачисление мерчанту
    TB-->>PM: ok
    PM-->>P: SETTLED
    Note over PM: если не подтверждён до expiresAt —<br/>invoice-expiry → EXPIRED

NFC-метки и POS

  • POS-инвойс (QR) — мерчант (one_merchant_app) формирует QR-инвойс, это тот же двухфазный INVOICE_PAYMENT с подписью qrSignature.
  • NFC на кассе / NFC-меткаNFC_CHARGE (pull-charge): списание с кошелька клиента и зачисление мерчанту; metadata.nfcTagId (целое) идентифицирует метку. recipientUserId, toTbAccountId, externalRef для NFC запрещены.

См. также