docs: sync wiki with 0.1.0+48 (Subscription)
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
|
||||
Reference in new issue
Block a user