From 9400050ade5c2e37f43f5fdc211817b499648f39 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 30 Oct 2026 13:24:53 +0200 Subject: [PATCH] feat(subscription): return to main menu after payment/renewal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handle_successful_payment: after status, reset FSM to lobby and send main menu keyboard (state injected by dispatcher) - confirm_development_payment (dev path): same — edit status then reply with main menu; add state param - tests: assert second message carries main-menu markup; _StubState clear/set_state --- VERSION | 2 +- bot.py | 5 ++++- payments.py | 14 +++++++++++++- tests/test_payments.py | 20 ++++++++++++++------ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index 1aa75a0..6e24410 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+79 +0.1.0+80 diff --git a/bot.py b/bot.py index 7df4507..ffab1a2 100644 --- a/bot.py +++ b/bot.py @@ -415,7 +415,7 @@ async def help_navigation(callback: CallbackQuery, state: FSMContext) -> None: @router.callback_query(F.data.startswith("dev_paid:")) -async def confirm_development_payment(callback: CallbackQuery) -> None: +async def confirm_development_payment(callback: CallbackQuery, state: FSMContext) -> None: if not is_development_environment(): await callback.answer() return @@ -444,6 +444,9 @@ async def confirm_development_payment(callback: CallbackQuery) -> None: await callback.message.edit_text( user_store.subscription_status_text(sub) ) + await callback.message.answer("Выбери действие.", reply_markup=main_menu_keyboard()) + await state.clear() + await state.set_state(BotMode.lobby) @router.message(CommandStart()) diff --git a/payments.py b/payments.py index 5710e18..55d577f 100644 --- a/payments.py +++ b/payments.py @@ -15,8 +15,11 @@ import os from contextlib import closing from aiogram import F, Router +from aiogram.fsm.context import FSMContext from aiogram.types import LabeledPrice, Message, PreCheckoutQuery, SuccessfulPayment +from keyboards import main_menu_keyboard + from user_store import ( SUBSCRIPTION_PLANS, activate_subscription_days, @@ -135,8 +138,16 @@ async def handle_pre_checkout(query: PreCheckoutQuery) -> None: await query.answer(ok=True) +async def _return_to_main_menu(message: Message, state: FSMContext | None) -> None: + """После успешной оплаты/продления возвращает пользователя в главное меню.""" + if state is not None: + await state.clear() + await state.set_state("BotMode:lobby") + await message.answer("Оплата принята.", reply_markup=main_menu_keyboard()) + + @router.message(F.successful_payment) -async def handle_successful_payment(message: Message) -> None: +async def handle_successful_payment(message: Message, state: FSMContext | None = None) -> None: payment = message.successful_payment if payment is None or message.from_user is None: return @@ -169,6 +180,7 @@ async def handle_successful_payment(message: Message) -> None: db_path, ) await message.answer(subscription_status_text(sub)) + await _return_to_main_menu(message, state) async def _reject(message: Message, charge_id: str, reason: str) -> None: diff --git a/tests/test_payments.py b/tests/test_payments.py index 65b8ad3..ca7d53a 100644 --- a/tests/test_payments.py +++ b/tests/test_payments.py @@ -20,6 +20,12 @@ class _StubState: async def get_state(self): return self.value + async def clear(self): + self.value = None + + async def set_state(self, state): + self.value = getattr(state, "state", state) + class PaymentStoreTests(unittest.TestCase): def setUp(self): @@ -166,8 +172,10 @@ class SuccessfulPaymentTests(unittest.IsolatedAsyncioTestCase): user_store.ensure_subscription(42, self.db) with patch.dict(os.environ, {"BOT_DB_PATH": self.db}): await payments.handle_successful_payment(message) - self.assertEqual(len(message.answers), 1) + self.assertEqual(len(message.answers), 2) self.assertIn("Подписка активна", message.answers[0]) + # Второе сообщение — возврат в главное меню с его клавиатурой. + self.assertIsNotNone(message.answer_kwargs[1].get("reply_markup")) self.assertTrue(payments.payment_recorded("charge1", self.db)) row = user_store.subscription_row(42, self.db) self.assertIsNotNone(row["last_paid_at"]) @@ -257,7 +265,7 @@ class SubscribeCommandTests(unittest.IsolatedAsyncioTestCase): patch.object(bot, "DEPLOY_ENV", "development"), patch.object(bot.asyncio, "to_thread", new=AsyncMock()) as to_thread, ): - await bot.confirm_development_payment(callback) + await bot.confirm_development_payment(callback, _StubState()) to_thread.assert_not_awaited() callback.answer.assert_awaited_once_with("Dev-оплата подтверждена. Срок подписки не изменён.") @@ -289,7 +297,7 @@ class SubscribeCommandTests(unittest.IsolatedAsyncioTestCase): patch.dict(os.environ, {"BOT_DB_PATH": db}), patch.object(bot.asyncio, "to_thread", new=_direct_thread_mock()) as to_thread, ): - await bot.confirm_development_payment(callback) + await bot.confirm_development_payment(callback, _StubState()) db_row = user_store.subscription_row(42, db) patch_calls = [call for call in to_thread.await_args_list if call.args[0] is user_store.activate_subscription_days] self.assertEqual(patch_calls[0].args[1:], (42, 10)) @@ -304,7 +312,7 @@ class SubscribeCommandTests(unittest.IsolatedAsyncioTestCase): patch.object(bot, "DEPLOY_ENV", "development"), patch.object(bot.asyncio, "to_thread", new=AsyncMock()) as to_thread, ): - await bot.confirm_development_payment(callback) + await bot.confirm_development_payment(callback, _StubState()) to_thread.assert_not_awaited() callback.answer.assert_awaited_once() @@ -316,7 +324,7 @@ class SubscribeCommandTests(unittest.IsolatedAsyncioTestCase): patch.object(bot, "DEPLOY_ENV", "production"), patch.object(bot.asyncio, "to_thread", new=AsyncMock()) as to_thread, ): - await bot.confirm_development_payment(callback) + await bot.confirm_development_payment(callback, _StubState()) to_thread.assert_not_awaited() callback.answer.assert_awaited_once_with() @@ -328,7 +336,7 @@ class SubscribeCommandTests(unittest.IsolatedAsyncioTestCase): patch.object(bot, "DEPLOY_ENV", "staging"), patch.object(bot.asyncio, "to_thread", new=AsyncMock()) as to_thread, ): - await bot.confirm_development_payment(callback) + await bot.confirm_development_payment(callback, _StubState()) to_thread.assert_not_awaited() callback.answer.assert_awaited_once_with()