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

Классы TigerBeetle-аккаунтов

Архитектурный документ перечисляет все классы аккаунтов, которые Payment Manager создаёт в TigerBeetle, описывает их назначение, схему именования, TB-параметры (code/ledger/flags) и роль в денежных потоках. Это эталонное справочное место, на которое опираются остальные документы архитектуры: оно отвечает на вопрос «какие сущности существуют в леджере и как они между собой связаны».


1. Где источник истины

Все имена и коды аккаунтов определяются в одном месте — src/ledger/accounts.ts:

  • accountName(type, opts) — строит каноническое строковое имя аккаунта (используется как первичный ключ в pm.tb_account_map).
  • accountId(name) — детерминированно вычисляет bigint TB-ID как uuidv5(name, TB_NS). Namespace UUID фиксирован навсегда — изменение инвалидирует все существующие TB-ID. Подробнее см. ./06-deterministic-ids.md.
  • ACCOUNT_CODES — карта AccountType → TB code (используется TigerBeetle для категоризации аккаунтов).
  • accountFlags(type) — выставляет debits_must_not_exceed_credits и history в зависимости от класса.

Схема таблицы pm.tb_account_map, которая хранит соответствие account_name → tb_account_id — в ../reference/database/06-tb-account-map.md.


2. Инвариант: единственный писатель TB

Write-доступ к TigerBeetle есть только у Payment Manager. Никакой другой сервис (Auth Center, Admin Panel, PSP-адаптеры, KYC-воркер, Notifications) не вызывает createAccounts / createTransfers напрямую. Admin Panel может читать TB (lookup), но любые изменения проходят через POST /intents с HMAC.

Это критически важно, потому что TigerBeetle — единственный источник истины по балансам. Допусти второго писателя — и инвариант transit.balance = 0 (см. §3) станет недоказуемым.


3. Перечень классов аккаунтов

Всего в системе семь классов аккаунтов. Каждый имеет уникальный TB code, свою схему имени и свой набор flags.

Класс Code Префикс имени debits_must_not_exceed_credits history
USER_WALLET 10 user.<userId>.<currency>
TRANSIT 20 system.transit.<channel>.<currency>
REVENUE 30 system.revenue.<currency>
NOSTRO 40 system.nostro.<provider>.<currency>
MERCHANT_WALLET (+ MERCHANT_SETTLEMENT) 50 / 60 merchant.<merchantId>.<currency> / merchant.<merchantId>.settlement.<currency> ✅ / ❌ ✅ / ✅
AGENT_WALLET (+ AGENT_SETTLEMENT) 90 / 100 agent.<agentId>.<currency> / agent.<agentId>.settlement.<currency> ✅ / ❌ ✅ / ✅
SERVICE_ACCOUNT 70 service.<serviceUserId>.<currency>
EQUITY 80 system.equity.<currency>

Все аккаунты живут на ledger = 1 (для THB). Мультиледжерная схема зарезервирована под другие валюты, но в проде на сегодня один.

3.1 USER_WALLET — кошелёк физлица

  • Назначение: хранит баланс конкретного пользователя в конкретной валюте.
  • Имя: user.<userId>.<currency> (например, user.42.THB).
  • Flags: debits_must_not_exceed_credits (нельзя уйти в минус) + history (полная история движений).
  • Кто создаёт: lazy — при первом платеже через ensureAccount в context-builder.
  • Кто пишет: PM, шаги саги (debit-source, credit-destination).

3.2 TRANSIT — клиринговый аккаунт канала

  • Назначение: служит «перевалочным пунктом» при двухходовых переводах. Деньги списываются с источника на TRANSIT, потом с TRANSIT на получателя — обе операции линкованы как одна линкованная цепочка в TigerBeetle (TransferFlags.linked).
  • Имя: system.transit.<channel>.<currency>. channel — это логический канал саги (INTERNAL_P2P, IPPS, MERCHANT, MERCHANT_INVOICE, SERVICE_TRANSFER, INTERNAL). Это не PSP (IPPS, QP, WISE — это PspName, см. модули psp-router/ и psp-adapters/).
  • Flags: без debits_must_not_exceed_credits, без history — TRANSIT обязан проходить через ноль, и его движения видны через саму историю transfers, отдельная account history не нужна.
  • Инвариант: balance(transit) = credits_posted − debits_posted = 0 после успешного завершения интента. Это главный финансовый инвариант системы: если на TRANSIT остались деньги — где-то посередине саги что-то застряло. TigerBeetle гарантирует это через linked transfers: либо оба перевода (source → transit и transit → destination) применяются, либо ни один.
  • Кто создаёт: seed на старте PM (seedAccounts) — каждый канал получает свой TRANSIT.
  • Кто пишет: PM, через authorize() + settle() в src/ledger/transfers.ts.

