DTO Contracts — все Zod-схемы request/response Payment Manager
Канонический реестр публичных DTO Payment Manager. Каждая схема описана с указанием файла и строки, типов полей, обязательности и связи с endpoint. Любое изменение DTO должно идти через эту страницу — она блокирует Phase 4 (API).
Назначение
Этот файл — единая «опись» Zod-схем, на которых строятся все публичные HTTP-контракты PM. Здесь нет ни прозы про сценарии, ни описаний каналов — только структура входов/выходов:
- что приходит на каждый endpoint (body, params, headers);
- что endpoint возвращает (200/201/4xx/5xx);
- какие поля обязательные, какие опциональные, какие имеют значение по умолчанию;
- из какого файла/строки исходного кода извлечена схема.
Раздел читают разработчики Auth Center, Admin Panel и mini-app backends, которые формируют запросы в PM, а также авторы ../../api/ — там идёт прозовое описание сценариев, а DTO берутся отсюда по ссылке.
Источник правды — Zod-схемы в src/. Если документ расходится с кодом — багует документ.
Группы DTO
| Группа |
Endpoint |
Раздел |
| Intent создание |
POST /intents |
§ 1 |
| Fee quote |
POST /intents/quote |
§ 2 |
| Invoice confirm |
POST /intents/:id/confirm |
§ 3 |
| Invoice cancel |
POST /intents/:id/cancel |
§ 4 |
| Policy evaluate |
POST /policies/evaluate |
§ 5 |
| Balance |
GET /accounts/:name/balance |
§ 6 |
| Transaction history |
GET /accounts/:name/transactions |
§ 7 |
Дополнительные DTO endpoint'ов POST /accounts, GET /intents/:id, GET /intents/:id/events относятся к этой же категории, но их описания вынесены в ../../api/ — здесь только семь основных групп.
Общие принципы
- Денежные суммы в JSON всегда строки (BigInt не сериализуется в JSON). Внутри Zod-схем — либо
z.number().int().positive() для входа (тысячи THB не превышают safe integer), либо z.string() для выхода.
- Единица измерения —
satang: 1 THB = 100 satang. Все amount, fee, balance — в satang.
idempotencyKey — UUID v4. PM рассматривает пару (idempotencyKey, serviceId) как ключ идемпотентности на POST /intents.
- Discriminated union по
operationType — единая BaseIntentBody парсится, потом по operationType выбирается конкретный OperationTypeDefinition.parseBody, который может расширить или сузить контракт (например, INVOICE_PAYMENT требует toTbAccountId, WITHDRAWAL — externalRef).
- HMAC headers обязательны для всех endpoint'ов:
X-Service-Id, X-Timestamp, X-Signature, X-User-Id. Описаны в 06-service-keys.md.
IntentStatus — единый enum для всех intent-эндпоинтов: CREATED, VALIDATED, AUTHORIZED, SETTLING, SETTLED, FAILED, MANUAL_REVIEW, CANCELED, EXPIRED.
1. IntentRequest / IntentResponse (POST /intents)
Главный endpoint PM — создаёт платёжный intent. Тело — discriminated union по operationType: единая база BaseIntentBody + extension от конкретного OperationTypeDefinition.parseBody.
1.1 BaseIntentBody — корень discriminated union
- Файл:
src/intent/operation-type.ts:4
- Используется в:
POST /intents (body)
| Поле |
Тип |
Обязательность |
Описание |
idempotencyKey |
string (UUID) |
да |
Уникальный ключ идемпотентности. Та же пара (idempotencyKey, serviceId) возвращает существующий intent с HTTP 200 |
operationType |
string |
да |
Тип операции — см. ниже список из 11 значений |
amount |
number (int > 0) |
да |
Сумма в satang. На входе число (для удобства), на выходе строка |
currency |
string (length=3) |
нет (default THB) |
ISO 4217 код валюты |
metadata |
Record<string, unknown> |
нет (default {}) |
Operation-specific метаданные (см. § 1.3) |
fromAccountName |
string |
нет |
Override источника. Требует fromAccountOverride permission |
toAccountName |
string |
нет |
Override получателя. Требует toAccountOverride permission |
toTbAccountId |
string (UUID) |
нет |
Получатель по TigerBeetle UUID. Требует allowToTbAccountId permission |
fromName |
string (max 255) |
нет |
Display name отправителя (для истории/чеков) |
toName |
string (max 255) |
нет |
Display name получателя |
comment |
string (max 500) |
нет |
Свободная заметка к платежу |
live |
boolean |
нет |
Если true — PM публикует статусы в Redis-канал intent.{id} |
externalRef |
string |
нет |
Внешний reference ID (требуется для WITHDRAWAL, запрещён для P2P_TRANSFER/NFC_CHARGE/MINIAPP_*) |
recipientUserId |
number (int > 0) |
нет |
Serverpod userId получателя. Требуется для P2P_TRANSFER (если нет toTbAccountId), запрещён для WITHDRAWAL, MINIAPP_CHARGE, NFC_CHARGE, INVOICE_PAYMENT |
1.2 Discriminated union по operationType (11 значений)
PM поддерживает ровно 11 типов операций. Каждый имеет свой OperationTypeDefinition в src/operation-types/*.ts, который parseBody() дополнительно валидирует, и resolveAccounts() — резолвит fromAccountName / toAccountName из userId + currency + metadata.
operationType |
Файл (name декл.) |
parseBody-инвариант |
resolveAccounts |
P2P_TRANSFER |
src/operation-types/p2p-transfer.ts:5 |
Требует recipientUserId или toTbAccountId; запрещён externalRef |
user.{userId}.{currency} → user.{recipientUserId}.{currency} |
IPPS_WITHDRAWAL |
src/operation-types/ipps.ts:32 |
Через ippsPreValidate: ippsWalletId зарегистрирован, metadata.ipps.receiverType валиден |
user.{userId}.{currency} → system.nostro.ipps.{currency} |
THAI_QR_PAY |
src/operation-types/ipps.ts:42 |
Идентично IPPS_WITHDRAWAL (тот же ippsPreValidate) |
user.{userId}.{currency} → system.nostro.ipps.{currency} |
WITHDRAWAL |
src/operation-types/withdrawal.ts:5 |
Требует externalRef; запрещён recipientUserId |
user.{userId}.{currency} → system.nostro.ipps.{currency} |
QP_TOPUP |
src/operation-types/withdrawal.ts:27 |
Без дополнительных проверок |
system.nostro.ipps.{currency} → user.{userId}.{currency} |
MINIAPP_CHARGE |
src/operation-types/miniapp.ts:5 |
Запрещены recipientUserId, externalRef |
user.{customerUserId}.{currency} → user.{merchantId}.{currency} |
MINIAPP_CREDIT |
src/operation-types/miniapp.ts:27 |
Запрещён externalRef |
user.{merchantId}.{currency} → user.{customerUserId}.{currency} |
SERVICE_DEPOSIT |
src/operation-types/service-transfer.ts:5 |
Требует metadata.recipientUserId (positive int) |
service.{userId}.{currency} → user.{recipientUserId}.{currency} |
ADMIN_TRANSFER |
src/operation-types/admin-transfer.ts:3 |
Без дополнительных проверок |
{ null, null } — оба аккаунта обязаны прийти через override |
INVOICE_PAYMENT |
src/operation-types/invoice-payment.ts:29 |
Требует toTbAccountId, metadata соответствует InvoiceMetadata; запрещён recipientUserId; ttlSeconds ≤ INVOICE_MAX_TTL_SECONDS |
{ null, null } — fromAccountName неизвестен до confirm |
NFC_CHARGE |
src/operation-types/nfc-charge.ts:8 |
Запрещены recipientUserId, toTbAccountId, externalRef; metadata.nfcTagId должен быть int |
{ null, null } — оба аккаунта через override |
Значения IPPS_DEPOSIT, MINIAPP_PAYMENT, RESERVED, INIT, DONE не существуют в коде. Если попадётся в клиенте — это бага клиента.
Поле metadata парсится BaseIntentBody как Record<string, unknown>, но каждый OperationTypeDefinition имеет неявный контракт на содержимое:
operationType |
Обязательные поля metadata |
Опциональные |
P2P_TRANSFER |
— |
tags[] (для limit-checks) |
IPPS_WITHDRAWAL, THAI_QR_PAY |
ipps.receiverType ∈ {MSISDN, NATID, EWALLETID, BANKAC, BILLERID}, ipps.receiverAccount. ipps.receiverBank для BANKAC/BILLERID |
— |
MINIAPP_CHARGE |
customerUserId, merchantId (опционален — fallback на X-User-Id) |
— |
MINIAPP_CREDIT |
customerUserId, merchantId |
— |
SERVICE_DEPOSIT |
recipientUserId (positive int) |
— |
INVOICE_PAYMENT |
(см. InvoiceMetadata, src/operation-types/invoice-payment.ts:15): appScope ∈ {w, c, a} default a |
note (≤500), posTerminalId (≤100), ttlSeconds, qrIssuedAt |
NFC_CHARGE |
— |
nfcTagId (int), lat, lon, to.businessCategoryCode |
ADMIN_TRANSFER |
— |
— |
WITHDRAWAL, QP_TOPUP |
— |
— |
1.4 CreateIntentResponse — ответ POST /intents
- Файл:
src/intent/handler.ts:31
- HTTP:
201 Created (новый intent), 200 OK (idempotency replay)
| Поле |
Тип |
Обязательность |
Описание |
intentId |
string (UUID) |
да |
Уникальный ID intent'а |
status |
IntentStatus |
да |
Текущий статус: CREATED, VALIDATED, AUTHORIZED, SETTLING, SETTLED, FAILED, MANUAL_REVIEW, CANCELED, EXPIRED |
channel |
string |
да |
Channel, выбранный router'ом: INTERNAL_P2P, IPPS_TRANSFER, MERCHANT_INVOICE, SERVICE_TRANSFER, ADMIN |
amount |
string (BigInt) |
нет |
Сумма в satang (только при idempotency replay) |
currency |
string |
нет |
Код валюты (только при idempotency replay) |
preFeeAmount |
string (BigInt) |
да |
Итоговая PRE-комиссия (с отправителя) |
postFeeAmount |
string (BigInt) |
да |
Итоговая POST-комиссия (с получателя) |
requiresMonitoring |
boolean |
да |
Если true — клиент должен подписаться на intent.{id} (Redis pub/sub) |
createdAt |
string (ISO 8601) |
да |
Время создания |
expiresAt |
string (ISO 8601) |
нет |
Время истечения (только для two-phase, например MERCHANT_INVOICE) |
qrSignature |
string (base64url) |
нет |
HMAC-SHA256 QR-подпись (16 байт) для MERCHANT_INVOICE |
version |
number (int) |
нет |
Optimistic concurrency version |
1.5 ErrorResponse — стандартное тело ошибок
- Файл:
src/intent/handler.ts:46
| Поле |
Тип |
Обязательность |
Описание |
error |
string |
да |
Код ошибки: VALIDATION_ERROR, FORBIDDEN, NOT_FOUND, LIMIT_EXCEEDED, INSUFFICIENT_FUNDS, IPPS_NOT_REGISTERED, IPPS_METADATA_INVALID, ACCOUNT_NOT_FOUND, TB_TRANSFER_ERROR, NO_ROUTE |
message |
string |
да |
Читаемое описание |
detail |
unknown |
нет |
Структурированная подробность. Для LIMIT_EXCEEDED: {ruleName, window, limitType, limit, current, requested} |
1.6 Возможные HTTP-коды POST /intents
| Код |
Сценарий |
| 200 |
Idempotency replay |
| 201 |
Новый intent создан |
| 400 |
BAD_REQUEST (missing/forbidden field), NO_ROUTE |
| 401 |
HMAC-аутентификация провалена, X-Timestamp вне окна ±60s |
| 403 |
Сервис неактивен, operationType не в allowedOperationTypes, account override недопустим |
| 422 |
VALIDATION_ERROR, ACCOUNT_NOT_FOUND, IPPS_NOT_REGISTERED, IPPS_METADATA_INVALID, INSUFFICIENT_FUNDS, LIMIT_EXCEEDED |
| 500 |
TB_TRANSFER_ERROR — TigerBeetle отверг transfer |
Прозовое описание сценариев — TODO: ../../api/intents.md.
2. QuoteRequest / QuoteResponse (POST /intents/quote)
Преview PRE-комиссии без создания intent'а. Без записи в БД — только расчёт.
2.1 QuoteBody — тело запроса
- Файл:
src/intent/quote.ts:6
| Поле |
Тип |
Обязательность |
Описание |
operationType |
enum |
да |
Один из: P2P_TRANSFER, IPPS_WITHDRAWAL, THAI_QR_PAY, WITHDRAWAL, MINIAPP_CHARGE, MINIAPP_CREDIT, QP_TOPUP, SERVICE_DEPOSIT, ADMIN_TRANSFER, INVOICE_PAYMENT |
amount |
number (int > 0) |
да |
Сумма в satang |
currency |
string (length=3) |
нет (default THB) |
ISO 4217 |
metadata |
Record<string, unknown> |
нет (default {}) |
Метаданные для matching правил комиссий (тэги, атрибуты) |
⚠️ Enum в QuoteBody не включает NFC_CHARGE — для NFC-операций fee-quote недоступен (см. src/intent/quote.ts:7). Если он понадобится — расширить enum.
2.2 QuoteResponse — ответ
- Файл:
src/intent/quote.ts:13
| Поле |
Тип |
Обязательность |
Описание |
fee |
string (BigInt) |
да |
Итоговая PRE-комиссия в satang |
totalCharge |
string (BigInt) |
да |
amount + fee — сколько спишется с отправителя |
breakdown[] |
{account, amount}[] |
да |
Разбиение по получателям комиссий |
breakdown[].account |
string |
да |
Имя fee-account |
breakdown[].amount |
string (BigInt) |
да |
Доля комиссии (satang) |
2.3 QuoteErrorResponse
- Файл:
src/intent/quote.ts:22
| Поле |
Тип |
Обязательность |
Описание |
error |
string |
да |
Код ошибки (VALIDATION_ERROR) |
details |
unknown |
да |
Подробности Zod-валидации |
2.4 HTTP-коды
| Код |
Сценарий |
| 200 |
OK — quote вычислен |
| 422 |
VALIDATION_ERROR — некорректный body |
Прозовое описание — TODO: ../../api/intents.md.
3. ConfirmBody / ConfirmResponse (POST /intents/:id/confirm)
Подтверждение MERCHANT_INVOICE покупателем. Двухфазный flow: покупатель сканирует QR → передаёт свой tbAccountId + userId → PM атомарно переводит CREATED → VALIDATED → AUTHORIZED → SETTLED.
3.1 ConfirmBody — тело запроса
- Файл:
src/intent/confirm-handler.ts:28
| Поле |
Тип |
Обязательность |
Описание |
payerTbAccountId |
string (UUID) |
да |
TigerBeetle UUID счёта плательщика |
payerUserId |
number (int > 0) |
да |
Serverpod userId плательщика |
idempotencyKey |
string (UUID) |
да |
Ключ идемпотентности (защищает от двойного списания при ретраях) |
3.2 Заголовки
| Header |
Обязательность |
Описание |
If-Match |
нет |
Ожидаемая version intent'а (optimistic concurrency). Если не совпадает — 409 VERSION_MISMATCH |
3.3 ConfirmResponse — ответ
- Файл:
src/intent/confirm-handler.ts:68 (replay), src/intent/confirm-handler.ts:190 (success)
| Поле |
Тип |
Обязательность |
Описание |
intentId |
string (UUID) |
да |
UUID intent'а |
status |
IntentStatus |
да |
Финальный статус (обычно SETTLED) |
channel |
string |
да |
Канал (всегда MERCHANT_INVOICE для confirm) |
createdAt |
string (ISO 8601) |
да |
Время создания intent'а |
version |
number (int) |
да |
Текущая version (после +1 при успешном confirm) |
replayed |
boolean |
нет |
true при idempotency replay (тот же idempotencyKey + payerUserId + уже не CREATED) |
3.4 HTTP-коды
| Код |
Сценарий |
| 200 |
OK — settled или idempotency replay |
| 400 |
PAYER_ACCOUNT_USER_MISMATCH, X-User-Id required |
| 404 |
INTENT_NOT_FOUND, PAYER_ACCOUNT_NOT_FOUND |
| 409 |
ALREADY_PROCESSED (intent не в CREATED), EXPIRED (expiresAt < now()), VERSION_MISMATCH (If-Match не совпал) |
Прозовое описание — TODO: ../../api/intents.md.
4. CancelBody / CancelResponse (POST /intents/:id/cancel)
Отмена MERCHANT_INVOICE мерчантом. Только инициатор инвойса (issuedByUserId) или сервис с X-User-Id: 0 (admin bypass) может отменить.
4.1 CancelBody — тело запроса
- Файл:
src/intent/cancel-handler.ts:19
| Поле |
Тип |
Обязательность |
Описание |
reason |
string (min 1, max 50) |
нет (default merchant_canceled) |
Причина отмены — пишется в intent_event.reason |
4.2 Заголовки
| Header |
Обязательность |
Описание |
X-User-Id |
да |
Integer — userId инициатора отмены. 0 = admin bypass |
4.3 CancelResponse — ответ
- Файл:
src/intent/cancel-handler.ts:69
| Поле |
Тип |
Обязательность |
Описание |
intentId |
string (UUID) |
да |
UUID intent'а |
status |
IntentStatus |
да |
Всегда CANCELED |
channel |
string |
да |
Канал intent'а (обычно MERCHANT_INVOICE) |
preFeeAmount |
string (BigInt) |
да |
Сохранённое значение PRE-fee (0 для не-settled инвойса) |
postFeeAmount |
string (BigInt) |
да |
Сохранённое значение POST-fee |
requiresMonitoring |
boolean |
да |
Всегда false (terminal state) |
createdAt |
string (ISO 8601) |
да |
Время создания intent'а |
4.4 HTTP-коды
| Код |
Сценарий |
| 200 |
OK — CANCELED |
| 400 |
X-User-Id required, CANCEL_NOT_SUPPORTED (channel не two-phase) |
| 403 |
UNAUTHORIZED_ACTOR — X-User-Id ≠ issuedByUserId и ≠ 0 |
| 404 |
INTENT_NOT_FOUND |
| 409 |
CANNOT_CANCEL — intent уже не в статусе CREATED (status возвращается в теле) |
Прозовое описание — TODO: ../../api/intents.md.
5. EvaluatePolicyRequest / EvaluatePolicyResponse (POST /policies/evaluate)
Вычисляет требуемый StepUp для платёжной операции. Auth Center вызывает перед созданием intent'а, чтобы понять — нужна ли дополнительная аутентификация (PIN, биометрия, OTP).
5.1 EvalReqSchema — тело запроса
- Файл:
src/policies/routes.ts:18
| Поле |
Тип |
Обязательность |
Описание |
userId |
number (int > 0) |
да |
userId инициатора платежа |
merchantId |
number (int > 0) |
нет |
userId мерчанта (для проверки new_payee) |
amount |
string (regex ^\d+$) |
да |
Сумма в minor units. Строка — чтобы избежать потери точности |
currency |
string (length=3) |
да |
ISO 4217 |
channel |
string (min 1, max 50) |
да |
Канал (INTERNAL_P2P, IPPS_TRANSFER, MERCHANT_INVOICE, …) |
appId |
string (min 1, max 50) |
да |
Идентификатор приложения (для scope app:X) |
operationType |
string (min 1, max 50) |
да |
Тип операции (INVOICE_PAYMENT, P2P_TRANSFER, …) |
5.2 Ответ — PolicyDecision
- Файл:
src/policies/routes.ts:63
| Поле |
Тип |
Обязательность |
Описание |
required |
string |
да |
Уровень StepUp: none, pin, biometric, otp, block |
reasonCode |
string |
да |
Код причины срабатывания политики |
policyId |
string | null |
да |
ID политики (null — default fallthrough) |
context |
object |
да |
Контекст вычисления |
context.dailyCumulative |
string (BigInt) |
да |
Сумма платежей за сегодня (для лимитов) |
context.isNewPayee |
boolean |
да |
true если первый платёж этому мерчанту |
5.3 HTTP-коды
| Код |
Сценарий |
| 200 |
OK — decision вычислен |
| 422 |
Zod validation ошибка (некорректный body) |
Подробнее о политиках — ../../../AUTH-POLICIES.md.
Прозовое описание — TODO: ../../api/policies.md.
6. BalanceResponse (GET /accounts/:name/balance)
Возвращает баланс одного аккаунта напрямую из TigerBeetle (credits_posted − debits_posted). Аккаунт идентифицируется по имени (user.123.THB) или TB UUID.
6.1 Параметры пути
- Файл:
src/ledger/routes.ts:115 (AccountParams)
| Параметр |
Тип |
Обязательность |
Описание |
name |
string |
да |
Имя аккаунта (user.123.THB) или TB UUID (matches UUID_RE) |
6.2 BalanceResponse — ответ
- Файл:
src/ledger/routes.ts:119
| Поле |
Тип |
Обязательность |
Описание |
accountName |
string |
да |
Имя аккаунта (возвращается тот же name, что пришёл) |
balance |
string (BigInt) |
да |
Баланс в satang. Вычисляется как credits_posted - debits_posted из TigerBeetle |
currency |
string |
да |
Код валюты (parsed из последнего сегмента имени, либо THB если был UUID) |
6.3 HTTP-коды
| Код |
Сценарий |
| 200 |
OK — баланс возвращён |
| 401 |
HMAC отсутствует/невалиден |
| 404 |
Account not found: {name} |
Прозовое описание — TODO: ../../api/accounts.md.
7. HistoryResponse (GET /accounts/:name/transactions)
Пагинированная история транзакций из pm.tx_history. Используется Flutter-приложением и Admin Panel для показа операций пользователя.
7.1 Параметры пути
- Файл:
src/ledger/routes.ts:115 (AccountParams)
| Параметр |
Тип |
Обязательность |
Описание |
name |
string |
да |
Имя аккаунта или TB UUID. Для UUID — резолвится через pm.tb_account_map в имя, по которому ищется в tx_history |
7.2 Querystring — TxQuerySchema
- Файл:
src/ledger/routes.ts:110
| Параметр |
Тип |
Обязательность |
Описание |
limit |
number (int, 1–100) |
нет (default 20) |
Сколько транзакций вернуть (max 100) |
offset |
number (int, ≥ 0) |
нет (default 0) |
Pagination offset |
7.3 TransactionsResponse — ответ
- Файл:
src/ledger/routes.ts:125
| Поле |
Тип |
Обязательность |
Описание |
transactions[] |
object[] |
да |
Массив транзакций (отсортирован по createdAt DESC) |
transactions[].id |
number |
да |
ID строки tx_history |
transactions[].intentId |
string (UUID) |
да |
UUID исходного intent'а |
transactions[].operationType |
string |
да |
Один из 11 operationType |
transactions[].direction |
string |
да |
DEBIT или CREDIT (с точки зрения этого аккаунта) |
transactions[].amount |
string (BigInt) |
да |
Сумма в satang |
transactions[].feeAmount |
string (BigInt) |
да |
Комиссия по этой ноге транзакции |
transactions[].currency |
string |
да |
Код валюты |
transactions[].createdAt |
string (ISO 8601) |
да |
Время записи в tx_history |
Поле transactions[].attributes (operation-specific PII gate) пока не возвращается наружу — фильтруется на этапе сериализации, см. src/ledger/routes.ts:205.
7.4 HTTP-коды
| Код |
Сценарий |
| 200 |
OK — массив (может быть пустым, если UUID не нашёлся в tb_account_map) |
| 401 |
HMAC отсутствует/невалиден |
Прозовое описание — TODO: ../../api/accounts.md.
См. также