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

Payments

PaymentEndpoint методы

Метод accountType PM вызов Источник данных
getMyTbAccountId все TbAccountIdDeriver (UUIDv5, без DB)
resolveRecipientByPhone consumer, wallet, agent DB phone hash lookup → v_user_tb_accounts
getTransactionHistory все SELECT public.v_tx_history (курсорная пагинация)
previewIntent все POST /intents/quote profile enrichment → _buildMetadata
createIntent все POST /intents v_user_tb_accounts + user profiles → _buildMetadata
getIntent все GET /intents/:id
previewInvoice consumer, wallet GET /intents/:id + POST /intents/quote + POST /policies/evaluate InvoiceQrDecoder + InvoiceQrSigner.verify
confirmInvoice consumer, wallet POST /intents/:id/confirm v_user_tb_accounts + step-up validation

createIntent

sequenceDiagram
    App->>PaymentEndpoint: createIntent(idempotencyKey, type, amount, currency, ...)
    PaymentEndpoint->>DB: v_user_tb_accounts → fromAccountName (сендер)
    PaymentEndpoint->>DB: user_profiles → fromName (отображаемое имя)
    alt flow=phone (recipientUserId передан)
        PaymentEndpoint->>DB: user_profiles → toName
    else flow=qr (toTbAccountId передан)
        PaymentEndpoint->>DB: v_user_tb_accounts → userId → user_profiles → toName
    end
    PaymentEndpoint->>PaymentEndpoint: _buildMetadata(senderId, recipientId, flow)
    Note over PaymentEndpoint: Round 1: users + profiles параллельно<br/>Round 2: агенты по referralAgentId параллельно
    PaymentEndpoint->>PspHmacClient: createIntent(CreateIntentRequest + metadata)
    PspHmacClient->>PM: POST /intents (HMAC signed)
    PM->>PspHmacClient: IntentDto
    PspHmacClient->>App: IntentDto

callPm() — error mapping

callPm() оборачивает каждый HMAC-вызов и конвертирует PaymentExceptionCloseloopException для корректной сериализации Serverpod.

HTTP PM статус PM error body PaymentErrorCode CloseloopException.code Что видит клиент
401 / 403 любой AUTH_FAILED AUTH_FAILED ошибка аутентификации
404 любой NOT_FOUND NOT_FOUND не найдено
422 INSUFFICIENT_FUNDS (оригинал PM) INSUFFICIENT_FUNDS недостаточно средств
422 LIMIT_EXCEEDED (оригинал PM) LIMIT_EXCEEDED превышен лимит
422 иное (оригинал PM) errCode из body PM-специфичная ошибка
5xx любой PSP_UNAVAILABLE PSP_UNAVAILABLE сервис недоступен
DioException timeout PSP_TIMEOUT PSP_TIMEOUT таймаут
DioException network PSP_UNAVAILABLE PSP_UNAVAILABLE PM недостижим

Для HTTP 422 код ошибки берётся напрямую из PM body (data['error']), не коллапсируется в VALIDATION_ERROR — это позволяет Flutter показывать конкретное сообщение пользователю.

_buildMetadata

PaymentMetadata содержит два профиля (from/to) + контекст запроса. Используется PM для fee-rules, аналитики, fiscal и referral.

Запросы выполняются в 2 параллельных раунда:

  • Раунд 1: User + UserProfile отправителя и получателя одновременно
  • Раунд 2: агенты по referralAgentId (UUID) из профилей раунда 1

Поля metadata:

Поле Источник Использование в PM
fromAccountType users.accountType fee-rules, маски
fromAccountTier user_profiles.accountTier fee-rules (JS expressions)
fromIsVatPayer user_profiles.isVatPayer fiscal / VAT split
fromTags user_profiles.tags SQL tag matching в fee-rules
fromReferralAgentId/UserId user_profiles.referralAgentId → lookup комиссионный сплит агента
toAccountType/Tier/Tags/... аналогично для получателя fee-rules получателя
source hardcode 'mobile_app' аналитика
flow 'phone' / 'qr' / null аналитика

TbAccountIdDeriver

Параметр Значение
Алгоритм UUIDv5 (RFC 4122)
Namespace 3e7b4a1c-9f2d-5e8a-b6c3-4d1f07e2a9b5 (TB_NS)
Canonical name user.{userId}.{currency}
Детерминизм userId=1, THB → фиксированный UUID без DB-запроса
Синхронизация байт-идентичен TypeScript реализации в payment-manager/src/ledger/accounts.ts

getMyTbAccountId() возвращает UUID только для USER_WALLET — не использовать для MERCHANT/AGENT аккаунтов.

HMAC-ключи

PspHmacClient поддерживает два ключа:

Ключ Env Применение
PmKey.auth PM_SERVICE_ID / PM_SECRET все стандартные вызовы
PmKey.merchant PM_MERCHANT_SERVICE_ID / PM_MERCHANT_SECRET NFC pull-charge (NFC_CHARGE)

Canonical string: {timestampSeconds}\n{METHOD}\n{path}\n{sha256hex(body)}

Заголовки: X-Service-Id, X-Timestamp, X-Signature, X-User-Id.

resolveRecipientByPhone — логика null

Метод возвращает null (без ошибки) в случаях:

  • профиль не найден по hash телефона
  • пользователь неактивен или архивирован
  • самоперевод (senderId == recipientId)
  • у получателя нет THB-кошелька в pm.tb_account_map

Телефон хешируется PiiHashService.hashPhone() перед DB-запросом — в БД хранится только hash, не plaintext.