Биллинг живёт в AmneziaWG-stats (Applications/billing_core.py, BillingProcessor.py), WhiteBox его потребляет через REST API.
billing_core.pyБез I/O и ORM. Импортируется тремя потребителями, чтобы они не разъезжались:
Interfaces/Views/admin_payers._create_billing_subscription — создание подписки (UI)Applications/BillingProcessor.process_payer — рантайм списание/throttle (cron)Interfaces/Views/admin_billing_simulator.simulate — предпросмотр день за днём (UI)compute_daily_rate(pmk) → max(ceil(pmk/30), 1)compute_period_paid(plan, months) → через plan.price_for() (или fallback −10/−20%)compute_subscription_init(plan, period_months, with_trial, now) → SubscriptionInit dataclassevaluate_billing_state(state, balance, now, *, trust_active) → BillingDecisionSubState.from_orm(sub) — снимок подписки| Состояние | Когда | Что происходит |
|---|---|---|
STATE_TRIAL |
Активный пробный период (trial_ends_at > now) |
Без списаний, полная скорость |
STATE_PAID_PERIOD |
Оплачено на N мес (tariff_expires_at > now, billing_type='manual') |
Без daily-списаний, полная скорость |
STATE_PROMO |
Promo-тариф (duration_days > 0) |
Без списаний, истекает по сроку |
STATE_ACTIVE |
Стандартный платный, баланс есть | Daily-списание |
STATE_THROTTLED |
Баланс ≤ 0 / истёк период | 128 кбит/с, уведомление в TG |
STATE_FREE |
Бесплатный тариф (is_free=True) |
Без списаний |
tests/test_billing_simulator_parity.py — 3 сценария (promo 7 дней, daily burns through balance, trial+daily+topup). Каждый создаёт реальную подписку, гонит process_payer день за днём и сравнивает балансы с симулятором. Должны совпадать день в день.
billing_type='manual', period_months=0) проваливался мимо. Юзер вечно работал на полной. Добавлена 4-я ветка.round(pmk/30), реальное создание ceil(pmk/30). Теперь оба через compute_daily_rate.admin_payers._create_billing_subscription брал datetime.now(UTC) из своего модуля, а не из BillingProcessor. Тест зелёный утром, красный вечером. Фикс: monkeypatch.setattr(admin_payers_mod, 'datetime', FakeDT).При нулевом балансе или истёкшем tariff_expires_at:
auto_throttle_rx = auto_throttle_tx = DEBT_THROTTLE_KBIT (default 128)notify_telegram → бот WhiteBox :5088/notifyconfig.yml → billing.debt_throttle_kbit128 кбит/с зарезервирован для billing throttle. Любой код, сбрасывающий throttle (например,
AggregateTrafficдля traffic-лимитов), должен пропускатьauto_throttle_rx == 128.Инцидент 2026-04-17: AggregateTraffic сбрасывал billing-throttle → BillingProcessor ставил снова + слал уведомление → юзер получал "Скорость ограничена до 128 Кбит/с" каждые 10 минут. Фикс: проверка источника.
/admin/throttled (2026-05-01)API /api/v1/system/throttled-users определяет тип записи billing по совпадению auto_throttle_rx == debt_throttle_kbit (из config.yml). UI показывает отдельной карточкой «Биллинг (долг)» с оранжевым badge'ем — раньше путалось с traffic-throttle и помечалось как «Авто».
Plan.trial_period_days (default 7) — длина пробного. После истечения: throttle 128 кбит/с (как при долге).Plan.duration_days — жёсткий срок promo-тарифа.−10% от price_monthly × 6−20% от price_monthly × 12price_6m / price_12m (0 = авто)Plan.price_for(months) / WbTariff.price_for(months) — единый API расчёта цены.Subscription.is_discounted — UI-флаг.POST /api/v1/payments при period_months > 1:
tariff_expires_at на N × 30 днейbilling_type='manual' (daily не списывается)Продакшен с 2026-04-16. Терминал боевой 1776164207893.
| Параметр | Значение |
|---|---|
| Телефон поддержки | +79609878224 |
| СНО | УСН "Доходы" 6% (Taxation: "usn_income") |
| Боевой терминал | 1776164207893 |
| Demo (откат) | 1776164207875DEMO |
| СБП | Включается в Т-Бизнесе, код менять не нужно |
| YooKassa | Disabled fallback (yukassa.enabled: false) |
Пароли терминалов — в
config.ymlна сервере (/opt/WhiteBox/config.yml), gitignored.
shared/payment/
├── __init__.py # re-export: is_configured, create_payment, handle_webhook, calc_amount
├── core.py # calc_amount, handle_webhook_common
├── provider.py # PaymentProvider(ABC)
├── yookassa.py # disabled fallback
├── tpay.py # TPayProvider + get_state() + poll_pending_payments()
└── factory.py # get_provider() по config, default=tpay
https://securepay.tinkoff.ru/v2/true/false (lowercase)OKorder_id → payer/months: таблица wb_tpay_pending_paymentsadmin/webhooks.py:tpay_webhook)| Статус | Действие |
|---|---|
CONFIRMED |
handle_webhook_common → AWG POST /payments → TG ✅ "Оплата принята" + кнопка "Главное меню" |
REJECTED / CANCELED / AUTH_FAIL / DEADLINE_EXPIRED |
processed_at в БД → TG ❌ "Платёж отклонён" + кнопка |
REFUNDED / REVERSED |
refund-запись (-amount) в AWG → TG 💸 "Возврат" + кнопка |
NEW, AUTHORIZED |
Игнорируются (промежуточные) |
_notify_admins_payment() отправляется всем TG ID из web.admin_telegram_ids:
/payers/{id} (ФИО), /users/{sid} (имя), /plans/{id} (тариф)Daemon-поток _poll_tpay_loop в wb-tgbot каждые 2 мин — проверяет необработанные платежи через GetState API. Подстраховка если webhook не дошёл (например, ТСПУ дропнул).
Подписка {тариф} ({SID[:8]}), {период} — без персональных данныхTax=none, PaymentMethod=full_prepayment, PaymentObject=service/deliverytbank.ru — футер/privacy/returns/delivery/privacy/delivery п.5/privacy-policy п.9/privacy-policy п.10, /returns п.6/privacy + /privacy-policy п.1.2Заменить в /opt/WhiteBox/config.yml payment.tpay.terminal_key и password на DEMO-значения, перезапустить wb-tgbot и wb-admin. Demo-терминал 1776164207875DEMO.
BillingProcessorChangelog