Подписка
Модель
Триал 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 |
время последнего изменения |
Жизненный цикл
/start→ensure_subscription→ триал 3 дня,is_active = 1.- Генерация постов и идей доступна, пока
_days_until > 0. - Триал/подписка кончилась →
_check_subscriptionблокирует генерацию, показывает статус и предлагает оплатить. /subscribe→sendInvoice(валютаXTR, 100 звёзд) →pre_checkout_query→successful_payment→activate_subscription(months=1)→ +30 дней от момента оплаты,is_active = 1./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:
- таблица
subscriptionsсоздаётся /startсоздаёт триал- оплата активирует 30 дней (
successful_payment) /balanceпоказывает статус- генерация блокируется при истёкшей подписке
- отмена продления не активирует подписку
/clearсохраняет подписку и триал- dev-подтверждение оплаты работает только в development