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

АРХИВ — реализовано полностью. Этот документ описывает дизайн "Фазы 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 остаются как кэш текущего активного тарифа для быстрого доступа. Обновляются при каждом переключении.


Приоритет тарифов

PERS (3) > PRO (2) > VIP (1) > SIMPLE (0)

При автопереключении: активируем замороженный тариф с наибольшим 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 дней)"