This commit is contained in:
Kirill
2026-07-02 23:13:13 +07:00
parent e997a20fe6
commit d4a57c0b6a
34 changed files with 524 additions and 67 deletions
+43
View File
@@ -0,0 +1,43 @@
# `modules/billing` — тарифы, инвойсы, оплата
## Поток оплаты
1. **`POST /billing/pay/initiate`** (`initiate_payment`) — сервер сам смотрит цену тарифа
в нужной валюте (`plan_prices`, см. [`migrations/0001_init.sql`](../../../migrations/0001_init.sql)),
запрашивает текущий курс TON у TonAPI, считает сумму в nanoTON с буфером **+2%**
(`calculate_ton_invoice` — компенсация волатильности курса между генерацией инвойса и
отправкой транзакции юзером) и создаёт `invoices`-запись со статусом `pending`.
Клиент **не может повлиять на сумму** — она целиком считается на сервере.
2. Фронтенд получает `{ destination, amount, comment }` и просит кошелёк (TonConnect)
отправить транзакцию с этим `comment` (= `invoice_id`) — по нему инвойс потом находится.
3. **`POST /billing/pay/confirm`** (`confirm_payment`) — клиент присылает `tx_hash`
отправленной транзакции. Сервер:
- проверяет, что инвойс принадлежит вызывающему юзеру (иначе можно было бы дёргать
чужие `invoice_id` и провоцировать обращения к блокчейну — DoS/перебор);
- идемпотентен: повторный вызов на уже `paid` инвойсе — no-op;
- просит `TonProvider::verify_payment` сходить в блокчейн и сверить **сумму (>=),
адрес назначения и комментарий** транзакции по хешу;
- при успехе переводит инвойс в `paid`, активирует подписку (`buy_subscription`,
заодно выдаёт один игровой билет, до кэпа в 10) и начисляет рефереру 10% от суммы
инвойса (`reward_referrer`).
4. **Фоновая сверка** (`reconcile_pending_payments`, гоняется из [`tasks.rs`](../../tasks.rs))
находит `pending`-инвойсы младше суток, чьи транзакции уже видны в истории
мастер-кошелька (по совпадению `comment == invoice_id`), и доводит их до `paid` — на
случай, если клиент не смог/не успел вызвать `/pay/confirm` сам.
## Провайдеры (`providers/`)
`PaymentProvider` — трейт, чтобы позже добавить Stripe/YooKassa, не трогая
`BillingService`. Сейчас единственная реализация — [`ton::TonProvider`] (HTTP к TonAPI,
без собственного состояния кроме адреса мастер-кошелька).
## Триал
`activate_trial` доступен только аккаунтам с `tg_id` (см. [`auth/README.md`](../auth/README.md#sybil-защита))
и только один раз (`has_used_trial`).
## Известные ограничения (см. корневой README «Недоделки»)
Синхронный запрос курса TON на каждый `initiate_payment` и пересборка `providers`
(включая повторное чтение env) при каждом `Clone` сервиса — оба помечены `// TODO` прямо
в `service.rs`.