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-вызов и конвертирует PaymentException → CloseloopException для корректной сериализации 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.