Биллинг и тарифы¶
Полный исходный дизайн-документ (Smart Billing Phase 2) архивирован в history/smart_billing.md — он описан там как план, но полностью реализован, эта страница описывает текущее состояние.
Роли/тарифы¶
Иерархия: SIMPLE (0) → VIP (1) → PRO (2) → PERS (3), плюс административная
роль GLAVA вне тарифной лестницы (персонал платформы, не продаётся).
Сравнение ролей везде в коде по одному и тому же списку ROLE_ORDER =
["SIMPLE","VIP","PRO","PERS","GLAVA"], дублированному (не вынесено в общий
модуль) в каждом контроллере, который что-то гейтит по тарифу — см. ниже.
Функциональные различия между тарифами¶
| Возможность | SIMPLE (0₽) | VIP (299₽/30д) | PRO (499₽/30д) | PERS (799₽/30д) |
|---|---|---|---|---|
| Веб-туннель (HTTP/HTTPS), профиль/визитка | ✅ | ✅ | ✅ | ✅ |
| Заявка в публичный каталог | ✅ | ✅ | ✅ | ✅ |
| SSH-туннель (TCP 22) | ❌ | ✅ | ✅ | ✅ |
| Раздел «Безопасность» — инциденты атак на свой туннель + email-уведомления | ❌ | ✅ | ✅ | ✅ |
| RDP, VNC-туннель | ❌ | ❌ | ✅ | ✅ |
| Аналитика трафика (посещения, UTM, гео, экспорт в Excel) | ❌ | ❌ | ✅ | ✅ |
| PostgreSQL, MySQL, Redis, MongoDB-туннель | ❌ | ❌ | ❌ | ✅ |
| Дополнительные слаги (мультислаговость) | ❌ | ❌ | до 2 | ∞ |
Источники истины для этой таблицы (если меняется тариф — обновить и код, и эту таблицу):
- TCP-порты —
TcpPortController.SYSTEM_PORTS(minRoleна каждый порт) +dixu_proxy/README.md. - Безопасность/инциденты —
IncidentsController.MIN_ROLE,InternalController.ATTACK_NOTIFY_MIN_ROLE(оба"VIP"). - Аналитика —
AnalyticsController.MIN_ROLE("PRO"). - Цены/описания тарифов —
BillingController.TARIFFS.
Ручное управление ролью (администратор)¶
Обычный путь — оплата (user_tariffs, см. ниже). Но администратору (email
из ADMIN env, либо пользователь с ролью GLAVA) доступен прямой оверрайд:
UI: /admin → вкладка «Туннели» → выпадающий список роли в строке
клиента → меняет немедленно (onchange="setRole(this)" →
POST /api/admin/users/{id}/role, TunnelController.setRole).
Важный нюанс: это пишет users.role напрямую, минуя user_tariffs.
Если у пользователя есть активный ОПЛАЧЕННЫЙ тариф в user_tariffs, то при
его естественном истечении TariffService.checkExpiredAndActivateNext
вызовет syncUserRole, который перезапишет users.role обратно на то,
что реально активно в user_tariffs (или на SIMPLE, если ничего не
осталось) — то есть ручной апгрейд для уже платящего клиента не "продлевает"
его тариф, а будет молча отменён следующим биллинговым циклом. Ручной
оверрайд предсказуемо держится только для пользователей без активных записей
в user_tariffs (бесплатные аккаунты, тестовые/служебные).
Модель "умного биллинга" (реализована)¶
Вместо простой даты истечения — счётчик оставшихся дней (user_tariffs.remaining_days).
Пользователь может держать несколько тарифов одновременно: один активный,
остальные заморожены, переключение мгновенное.
- Списание: 1 день в сутки у каждого тарифа, который был активен хотя бы
раз за текущие сутки — независимо от того, сколько раз переключались между
тарифами в течение дня (lazy-deduction через
last_deducted_date). - Переключение (
POST /api/billing/switch): без ограничений по количеству в день; даунгрейд на тариф с более низким tier заблокирован, пока активен более высокий. - Автопереключение при истечении (
SmartBillingScheduler, ежедневно 00:05): приremaining_days = 0у активного тарифа ищет следующий по приоритету замороженный (PERS(3) > PRO(2) > VIP(1)), активирует его, шлёт email; если замороженных нет — откат наSIMPLE. - Заморозка туннеля: пока
tunnel_frozen_by != 'none', счётчики всех тарифов пользователя не уменьшаются. - Платёжные провайдеры:
- Т-Банк / Тинькофф (
TinkoffService) — для российских пользователей, пополнение баланса и списание с него. Эндпоинты:/api/billing/pay,/topup,/notify,/payment-status/{id},/history. - Lemon Squeezy (
LemonSqueezyService) — для иностранных пользователей, subscription-модель, прямая покупка дней без баланса. Эндпоинты:/api/billing/init-ls,/api/billing/notify-ls. Подробный гайд по настройке: guides/lemon-squeezy.md.
Схема БД¶
user_tariffs (миграция V28, payment_ref вместо payment_id из исходного
плана) — role, remaining_days, status (active/frozen/expired),
last_deducted_date. Поля role/role_expires_at в users — кэш текущего
активного тарифа для быстрого доступа, обновляются при каждом переключении.
Приватный режим (private_mode)¶
Отдельная от ролей ось аккаунта (комбинируется с лестницей ролей, не
параллельная лестница): пользователь один раз — при первом входе, на
экране /auth/account-type-setup — выбирает публичный или приватный
аккаунт. Выбор необратим (для смены — новый аккаунт), хранится в
users.private_mode (NULL = выбор не сделан, перехватывается редиректом
из AuthController.showProtectedPage).
- Публичный (как было раньше) — визитка, каталог, авторские короткие ссылки, отзывы с весом в рейтинге.
- Приватный — ничего из вышеперечисленного (см.
docs/SECURITY.md, access-gate); туннель доступен только владельцу через его сессию dix.su. На бесплатной роли SIMPLE — пробный период 14 дней (users.private_trial_expires_at), по истечении без перехода на платную роль (VIP+) — заморозка туннеля (tunnel_frozen_by='system'), проверяется ежедневноSmartBillingScheduler.freezeExpiredPrivateTrials.
Верификация перед выдачей приватного режима — одноразовый платёж 1 ₽
через Тинькофф (POST /api/billing/verify-private, тот же паттерн, что
/topup, tariff_id='private_verify') + самостоятельно введённое ФИО
(банковский Init/Confirm API не отдаёт держателя карты мерчанту — ФИО
собирается на форме как подстраховка, хранится в private_verifications).
Подтверждение оплаты в /notify устанавливает private_mode=true и
private_trial_expires_at=NOW()+14 дней.
Приглашённые пользователи (PRO и выше) — владелец приватного аккаунта на
тарифе PRO или выше может пригласить других зарегистрированных пользователей
dix.su по их slug'у; приглашённые проходят access-gate так же, как и
владелец (см. docs/SECURITY.md). Лимиты по роли владельца
(PrivateInviteController.MAX_INVITES): PRO — до 10, PERS — до 100.
Лимит проверяется на стороне modulauth при добавлении
(POST /api/private/invites); сам факт допуска на туннеле проверяет
dixu_proxy (таблица private_tunnel_invites, миграция V38) — при понижении
роли владельца ниже PRO приглашённые теряют доступ немедленно, без удаления
самих записей о приглашении.
Сквозное шифрование (Фаза 2, серверная часть реализована, заблокирована
внешней зависимостью device_client) — отдельный вход
e2e-<token>.{BASE_DOMAIN} без терминации TLS на сервере (POST /api/private/e2e/mint,
PrivateE2eController). Та же лимитная база, что и выше — токен на
приглашённого выдаётся только если он уже есть в private_tunnel_invites,
второй системы лимитов нет. Подробности механизма и статус блокирующей
зависимости — docs/SECURITY.md, раздел «Фаза 2: сквозное шифрование».
Чеки НПД («Мой налог»)¶
Актуально, только пока платформа работает как Самозанятый с НПД. При переходе на ИП/ООО этот модуль отключается — чеки уходят через кассу с ОФД (отдельная интеграция).
Включение/отключение¶
Управляется одной переменной в .env:
BUSINESS_FORM=npd # включён НПД-модуль
BUSINESS_FORM= # выключен (ИП, ООО, или ещё не определена ОПФ)
Если BUSINESS_FORM не равен npd, MoyNalogService.tryCheckForPayment() возвращает
ok=false немедленно — никаких HTTP-запросов к lknpd.nalog.ru не происходит.
Дополнительный рубильник внутри: npd_settings.is_active = false в БД также отключает
формирование чеков, даже если BUSINESS_FORM=npd. Таким образом:
BUSINESS_FORM |
is_active в БД |
Чеки |
|---|---|---|
npd |
true |
✅ формируются |
npd |
false |
❌ не формируются |
пусто / ip / ooo |
любое | ❌ не формируются |
Когда создаётся чек¶
Автоматически — в BillingController.notify() после получения вебхука от Т-Банка
со статусом CONFIRMED:
tariff_id = 'topup'— пополнение баланса (деньги реально пришли от пользователя)tariff_id = 'private_verify'— разовый платёж 1 ₽ за верификацию приватного режима
Внутренние списания с баланса (POST /api/billing/pay) чека не создают — деньги
уже были получены при topup.
device_id и параллельное использование на нескольких сервисах¶
refresh_token в «Мой налог» — общий для всего аккаунта самозанятого, не привязан к
конкретному устройству. device_id — просто UUID, которым клиент «представляется» при
обмене токенов. Один refresh_token можно использовать одновременно с разными device_id
на разных сервисах (Django-магазин, dix.su, мобильное приложение и т.д.) — они не
аннулируют сессии друг друга. ФНС не накладывает ограничений на количество активных
device_id.
Практически это означает:
- Авторизация на dix.su через SMS не выбивает сессию в Django-магазине или приложении
- Можно скопировать refresh_token из любого другого сервиса и вставить его сюда (раздел
«Уже есть refresh_token?» ниже) — оба сервиса будут работать с разными device_id
Первоначальная настройка (подключение к «Мой налог»)¶
Все операции делаются в браузере: /admin → вкладка «НПД / Мой налог».
Вариант А — SMS-авторизация (если токена ещё нет):
1. Ввести телефон, привязанный к аккаунту lknpd.nalog.ru
2. Нажать «Запросить SMS-код» → придёт SMS от «Мой налог»
3. Ввести код → «Подтвердить»
4. refresh_token и ИНН сохраняются в npd_settings автоматически
Вариант Б — вставить готовый токен (если уже авторизован на другом сервисе):
1. Найти refresh_token в БД другого сервиса:
- Django: SELECT npd_refresh_token FROM pages_mainpage LIMIT 1;
- Или любой другой источник, где уже получен токен
2. Скопировать токен и вставить в поле «Уже есть refresh_token? Вставить вручную»
3. Указать ИНН (если не заполнен) → «Сохранить токен»
4. Сгенерировать новый device_id в блоке настроек → «Сохранить настройки»
Вставка токена вручную не делает SMS-запрос и не затрагивает другие сервисы, использующие тот же токен.
Шаг 2 — Настройки:
- НПД активен — чекбокс, без него чеки не создаются даже при наличии токена
- ИНН самозанятого — заполняется автоматически после SMS-авторизации, проверить
- Device ID — нажать «Сгенерировать» (UUID, привязывает сессию к "устройству")
- Тип покупателя — FROM_INDIVIDUAL (физ. лицо) или FROM_LEGAL_ENTITY (ИП/ЮЛ)
- Тип услуги — service (услуга) или commodity (товар) — для дix.su: service
- Email для копии — опционально, дублирует чек на этот адрес
Шаг 3 — Сохранить настройки → нажать «Сохранить настройки».
Шаг 4 — Включить в .env:
ssh -p 1022 root@217.114.43.49 "/root/dixsu/rebuild_all.sh modulauth"
Что видит пользователь¶
После оплаты: - Email «Чек НПД (Мой налог) — dix.su» со ссылкой на чек - В разделе Тарифы → История платежей — столбец «Чек» с кликабельной ссылкой
Хранение¶
npd_settings(таблица) — реквизиты самозанятого,refresh_token, флаги (V42)payments.npd_check_uuid,payments.npd_check_url— UUID и URL каждого созданного чекаrefresh_tokenне передаётся фронту даже в ответ на/api/admin/npd/settings(только флагhas_refresh_token: true/false)
Смена ОПФ (переход на ИП/ООО)¶
- Убрать
BUSINESS_FORM=npdиз.env(или поставитьBUSINESS_FORM=ip) - Задеплоить
modulauth - NPD-модуль выключается без удаления данных — история чеков сохраняется
- Подключить кассу с ОФД (Атол, Эвотор, МодульКасса и т.д.) — отдельная интеграция, не входит в текущую кодовую базу
Где смотреть код¶
modulauth/.../billing/BillingController.java— все HTTP-эндпоинты.modulauth/.../billing/TariffService.java— lazy-deduction, syncusers.role.modulauth/.../billing/SmartBillingScheduler.java— ежедневный автопереключатель (@Scheduled).modulauth/.../billing/TinkoffService.java— интеграция с Т-Банком (RU).modulauth/.../billing/LemonSqueezyService.java— интеграция с LS (international).modulauth/.../billing/MoyNalogService.java— клиент lknpd.nalog.ru (SMS-авт., refresh→access токен, создание чека, guard поBUSINESS_FORM).modulauth/.../billing/NpdAdminController.java— API настроек НПД, только для${ADMIN}(/api/admin/npd/*).