Clone
3
Subscription
Hermes Agent edited this page 2026-10-11 12:06:02 +00:00

Подписка

Модель

Триал 3 дня → подписка 30 дней за 100 звёзд (Telegram Stars).

Константы в user_store.py:

TRIAL_DAYS = 3
SUBSCRIPTION_MONTH_DAYS = 30
SUBSCRIPTION_PRICE_STARS = 100

Таблица subscriptions

Колонка Смысл
telegram_user_id PK, id пользователя
trial_started_at момент создания (SQLite CURRENT_TIMESTAMP, UTC-нейтральный)
last_paid_at момент/конец оплаченного периода; NULL = ещё триал
is_active 1 — активна, 0 — отключена
monthly_days длительность оплаченного периода (30)
updated_at время последнего изменения

Жизненный цикл

  1. /start → ensure_subscription → триал 3 дня, is_active = 1.
  2. Генерация постов и идей доступна, пока _days_until > 0.
  3. Триал/подписка кончилась → _check_subscription блокирует генерацию, показывает статус и предлагает оплатить.
  4. /subscribe → sendInvoice (валюта XTR, 100 звёзд) → pre_checkout_query → successful_payment → activate_subscription(months=1) → +30 дней от момента оплаты, is_active = 1.
  5. /clear очищает данные обучения и историю; подписка и триал не меняются.

Расчёт дней (_days_until):

  • Триал: trial_started_at + TRIAL_DAYS
  • Подписка: last_paid_at
  • Возвращает полные дни; если is_active = 0 или срок истёк — -1

Реальная оплата Stars

Оплата настоящая — через Telegram Stars (payments.py):

  • /subscribe и кнопка «Продлить подписку» выставляют инвойс (sendInvoice, валюта XTR, provider_token пустой — цифровой товар).
  • pre_checkout_query подтверждает только свой инвойс на 100 звёзд в XTR, остальное отклоняет.
  • Подписка активируется только в successful_payment — сам /subscribe её не продлевает.
  • telegram_payment_charge_id каждого платежа пишется в таблицу payments, поэтому один платёж не может активировать подписку дважды; по этому же id делается возврат через refundStarPayment.

Dev-подтверждение

При точном DEPLOY_ENV=development после обычного сообщения об инвойсе отдельным сообщением показывается кнопка «🧪 Оплатил (dev)». Она включает доступ на 30 дней без реального платежа и доступна только пользователю, которому выставлен счёт. В production кнопка и callback не выдают доступ (fail-closed).


Что доступно без подписки

Команда / действие Доступно
/start, /help, /balance, /subscribe ✅
Переходы между режимами ✅
Обучение (скриншоты, текст, голос) ✅
Генерация поста ❌
«✍️ Придумать пост» / /post ❌
/idea ❌
Пост по изображению ❌

Логика: блокирующая проверка стоит внутри _classify_and_respond и _generate_post_from_training, до вызова модели.


Сообщения

Вход в режим «Подписка» показывает тип активного периода (триал/подписка) и остаток дней, плюс inline-кнопку «Продлить подписку», которая сразу открывает Stars-инвойс.

/balance — тот же статус в виде команды: триал активен (сколько дней осталось) / подписка активна (сколько дней) / период истёк.

При истёкшем периоде:

Твой период (триал или подписка) истёк. Оплати подписку, чтобы продолжить. Стоимость — 100 звёзд в месяц.


Часовые пояса

SQLite CURRENT_TIMESTAMP возвращает UTC без часового пояса. Код приводит все значения к naive UTC перед сравнением:

def utc(value: str) -> datetime.datetime:
    dt = datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
    if dt.tzinfo is not None:
        return dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
    return dt

Это убирает сдвиги при сравнении expires и now.


Тесты

SubscriptionTests в tests/test_writer_features.py:

  1. таблица subscriptions создаётся
  2. /start создаёт триал
  3. оплата активирует 30 дней (successful_payment)
  4. /balance показывает статус
  5. генерация блокируется при истёкшей подписке
  6. отмена продления не активирует подписку
  7. /clear сохраняет подписку и триал
  8. dev-подтверждение оплаты работает только в development