3.3 REVENUE — собственный доход системы

  • Назначение: аккумулирует все комиссии OneWallet (APP_FEE). Каждый платный интент порождает дополнительный шаг apply-fee, который зачисляет фи с TRANSIT на REVENUE.
  • Имя: system.revenue.<currency> (одно на валюту).
  • Flags: только history — REVENUE может расти бесконечно, и нам нужна детальная история начислений для бухгалтерии.
  • TB transfer code для фи: TRANSFER_CODES.APP_FEE = 20 (см. src/ledger/transfer-codes.ts).

3.4 NOSTRO — корреспондентский счёт у внешнего провайдера

  • Назначение: «зеркало» нашего реального счёта у банка/PSP. Когда пользователь топит баланс через IPPS, деньги физически лежат на нашем счету в банке, и в TB это отражается как NOSTRO → USER_WALLET. При выводе наоборот.
  • Имя: system.nostro.<provider>.<currency>. provider — идентификатор партнёра, не путать с PspName. Сейчас сидируется ipps (default) и bank.
  • Flags: только history. Без debits_must_not_exceed_credits — технически TB-аккаунт может уйти в минус относительно реального банка, потому что мы отражаем внешний поток с задержкой.

3.5 MERCHANT_WALLET / MERCHANT_SETTLEMENT — мерчант

  • Назначение: отдельный класс аккаунтов для мерчантов (юрлиц, принимающих оплаты через инвойсы). См. [memory: project-multi-entity-accounts] — мерчант не «просто кошелёк пользователя», у него свой класс TB-аккаунта с отдельным code (50 / 60) и своей политикой ролей в context-builder.
  • Имя:
  • merchant.<merchantId>.<currency> — операционный кошелёк, куда падают платежи от покупателей.
  • merchant.<merchantId>.settlement.<currency> — settlement-аккаунт, с которого деньги уходят на банковский счёт мерчанта по расписанию.
  • Flags WALLET: debits_must_not_exceed_credits + history.
  • Flags SETTLEMENT: только history — settlement-аккаунт может быть проведён в минус для отражения payout-операций до их технической post-фазы.

3.6 AGENT_WALLET / AGENT_SETTLEMENT — агент

  • Назначение: аналог merchant, но для агентов (cash-in/cash-out точки, например физлица или магазины, обслуживающие топ-апы налом). Это также отдельный класс, code 90 / 100. Отдельный класс нужен, чтобы лимиты, отчёты и комплаенс-правила для агентов применялись только к ним и не пересекались с мерчантами и физлицами.
  • Имя: agent.<agentId>.<currency> и agent.<agentId>.settlement.<currency>.
  • Flags: аналогично merchant.

3.7 SERVICE_ACCOUNT — служебный аккаунт mini-app

  • Назначение: mini-app внутри OneWallet (например, оплата коммуналки или мобильной связи) держит собственный сервисный кошелёк, с которого списываются деньги пользователя в её пользу.
  • Имя: service.<serviceUserId>.<currency>.
  • Flags: debits_must_not_exceed_credits + history.

3.8 EQUITY — equity-аккаунт системы

  • Назначение: «затыкающий» аккаунт для внешних потоков, не привязанных к конкретному пользовательскому переводу — например, отражение комиссий PSP, корректировок, операционных списаний из Admin Panel (channel=ADMIN).
  • Имя: system.equity.<currency>.
  • Flags: только history. Без debits_must_not_exceed_credits — EQUITY может уходить в минус, потому что трекает суммарный внешний flow.

4. Поток денег: P2P-перевод

Ниже — пример канонического INTERNAL_P2P платежа: пользователь A переводит пользователю B 100 THB с комиссией системы 1 THB. Все TB-transfers внутри одного интента линкованы (TransferFlags.linked) и применяются атомарно — либо все, либо ни одного.

