# `modules/billing` — тарифы, инвойсы, оплата ## Поток оплаты 1. **`POST /billing/pay/initiate`** (`initiate_payment`) — сервер сам смотрит цену тарифа в нужной валюте (`plan_prices`, см. [`migrations/0001_init.sql`](../../../migrations/0001_init.sql)), берёт курс TON (сначала кеш в Redis — `ton_rate:`, TTL 60с; на промах — TonAPI, ответ кладётся в кеш), считает сумму в nanoTON с буфером **+2%** (`calculate_ton_invoice` — компенсация волатильности курса между генерацией инвойса и отправкой транзакции юзером) и создаёт `invoices`-запись со статусом `pending`. Клиент **не может повлиять на сумму** — она целиком считается на сервере. Запрос к TonAPI идёт по коду валюты из инвойса (не захардкожен на `usd,rub`), а ответ разбирается по ключу в ВЕРХНЕМ регистре (`prices.USD`) — так его и отдаёт TonAPI. 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`). ## Известные ограничения Пересборка `providers` (включая повторное чтение env) при каждом `Clone` сервиса — axum клонирует `ApiState`, а с ним и `BillingService`, на каждый HTTP-запрос; см. `// TODO` прямо в `service.rs`. Курс TON теперь кешируется (см. выше), это ограничение — нет.