ADR 0004: Единый платёжный endpoint POST /intents¶
Дата: 2026-06-06 Статус: Accepted
На какие вопросы отвечает¶
- Через какой API делается любой платёж в OneWallet?
- Почему P2P-перевод, оплата QR-инвойса, NFC-списание, пополнение и комиссия миниаппа идут в один и тот же endpoint?
- Чем различаются вызывающие (Auth Center, nginx, mini-app, будущие PSP) на уровне API?
- Что отличает один платёж от другого, если endpoint один?
- Почему INTERNAL-перевод завершается сразу, а внешний возвращается «недоделанным»?
Контекст¶
Деньги в OneWallet пишутся только в TigerBeetle, и единственный write-клиент к нему — Payment Manager (см. ADR 0001). К PM обращаются разные источники:
- Auth Center (Serverpod) — P2P, инициация QR/NFC из приложений;
- хостовой nginx-ingress (
/api/pm/*, service-idnginx-gateway); - mini-app backends (charge/credit);
- в перспективе — PSP-инициированные потоки.
Альтернатива — по отдельному endpoint на каждую операцию (/p2p, /withdrawal, /invoice, /nfc…) — была отвергнута: дублирование валидации, лимитов, расчёта комиссий, аудита (intent_event) и связи с леджером в N местах; рост площади атаки; рассинхрон контрактов между сервисами.
Решение¶
Все платежи проходят через один endpoint — POST /intents (источник: один обработчик app.post('/intents', …) в projects/payment-manager/src/intent/handler.ts). Тип операции передаётся полем operationType, а не разными путями.
Поддерживаемые operationType (11): P2P_TRANSFER, IPPS_WITHDRAWAL, THAI_QR_PAY, WITHDRAWAL, QP_TOPUP, MINIAPP_CHARGE, MINIAPP_CREDIT, SERVICE_DEPOSIT, ADMIN_TRANSFER, INVOICE_PAYMENT, NFC_CHARGE.
Единые свойства endpoint для всех вызывающих:
- Единая аутентификация — HMAC-SHA256 (
X-Service-Id/X-Timestamp/X-Signature), без «доверенных» исключений (см. ADR 0002). - Единая воронка — валидация → лимиты (
limit_rule) → расчёт комиссий (fee_rule) → выбор канала (payment_routeпоoperationType+amount) → запись в TigerBeetle (two-phase) → аудит вintent/intent_event. - Единый трейсинг —
trace_id = intent_idв каждом событии.
Вызывающие различаются только значением X-Service-Id (свой HMAC-секрет) и полезной нагрузкой; бизнес-логика и контракт одни на всех.
Один endpoint — две развязки финализации¶
POST /intents создаёт intent и сразу прогоняет его по state machine (9 состояний). Финал зависит от выбранного канала:
- INTERNAL (например
P2P_TRANSFER):CREATED → … → SETTLEDсинхронно, в одном HTTP-запросе. - Внешние PSP-каналы (IPPS/QP): запрос возвращается на
AUTHORIZEDсrequiresMonitoring=true, финал приходит асинхронно через Redis pub/subintent.{id}.
Особый случай:
INVOICE_PAYMENTсоздаётся через тот жеPOST /intents(продавец резервирует сумму, плательщик неизвестен до подтверждения), а завершается отдельнымиPOST /intents/:id/confirm|/cancel. См. ADR 0006 и dev-доку ниже.
flowchart LR
AC[Auth Center] -->|HMAC| EP
NG[nginx-gateway] -->|HMAC| EP
MA[mini-app backend] -->|HMAC| EP
PSP[PSP flows] -->|HMAC| EP
EP[POST /intents\noperationType=...] --> SM{channel?}
SM -->|INTERNAL| S[SETTLED синхронно]
SM -->|PSP| A[AUTHORIZED\nrequiresMonitoring=true]
A -.->|Redis pub/sub intent.id| F[SETTLED / FAILED]
S --> TB[(TigerBeetle)]
F --> TB
Пример¶
P2P-перевод из приложения (Auth Center как подписант). Тело запроса:
{
"operationType": "P2P_TRANSFER",
"fromAccountName": "user-1042-main",
"recipientUserId": 2087,
"amount": "150.00",
"currency": "THB"
}
P2P_TRANSFER маршрутизируется на канал INTERNAL → ответ приходит уже со статусом SETTLED в том же запросе. Если бы тот же endpoint вызвали с operationType=IPPS_WITHDRAWAL, ответ вернулся бы на AUTHORIZED c requiresMonitoring=true, а финал — событием intent.{id} в Redis.
Последствия¶
Плюсы:
- Один контракт и одна реализация валидации/лимитов/комиссий/аудита — меньше дублирования и рассинхрона.
- Новая операция = новая запись payment_route + ветка в реестре операций, а не новый endpoint и новый клиент.
- Единая граница безопасности: один HMAC-механизм, одна точка логирования и идемпотентности.
Минусы / ограничения:
- Endpoint «полиморфен» — поля payload зависят от operationType (например, у INVOICE_PAYMENT плательщик неизвестен на этапе create). Валидация per-operation обязательна.
- Две модели завершения (sync INTERNAL vs async PSP) в одном API — вызывающий обязан корректно обрабатывать requiresMonitoring и подписку на intent.{id}.
- Любая ошибка в общей воронке затрагивает все операции — высокие требования к тестам.
Связанные документы¶
- dev/04 — Платежи и леджер — детальный поток, state machine, каналы, two-phase.
- ADR 0001 — TigerBeetle как леджер
- ADR 0002 — Единый HMAC
- ADR 0006 — Клиенты и appId
- PM-доки:
../../projects/payment-manager/docs/dev/