diff --git a/IDEA.MD b/IDEA.MD index be901bb..616816e 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -21,7 +21,7 @@ Telegram-бот, который по запросу пользователя г - **Главное меню** — внутреннее состояние после `/start` или выхода из режима; пользователь не выбирает его как отдельный режим. - **Обучение** — принимает скриншоты, накапливает образцы, показывает стиль (`/style`); `/clear` очищает профиль и историю, не затрагивая подписку или триал. - **Генерация** — создаёт и редактирует посты, предлагает идеи (`/post`, `/idea`); генерация требует активного доступа. -- **Подписка** — при входе показывает активный период и остаток дней; inline-кнопки продления и оплаты ведут к Stars-инвойсу (`/balance`, `/subscribe`). +- **Подписка** — при входе показывает активный период и остаток дней; inline-кнопка продления сразу открывает Stars-инвойс (`/balance`, `/subscribe`). - **Помощь** — справка и навигация по её разделам (`/help`). `/start` показывает ровно одно welcome-сообщение без автоматического вызова помощи. При входе в любой режим показывается короткий onboarding. Главное меню содержит только четыре пользовательских режима; внутри каждого режима клавиатура состоит из кнопки «Главное меню». Команды `/training`, `/generate`, `/subscription`, `/help` доступны из любого режима; профильные действия — только в своём режиме. @@ -48,7 +48,7 @@ Telegram-бот, который по запросу пользователя г - Таблица `subscriptions`: `telegram_user_id`, `trial_started_at`, `last_paid_at`, `is_active`, `monthly_days`, `updated_at`. - `/start` создаёт запись триала (`ensure_subscription`), если её нет. - `/subscribe` отправляет инвойс на 100 звёзд; подписка продлевается только обработчиком `successful_payment` после подтверждённой оплаты. -- Вход в «Подписка» сразу показывает активный тип периода и сколько дней осталось; inline-кнопка «Продлить подписку» открывает сообщение для покупки. +- Вход в «Подписка» сразу показывает активный тип периода и сколько дней осталось; inline-кнопка «Продлить подписку» сразу открывает Stars-инвойс. - `/balance` показывает статус: триал активен (сколько дней осталось) / подписка активна (сколько дней) / период истёк. - Генерация поста и идеи блокируется, если подписка не активна (`_check_subscription`); переходы между режимами доступны всегда. - `/balance` и `/subscribe` доступны в режиме «Подписка»; `/clear` доступна только в режиме «Обучение». diff --git a/README.md b/README.md index 4ef0f88..4b4053b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Telegram-бот для генерации постов для соцсетей. «Главное меню» — внутренняя точка входа после `/start` и выхода из режима, а не отдельный режим. Внутри любого режима reply-клавиатура содержит только «🏠 Главное меню». При входе показывается короткий onboarding. В «Подписке» сразу отображаются тип активного периода и оставшиеся дни; inline-кнопка -«Продлить подписку» открывает предложение, а кнопка оплаты создаёт Stars-инвойс. +«Продлить подписку» сразу открывает Stars-инвойс на оплату. Команды перехода: `/start`, `/training`, `/generate`, `/subscription`, `/help`; `/start` возвращает в главное меню. Команды `/style` и `/clear` доступны только в обучении; `/post` и `/idea` — в генерации; @@ -65,7 +65,7 @@ onboarding. В «Подписке» сразу отображаются тип | Придумать идею | В режиме «Генерация» пишет «предложи тему» или `/idea` | Одна идея одним сообщением | | Настроить стиль | В режиме «Обучение» присылает скриншоты постов | Бот распознаёт тексты, накапливает их и обновляет ToV | | Голосом | В режиме «Генерация» отправляет голосовое | STT расшифровывает → обычный текстовый запрос | -| Узнать статус / продлить | Входит в режим «Подписка», затем нажимает «Продлить подписку» и «Оплатить» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд | +| Узнать статус / продлить | Входит в режим «Подписка» и нажимает «Продлить подписку» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд | | Очистить данные | В режиме «Обучение» вызывает `/clear` | Очищает стиль и историю, не меняя подписку или триал | ### Индикаторы и обратная связь diff --git a/VERSION b/VERSION index e73944a..f5ec1b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+30 +0.1.0+31 diff --git a/bot-dialog-flow.md b/bot-dialog-flow.md index 84d1ab6..d9d35f4 100644 --- a/bot-dialog-flow.md +++ b/bot-dialog-flow.md @@ -23,9 +23,7 @@ flowchart TB SELECT --> SUBS SUBS --> STATUS[Показать активный период
и остаток дней] STATUS --> RENEW[Inline: «Продлить подписку»] - RENEW --> OFFER[Сообщение с предложением оплаты] - OFFER --> PAY[Inline: «Оплатить»] - PAY --> INVOICE[Stars-инвойс] + RENEW --> INVOICE[Сразу открыть Stars-инвойс] INVOICE --> SUCCESS[Оплата подтверждена
successful_payment] SELECT --> HELP[Помощь
короткий onboarding и разделы] diff --git a/bot.py b/bot.py index 8abe19f..ff5f527 100644 --- a/bot.py +++ b/bot.py @@ -346,17 +346,6 @@ def subscription_renew_keyboard() -> InlineKeyboardMarkup: ) -def subscription_purchase_keyboard() -> InlineKeyboardMarkup: - return InlineKeyboardMarkup( - inline_keyboard=[[ - InlineKeyboardButton( - text=f"Оплатить {SUBSCRIPTION_PRICE_STARS} ⭐", - callback_data="subscription:buy", - ) - ]] - ) - - async def balance_command(message: Message, state: FSMContext) -> None: if not await require_mode(message, state, "subscription"): return @@ -412,23 +401,6 @@ async def _send_subscription_invoice(message: Message, user_id: int) -> None: @router.callback_query(F.data == "subscription:renew") async def subscription_renew(callback: CallbackQuery, state: FSMContext) -> None: - if await current_mode(state) != "subscription": - await callback.answer("Открой режим «Подписка» из главного меню.", show_alert=True) - return - message = callback.message - if not isinstance(message, Message): - await callback.answer("Открой режим «Подписка» ещё раз.", show_alert=True) - return - await callback.answer() - await message.answer( - f"Продление подписки на 30 дней — {SUBSCRIPTION_PRICE_STARS} ⭐. " - "Нажми кнопку, чтобы открыть счёт.", - reply_markup=subscription_purchase_keyboard(), - ) - - -@router.callback_query(F.data == "subscription:buy") -async def subscription_buy(callback: CallbackQuery, state: FSMContext) -> None: if await current_mode(state) != "subscription": await callback.answer("Открой режим «Подписка» из главного меню.", show_alert=True) return diff --git a/help_message.py b/help_message.py index 3cfc069..cad54fd 100644 --- a/help_message.py +++ b/help_message.py @@ -46,7 +46,7 @@ HELP_SECTIONS: dict[str, tuple[str, str]] = {
  • /balance — узнать статус: сколько осталось дней триала или подписки.
  • /subscribe — оплатить подписку на 30 дней.
  • -

    При входе в режим «Подписка» бот сразу показывает активный период и сколько дней осталось. Нажми «Продлить подписку», затем «Оплатить», чтобы открыть счёт. Когда период закончится, генерация постов и идей станет недоступна.

    """, +

    При входе в режим «Подписка» бот сразу показывает активный период и сколько дней осталось. Нажми «Продлить подписку», чтобы сразу открыть счёт в звёздах. Когда период закончится, генерация постов и идей станет недоступна.

    """, ), "commands": ( "⌨️ Команды", diff --git a/tests/test_keyboards.py b/tests/test_keyboards.py index 95fdf5b..438b9c8 100644 --- a/tests/test_keyboards.py +++ b/tests/test_keyboards.py @@ -173,46 +173,24 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase): "subscription:renew", ) - async def test_renew_callback_opens_purchase_prompt(self): + async def test_renew_callback_sends_invoice_without_intermediate_message(self): message_bot = AsyncMock() message = Message( message_id=91, date=datetime.now(), chat=Chat(id=555, type="private"), ).as_(message_bot) - callback = SimpleNamespace( - message=message, - answer=AsyncMock(), - ) - await bot.subscription_renew(callback, _StubState("subscription")) - - callback.answer.assert_awaited_once_with() - sent_call = message_bot.await_args - if sent_call is None: - self.fail("purchase prompt was not sent") - sent = sent_call.args[0] - self.assertIn("Продление подписки", sent.text) - self.assertEqual( - sent.reply_markup.inline_keyboard[0][0].callback_data, - "subscription:buy", - ) - - async def test_buy_callback_sends_invoice_for_clicking_user(self): - message = Message( - message_id=92, - date=datetime.now(), - chat=Chat(id=555, type="private"), - ).as_(AsyncMock()) callback = SimpleNamespace( message=message, from_user=SimpleNamespace(id=42), answer=AsyncMock(), ) with patch.object(bot, "_send_subscription_invoice", new=AsyncMock()) as send_invoice: - await bot.subscription_buy(callback, _StubState("subscription")) + await bot.subscription_renew(callback, _StubState("subscription")) callback.answer.assert_awaited_once_with() send_invoice.assert_awaited_once_with(message, 42) + message_bot.assert_not_awaited() async def test_idea_command_generates_only_in_generation_mode(self): message = _StubMessage("/idea")