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

Биллинг и тарифы

Полный исходный дизайн-документ (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:

BUSINESS_FORM=npd
Затем деплой: 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)

Смена ОПФ (переход на ИП/ООО)

  1. Убрать BUSINESS_FORM=npd из .env (или поставить BUSINESS_FORM=ip)
  2. Задеплоить modulauth
  3. NPD-модуль выключается без удаления данных — история чеков сохраняется
  4. Подключить кассу с ОФД (Атол, Эвотор, МодульКасса и т.д.) — отдельная интеграция, не входит в текущую кодовую базу

Где смотреть код

  • modulauth/.../billing/BillingController.java — все HTTP-эндпоинты.
  • modulauth/.../billing/TariffService.java — lazy-deduction, sync users.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/*).