docs: sync wiki with 0.1.0+48 (Subscription)

hermes committed 2026-10-11 12:06:02 +00:00
1 parent 5a50072075
commit fdf3ec619f
1 file changed
+111
+111
@@ -0,0 +1,111 @@
# Подписка
## Модель
**Триал 3 дня → подписка 30 дней за 100 звёзд (Telegram Stars).**
Константы в `user_store.py`:
```python
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 перед сравнением:
```python
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