flowchart LR
    A["user.A.THB<br/>(USER_WALLET)"]
    T["system.transit.INTERNAL_P2P.THB<br/>(TRANSIT)"]
    B["user.B.THB<br/>(USER_WALLET)"]
    R["system.revenue.THB<br/>(REVENUE)"]

    A -- "1. debit 101<br/>code=PAYMENT" --> T
    T -- "2. credit 100<br/>code=PAYMENT" --> B
    T -- "3. credit 1<br/>code=APP_FEE" --> R

    classDef wallet fill:#e0f2fe,stroke:#0284c7
    classDef transit fill:#fef3c7,stroke:#d97706,stroke-dasharray: 4 2
    classDef revenue fill:#dcfce7,stroke:#16a34a
    class A,B wallet
    class T transit
    class R revenue

После применения всех трёх transfers:

  • user.A.THB: −101 THB (101 списано).
  • user.B.THB: +100 THB (получил перевод).
  • system.revenue.THB: +1 THB (комиссия системы накопилась).
  • system.transit.INTERNAL_P2P.THB: credits_posted = 101, debits_posted = 101balance = 0.

Если на TRANSIT после интента остался ненулевой баланс — это инцидент, который ловится verifySystemAccounts() на старте PM и алёртами operations-стека.

4.1 Two-phase: pending → post / void

Шаги выше используют двухфазную модель TigerBeetle:

  1. Authorize (src/ledger/transfers.ts::authorize) — создаёт pending transfers с TransferFlags.pending и timeout=300s. Деньги замораживаются на debits_pending / credits_pending.
  2. Settle (src/ledger/transfers.ts::settle) — пост этих pending через TransferFlags.post_pending_transfer. Деньги мигрируют в debits_posted / credits_posted.
  3. Void (src/ledger/transfers.ts::voidAll) — откатывает pending через TransferFlags.void_pending_transfer (при ошибке саги).

Все post/void-ID детерминированы от pending-ID (postId(pid) = pid ^ 1n, voidId(pid) = pid ^ 2n) — это позволяет безопасно ретраить операции без риска создать дубликат. Подробнее о схеме детерминированных ID см. ./06-deterministic-ids.md.


5. Лениво vs. seed на старте

Класс Когда создаётся
USER_WALLET Лениво — при первом платеже (через ensureAccount)
MERCHANT_* Лениво — при создании мерчанта в Auth Center
AGENT_* Лениво — при онбординге агента
SERVICE_ACCOUNT Лениво — при регистрации mini-app
TRANSIT SeedseedAccounts() на старте PM (по одному на канал)
REVENUE Seed — один на валюту
NOSTRO Seed — один на пару provider × currency
EQUITY Seed — один на валюту

seedAccounts(db) идемпотентен — повторный вызов не создаёт дубликатов (использует pm.tb_account_map как guard и обрабатывает CreateAccountStatus.exists от TB).

После seed выполняется verifySystemAccounts(db), который для каждого системного аккаунта параллельно проверяет, что он есть и в TB (через lookupAccounts), и в pm.tb_account_map. Расхождение логируется на уровне error.


6. Где это используется

  • Создание / резолв аккаунтов: src/ledger/accounts.tscreateAccount, getAccountByName, getAccountByTbId.
  • Применение transfers саги: src/ledger/transfers.tsauthorize, settle, voidAll.
  • Описание модуля ledger в целом: ../modules/ledger.md.
  • Таблица pm.tb_account_map (mapping account_name → uuid): ../reference/database/06-tb-account-map.md.
  • Резолв ролей (кто чей кошелёк по контексту интента) описан в ./03-intent-saga.md — Auth Center всегда передаёт явное fromAccountName / toAccountName, PM никогда не «угадывает» аккаунт по роли (см. memory: feedback-account-resolution).

7. Чек-лист для нового класса аккаунтов

Если когда-нибудь понадобится завести новый класс (например, escrow):

  1. Добавить литерал в AccountType (src/shared/schema.ts).
  2. Добавить ветку в accountName() и решить схему имени.
  3. Добавить code в ACCOUNT_CODES (новое число, не пересекающееся с существующими).
  4. Решить, какие flags нужны (обычно history; для пользовательских кошельков — также debits_must_not_exceed_credits).
  5. Если аккаунт системный — добавить в SYSTEM_ACCOUNTS, чтобы создавался seed-ом и проверялся verifySystemAccounts.
  6. Описать новый класс здесь, в этом документе.
  7. Обновить миграции и pm.tb_account_map (если меняется лимит длины account_name или появляются новые accountType-значения).

См. также: