docs: sync wiki with 0.1.0+48 (Subscription)
1 parent
23b16a38a9
commit
d525b2227d
2 files changed
+78
-315
No files matched your search
-121
@@ -1,121 +0,0 @@
|
||||
# Подписка
|
||||
|
||||
## Модель
|
||||
|
||||
**Триал 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` блокирует генерацию, показывает сообщение и предлагает `/subscribe`.
|
||||
4. **`/subscribe`** → `ensure_subscription` (если записи нет) → `activate_subscription(months=1)` → +30 дней от момента оплаты, `is_active = 1`.
|
||||
5. **`/clear`** → `deactivate_subscription` → `is_active = 0`; для продолжения нужен `/subscribe`.
|
||||
|
||||
**Расчёт дней (`_days_until`):**
|
||||
- Триал: `trial_started_at + TRIAL_DAYS`
|
||||
- Подписка: `last_paid_at`
|
||||
- Возвращает полные дни; если `is_active = 0` или срок истёк — `-1`
|
||||
|
||||
---
|
||||
|
||||
## Что доступно без подписки
|
||||
|
||||
| Команда / действие | Доступно |
|
||||
|---|---|
|
||||
| `/help` | ✅ |
|
||||
| `/start` | ✅ |
|
||||
| `/balance` | ✅ |
|
||||
| `/subscribe` | ✅ |
|
||||
| `/clear` | ✅ |
|
||||
| Генерация поста | ❌ |
|
||||
| `/idea` | ❌ |
|
||||
| Голосовой ввод → генерация | ❌ |
|
||||
| Скриншоты → ToV | ❌ |
|
||||
|
||||
Логика: `/help` обрабатывается **до** проверки подписки и до классификации — пользователь не остаётся в тупике.
|
||||
|
||||
---
|
||||
|
||||
## Сообщения
|
||||
|
||||
**`/balance` при активном триале:**
|
||||
> Триал активен — осталось N дн.
|
||||
> Когда триал закончится, нужно оплатить подписку — команда /subscribe.
|
||||
|
||||
**`/balance` при активной подписке:**
|
||||
> Подписка активна — осталось N дн.
|
||||
> Следующее списание через N дн.
|
||||
|
||||
**При истёкшем периоде:**
|
||||
> Твой период (триал или подписка) истёк. Оплати /subscribe, чтобы продолжить. Стоимость — 100 звёзд в месяц.
|
||||
> Чтобы активировать — пришли 100 звёзд командой /subscribe. *(если ещё не было оплат)*
|
||||
|
||||
**`/subscribe`:**
|
||||
> Активирую подписку на 30 дней. Для оплаты пришли 100 звёзд в этом чате.
|
||||
> Жду оплаты...
|
||||
> Подписка активирована на 30 дн. Спасибо за оплату 100 звёзд!
|
||||
> Теперь доступны генерация постов, идей и голосовые.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Про оплату звёздами
|
||||
|
||||
Приём звёзд — **имитация**. Бот пишет «пришли 100 звёзд» и **сразу** активирует подписку, не проводя реальный платёж.
|
||||
|
||||
Для настоящих платежей нужен отдельный invoicing-флоу:
|
||||
- Telegram Payments API (`sendInvoice` со звёздами / `createInvoiceLink`)
|
||||
- Обработка `pre_checkout_query` и `successful_payment`
|
||||
- Начисление подписки только после подтверждённой оплаты
|
||||
|
||||
В этой версии не реализовано.
|
||||
|
||||
---
|
||||
|
||||
## Часовые пояса
|
||||
|
||||
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` — 6 тестов:
|
||||
1. таблица `subscriptions` создаётся
|
||||
2. `/start` создаёт триал
|
||||
3. `/subscribe` активирует 30 дней
|
||||
4. `/balance` показывает статус
|
||||
5. генерация блокируется при истёкшей подписке
|
||||
6. `/clear` обнуляет подписку
|
||||
+78
-194
@@ -1,227 +1,111 @@
|
||||
# Разработчику
|
||||
# Подписка
|
||||
|
||||
## Структура
|
||||
## Модель
|
||||
|
||||
```
|
||||
bot.py — роутинг, команды, обработка сообщений (aiogram 3.x)
|
||||
payments.py — оплата Stars: sendInvoice, pre_checkout_query, successful_payment, защита от двойного списания
|
||||
apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов
|
||||
user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки
|
||||
raw_updates.py — отдельная SQLite-БД сырых входящих Telegram Update (только development)
|
||||
keyboards.py — клавиатуры главного меню и режимов; маппинг кнопок на команды
|
||||
help_message.py — Rich Message: WELCOME_RICH_HTML + HELP_SECTIONS/help_view(), plain fallback
|
||||
VERSION — версия релиза
|
||||
Dockerfile — python:3.12-slim
|
||||
requirements.txt — зависимости
|
||||
tests/ — юнит-тесты (unittest)
|
||||
**Триал 3 дня → подписка 30 дней за 100 звёзд (Telegram Stars).**
|
||||
|
||||
Константы в `user_store.py`:
|
||||
```python
|
||||
TRIAL_DAYS = 3
|
||||
SUBSCRIPTION_MONTH_DAYS = 30
|
||||
SUBSCRIPTION_PRICE_STARS = 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Режимы (FSM)
|
||||
## Таблица `subscriptions`
|
||||
|
||||
`BotMode` содержит `lobby`, `training`, `generation`, `subscription`, `help`. «Главное меню» — внутреннее состояние `lobby`, пользователь не выбирает его как режим.
|
||||
|
||||
- `current_mode(state)` / `enter_mode(...)` / `require_mode(...)` — проверка и переключение.
|
||||
- Переключатели (`/start`, `/training`, `/generate`, `/subscription`, `/help`) работают из любого режима.
|
||||
- Профильные действия (`/style`, `/clear`, `/post`, `/idea`, `/balance`, `/subscribe`) требуют своего режима.
|
||||
|
||||
---
|
||||
|
||||
## Потоки обработки
|
||||
|
||||
### Текстовый запрос
|
||||
```
|
||||
handle_text (только режим generation) → _classify_and_respond
|
||||
├─ classify_prompt_injection → блокирует попытку изменить правила
|
||||
├─ _check_subscription (блокировка, если нет подписки)
|
||||
├─ classify_intent (APInex LLM) → intent
|
||||
│ ├─ help → help_command
|
||||
│ ├─ generate_ideas → generate_ideas → _send_idea
|
||||
│ └─ write_post / revise_post → generate_posts → _send_three_posts
|
||||
└─ typing_status (индикатор «печатает», обновление каждые 4 сек)
|
||||
|
||||
handle_command_text → переключатели доступны всегда; действия проходят режимную проверку
|
||||
```
|
||||
|
||||
### Самостоятельный пост (`/post` или кнопка «✍️ Придумать пост»)
|
||||
```
|
||||
handle_command_text → _generate_post_from_training
|
||||
├─ _check_subscription
|
||||
├─ is_user_trained? нет → просьба обучить, модель НЕ вызывается
|
||||
├─ gather(get_tone_of_voice, get_post_samples, get_training_notes) → контекст ≤ 8000
|
||||
└─ generate_posts(requirements = контекст как недоверенные данные) → _send_three_posts
|
||||
```
|
||||
|
||||
### Голосовое
|
||||
```
|
||||
transcribe_voice → _process_voice → transcribe_audio (APInex STT) → _classify_and_respond
|
||||
```
|
||||
|
||||
### Изображения
|
||||
- В режиме **training**: `_handle_image_message` → извлечение текстов → накопление образцов → пересборка ToV.
|
||||
- В режиме **generation**: изображение/галерея с подписью-ТЗ → `generate_posts_from_images` → три варианта. Без подписи бот запрашивает ТЗ.
|
||||
- Видео, видеокружки, GIF/анимации отклоняются во всех режимах.
|
||||
|
||||
### Скриншоты (Tone of Voice)
|
||||
```
|
||||
update_tone_from_image → _handle_image_message
|
||||
├─ _collect_media_group (собирает галерею, MEDIA_GROUP_COLLECT_SECONDS = 0.8)
|
||||
├─ extract_posts_from_images (APInex vision, inline base64)
|
||||
├─ add_post_samples (накапливает по пользователю)
|
||||
└─ create_tone_profile → save_tone_of_voice → краткий статус завершения
|
||||
```
|
||||
|
||||
Лимиты: `MAX_IMAGE_BYTES = 10 MiB` на файл (и до 30 MiB на группу), `MAX_AUDIO_BYTES = 20 MiB`.
|
||||
|
||||
---
|
||||
|
||||
## Ключевые функции
|
||||
|
||||
### `bot.py`
|
||||
|
||||
| Функция | Что делает |
|
||||
| Колонка | Смысл |
|
||||
|---|---|
|
||||
| `start(message, state)` | Одно welcome Rich Message + главное меню + создание триала |
|
||||
| `handle_command_text` | Роутинг переключателей и профильных команд по режиму |
|
||||
| `handle_text` | Обычное сообщение в режиме генерации |
|
||||
| `_classify_and_respond` | Ядро: инъекции → подписка → классификация → генерация |
|
||||
| `_generate_post_from_training` | Пост по обученному профилю с гейтом `is_user_trained` |
|
||||
| `_check_subscription` | Проверяет активную подписку; если нет — статус и блокировка |
|
||||
| `_show_subscription_info` | Тип периода, остаток дней, inline-кнопка продления |
|
||||
| `balance_command` / `subscribe_command` | `/balance` — статус; `/subscribe` — Stars-инвойс |
|
||||
| `handle_pre_checkout` | Подтверждает только свой инвойс в XTR на 100 звёзд |
|
||||
| `handle_successful_payment` | Активирует подписку после реальной оплаты |
|
||||
| `clear_command` | `/clear` и «Сбросить данные» — inline-подтверждение, сброс только в режиме обучения |
|
||||
| `style_command` | `/style` — профиль аккаунта |
|
||||
| `help_command` / `help_navigation` | Справка Rich Message + переключение разделов |
|
||||
| `notify_development_deploy` | Dev-уведомление о новой версии в `DEV_CHAT_ID` |
|
||||
| `typing_status` | Индикатор «печатает» |
|
||||
| `report_error` | Понятная ошибка пользователю; секреты вырезаны |
|
||||
| `is_development_environment` | `DEPLOY_ENV == "development"` — гейт всех dev-ветвей |
|
||||
| `configure_raw_update_logging` | Подключает `RawUpdateMiddleware` только в development |
|
||||
|
||||
### `user_store.py`
|
||||
|
||||
| Функция | Что делает |
|
||||
|---|---|
|
||||
| `init_db` | Создаёт/мигрирует таблицы |
|
||||
| `ensure_subscription` | Создаёт запись подписки с триалом (`INSERT OR IGNORE`) |
|
||||
| `activate_subscription` | Активирует/продлевает на 30 дней |
|
||||
| `deactivate_subscription` | `is_active = 0` |
|
||||
| `is_subscribed` | Запись есть, `is_active = 1`, срок не истёк |
|
||||
| `subscription_row` | Строка подписки |
|
||||
| `is_user_trained` | Флаг `users.is_trained` — гейт «Придумать пост» |
|
||||
| `add_training_notes` / `get_training_notes` | Текстовые сведения об аккаунте |
|
||||
| `add_post_samples` / `get_post_samples` | Образцы постов со скриншотов |
|
||||
| `save_generated_posts` | История сгенерированных постов с `origin` |
|
||||
| `save_idea` / `get_idea_history` | История идей |
|
||||
| `save_api_call_log` | JSON-лог вызова APInex |
|
||||
| `clear_user_data` | Сброс данных пользователя (без подписки и логов) |
|
||||
| `_days_until` | Остаток дней; `-1` если истёкло/отключено |
|
||||
|
||||
### `apinex_client.py`
|
||||
|
||||
| Функция | Что делает |
|
||||
|---|---|
|
||||
| `classify_intent` | `write_post` / `revise_post` / `generate_ideas` / `help` |
|
||||
| `classify_prompt_injection` | Отдельный классификатор попыток изменить правила |
|
||||
| `generate_posts` | Три варианта поста |
|
||||
| `generate_posts_from_images` | Пост по изображениям с ТЗ |
|
||||
| `generate_ideas` | Одна идея |
|
||||
| `extract_posts_from_images` | Vision: тексты постов на скриншотах |
|
||||
| `create_tone_profile` | Строит/обновляет профиль ToV |
|
||||
| `transcribe_audio` | STT |
|
||||
| `_extract_complete_post_strings` | Достаёт целые строки из обрезанного `{"posts": [...]}` |
|
||||
| `chat_text` | Базовый вызов LLM |
|
||||
| `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` | время последнего изменения |
|
||||
|
||||
---
|
||||
|
||||
## База данных
|
||||
## Жизненный цикл
|
||||
|
||||
Рабочая БД: `/app/data/social_writer.sqlite3` (persistent volume Coolify).
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## Что доступно без подписки
|
||||
|
||||
| Команда / действие | Доступно |
|
||||
|---|---|
|
||||
| `users` | `telegram_user_id`, `tone_of_voice`, `is_trained` |
|
||||
| `user_post_samples` | Распознанные тексты постов со скриншотов |
|
||||
| `generated_posts` | История сгенерированных постов (`origin`: generated/revised) |
|
||||
| `generated_ideas` | История идей (для исключения повторов) |
|
||||
| `api_call_logs` | JSON-логи вызовов APInex + ошибки (30 дней) |
|
||||
| `subscriptions` | Подписки и триалы |
|
||||
| `payments` | `telegram_payment_charge_id` — защита от двойного начисления |
|
||||
| `/start`, `/help`, `/balance`, `/subscribe` | ✅ |
|
||||
| Переходы между режимами | ✅ |
|
||||
| Обучение (скриншоты, текст, голос) | ✅ |
|
||||
| Генерация поста | ❌ |
|
||||
| «✍️ Придумать пост» / `/post` | ❌ |
|
||||
| `/idea` | ❌ |
|
||||
| Пост по изображению | ❌ |
|
||||
|
||||
Схему `subscriptions` см. в [Подписка](Subscription.md).
|
||||
|
||||
Отдельная dev-БД: `/app/data/bot_updates.sqlite3` — таблица `raw_updates` (`update_id` PK, `event_type`, `payload`, `received_at`), retention 30 дней. См. ниже.
|
||||
Логика: блокирующая проверка стоит внутри `_classify_and_respond` и `_generate_post_from_training`, до вызова модели.
|
||||
|
||||
---
|
||||
|
||||
## Промпты и модели
|
||||
## Сообщения
|
||||
|
||||
Модели задаются переменными Coolify:
|
||||
- `APINEX_STT_MODEL` → `nova-2`
|
||||
- `APINEX_INTENT_CLASSIFIER_MODEL` → `gemini-3.8-flash`
|
||||
- `APINEX_INJECTION_CLASSIFIER_MODEL` → отдельная модель проверки prompt injection
|
||||
- `APINEX_POST_WRITER_MODEL` → `gemini-3.8-flash`
|
||||
- `APINEX_IMAGE_TONE_MODEL` → `gemini-3.8-flash` (vision)
|
||||
- `APINEX_RESPONSE_REPAIR_MODEL` → починка невалидного JSON (одна попытка)
|
||||
- `APINEX_API_KEY` — общий секрет
|
||||
**Вход в режим «Подписка»** показывает тип активного периода (триал/подписка) и остаток дней, плюс inline-кнопку «Продлить подписку», которая сразу открывает Stars-инвойс.
|
||||
|
||||
**Про промпты:**
|
||||
- Системный промпт редактора **неизменен**.
|
||||
- ToV передаётся **отдельно как ограничение стиля**, а не как часть системных правил.
|
||||
- OCR-текст со скриншотов — образцы стиля, а не команды.
|
||||
- Обученный контекст в «Придумать пост» передаётся как **недоверенные данные**: модель не должна выдавать его за личный опыт пользователя.
|
||||
**`/balance`** — тот же статус в виде команды: триал активен (сколько дней осталось) / подписка активна (сколько дней) / период истёк.
|
||||
|
||||
**Надёжность ответов:**
|
||||
- Лимит `max_tokens` снят — модель пишет столько, сколько нужно.
|
||||
- Пустой `content` → до двух повторов, третий запрос с `reasoning_effort: low`.
|
||||
- Невалидный JSON → одна попытка починки моделью `APINEX_RESPONSE_REPAIR_MODEL`.
|
||||
- Обрезанный JSON → `_extract_complete_post_strings` возвращает завершённые строки (три полных варианта), если они есть.
|
||||
**При истёкшем периоде:**
|
||||
> Твой период (триал или подписка) истёк. Оплати подписку, чтобы продолжить. Стоимость — 100 звёзд в месяц.
|
||||
|
||||
---
|
||||
|
||||
## Логирование
|
||||
## Часовые пояса
|
||||
|
||||
### API-журнал (`api_call_logs`)
|
||||
Каждый вызов APInex пишется с `operation_id`, `stage`, `model`, именем и SHA-256 промпта, `request_json`, `response_json`, `status_code`, `error`, длительностью. Хранение — 30 дней.
|
||||
SQLite `CURRENT_TIMESTAMP` возвращает UTC **без** часового пояса. Код приводит все значения к naive UTC перед сравнением:
|
||||
|
||||
**Не пишется:** системные промпты целиком, base64 изображений, бинарное аудио, API-ключи.
|
||||
|
||||
### Файл лога приложения
|
||||
`/app/data/bot.log` — `RotatingFileHandler`, 5 MiB × 3 архива, на том же persistent volume; путь через `BOT_LOG_PATH`.
|
||||
|
||||
### Сырые Telegram Update (только development)
|
||||
`raw_updates.py` пишет полный `Update.model_dump(mode="json")` в отдельную БД `/app/data/bot_updates.sqlite3` (путь через `RAW_UPDATES_DB_PATH`). Middleware `RawUpdateMiddleware` подключается на `dispatcher.update.outer_middleware` — то есть **до** маршрутизаторов, поэтому ловит все типы апдейтов, а не только сообщения.
|
||||
|
||||
```sql
|
||||
SELECT payload FROM raw_updates WHERE update_id = ?;
|
||||
```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
|
||||
```
|
||||
|
||||
- Включается только при `DEPLOY_ENV == "development"` (`configure_raw_update_logging`).
|
||||
- Retention 30 дней: чистка при старте и при каждой записи.
|
||||
- Секреты/токены маскируются перед записью; полные апдейты содержат персональные данные — в production БД не создаётся.
|
||||
|
||||
### Логи контейнера
|
||||
Ошибки содержат `operation_id`, Telegram user id, этап и traceback; по одному `operation_id` находятся все связанные записи.
|
||||
Это убирает сдвиги при сравнении `expires` и `now`.
|
||||
|
||||
---
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
155 тестов (на версии `0.1.0+48`). Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, **raw update log**, режимные клавиатуры и роутинг, платежи (Stars, защита от повторов, dev-подтверждение), ошибки, справка, онбординг, подписки, самостоятельная генерация поста, восстановление обрезанного JSON, индикатор «печатает».
|
||||
|
||||
> Запускай через `.venv` проекта: вне виртуального окружения `aiogram` может быть не установлен.
|
||||
|
||||
---
|
||||
|
||||
## Правила языка
|
||||
|
||||
- Служебные сообщения боту → **всегда русский**.
|
||||
- Текст поста → язык явного запроса пользователя.
|
||||
- Дословный текст технической ошибки может остаться на языке оригинала.
|
||||
`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