АРХИВ — реализовано полностью. Этот документ описывает дизайн "Фазы 2" как план, но всё описанное здесь уже реализовано (миграция V28,
user_tariffs, lazy-deduction,/api/billing/switch,SmartBillingScheduler). Текущее описание модели — ../BILLING.md. Оставлено как исторический референс дизайна.
Smart Billing — Фаза 2: Умное переключение тарифов¶
Концепция¶
Вместо даты истечения — счётчик оставшихся дней использования. Пользователь может держать несколько тарифов одновременно: один активный, остальные заморожены. Переключение между ними происходит мгновенно через диалог с предупреждением.
Механика списания дней¶
- День списывается у каждого тарифа, который был активен в течение текущих суток
- Если в течение дня переключался между тарифами — у всех задействованных списывается ровно 1 день (не больше, сколько бы раз ни переключался)
- Пример: с 09:00 по 09:05 был PERS, с 09:05 до конца дня VIP → оба потеряли по 1 дню
- Если тариф не был активен сегодня — его счётчик не уменьшается
Реализация¶
Поле last_deducted_date DATE в user_tariffs.
Lazy-deduction: при каждом обращении к биллингу проверяем last_deducted_date < TODAY → списываем 1 день.
Переключение тарифов¶
Диалог при переключении¶
При нажатии "Переключиться" → модальный диалог: - Если новый тариф сегодня ещё не использовался: "Тариф X будет активирован. Текущий тариф Y заморозится." - Если новый тариф уже использовался сегодня: "Внимание: тариф X уже использовался сегодня и потеряет ещё 1 день (итого 2 дня за сегодня не спишется — только 1). Продолжить?" - Если переключение внутри одного дня (оба тарифа уже задействованы сегодня): "Переключение бесплатное — дополнительных дней не спишется ни с одного тарифа."
Правила¶
- Нет ограничений на количество переключений в день
- За один день суммарно с каждого тарифа списывается максимум 1 день
last_deducted_dateпроставляется при активации тарифа (если не равен сегодня)
Автопереключение при истечении¶
Когда remaining_days = 0 у активного тарифа:
1. Ищем следующий по приоритету замороженный тариф (высший tier в первую очередь)
2. Активируем его
3. Отправляем email-уведомление: "Ваш тариф X закончился. Активирован Y (осталось N дней)."
4. Если замороженных тарифов нет — переключаем на SIMPLE
Реализация: Spring @Scheduled(cron = "0 5 0 * * *") — раз в сутки в 00:05.
Интеграция с заморозкой туннеля¶
Когда туннель заморожен (tunnel_frozen_by != 'none'):
- Счётчик всех тарифов пользователя не уменьшается
- Lazy-deduction проверяет user.isTunnelFrozen() → пропускает списание
- Размораживание туннеля не даёт "обратно" дни — просто возобновляет счётчик
Схема БД (новая таблица)¶
CREATE TABLE user_tariffs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
role VARCHAR(20) NOT NULL, -- VIP / PRO / PERS
remaining_days INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'frozen', -- active / frozen / expired
last_deducted_date DATE,
purchased_at TIMESTAMP NOT NULL DEFAULT NOW(),
activated_at TIMESTAMP,
payment_id BIGINT REFERENCES payments(id)
);
CREATE INDEX idx_user_tariffs_user ON user_tariffs(user_id, status);
Поля role и role_expires_at в таблице users остаются как кэш текущего активного тарифа для быстрого доступа. Обновляются при каждом переключении.
Приоритет тарифов¶
При автопереключении: активируем замороженный тариф с наибольшим tier.
При равном tier: активируем тот, у которого больше remaining_days.
API (новые endpoints)¶
GET /api/billing/tariffs — возвращает все тарифы пользователя (active + frozen) с remaining_days
POST /api/billing/switch — { fromTariffId, toTariffId } — переключение с диалогом
POST /api/billing/pay — покупка добавляет в пул (frozen), не активирует автоматически
(или активирует если нет активного тарифа)
GET /api/billing/my-tariffs — список всех купленных тарифов пользователя с остатками
Email уведомления¶
Отправлять на user.email:
- При автопереключении (тариф закончился, активирован следующий)
- За 3 дня до истечения активного тарифа (если нет замороженных)
- При подтверждении пополнения баланса (уже есть — topup webhook)
Оценка трудозатрат¶
| Задача | Оценка |
|---|---|
| DB миграция + user_tariffs таблица | 3 ч |
| Рефакторинг pay() → добавление в пул | 2 ч |
| Lazy deduction logic + tunnel check | 3 ч |
| POST /billing/switch + диалог фронт | 4 ч |
| @Scheduled автопереключение | 2 ч |
| Email уведомления | 2 ч |
| Рефакторинг tariffs() + my-tariffs | 2 ч |
| Итого | ~18 ч |
Текущий статус (Фаза 1 — сделано)¶
- ✅ Продление того же тарифа накапливает дни (
expires_at + 30dвместоnow() + 30d) - ✅ Даунгрейд заблокирован пока активен более высокий тариф
- ✅ Кнопка показывает "🔒 Активен PERS до [дата]" для недоступных тарифов
- ✅ Кнопка продления показывает "Продлить (+30 дней)"