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

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, WITHDRAWALexternalRef).
  • 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 не существуют в коде. Если попадётся в клиенте — это бага клиента.

1.3 Operation-specific metadata

Поле 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_ACTORX-User-IdissuedByUserId и ≠ 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.


См. также

  • ./02-channels.md — реестр channel-кодов, на которые маппятся operationType.
  • ./03-operation-types.md — детали по каждому из 11 operationType: какие fields обязательны, кто инициатор, какой channel применим.
  • ./05-error-codes.md — полный реестр кодов ошибок PM.
  • ./06-service-keys.md — HMAC permissions для service-key (fromAccountOverride, toAccountOverride, allowToTbAccountId).
  • ../../../../PASSPORT.md — index-манифест канонического контракта PM.