fix: use reply keyboard for subscription purchase
This commit is contained in:
1 parent
aba1eb4ced
commit
073c7d2c81
4 files changed
+89
-33
No files matched your search
@@ -39,7 +39,7 @@ from raw_updates import RawUpdateMiddleware, init_raw_updates_db
|
||||
|
||||
from apinex_client import APINEX_STT_URL, POST_HISTORY_CHAR_LIMIT, POST_HISTORY_LIMIT, classify_intent, classify_prompt_injection, create_tone_profile, current_operation_id, diagnostic_operation, extract_posts_from_images, extract_transcript, generate_ideas, generate_posts, generate_posts_from_images, transcribe_audio
|
||||
from help_message import WELCOME_RICH_HTML, help_plain_view, help_view, rich_to_plain
|
||||
from keyboards import MAIN_MENU_VALUE, SUPPORT_VALUE, TRAINING_RESET_VALUE, help_inline_keyboard, main_menu_keyboard, mode_keyboard, resolve_button_text
|
||||
from keyboards import MAIN_MENU_VALUE, SUBSCRIPTION_ACTION_VALUE, SUPPORT_VALUE, TRAINING_RESET_VALUE, help_inline_keyboard, main_menu_keyboard, mode_keyboard, resolve_button_text
|
||||
from payments import send_subscription_invoice
|
||||
import support
|
||||
from user_store import (
|
||||
@@ -148,10 +148,7 @@ MODE_COPY = {
|
||||
"Генерация",
|
||||
"Напиши запрос или отправь голосовое.\nДля правки ответь на мой вариант.",
|
||||
),
|
||||
"subscription": (
|
||||
"Подписка",
|
||||
"Проверь статус ниже.\nКнопка откроет оплату или продление.",
|
||||
),
|
||||
"subscription": ("Подписка", "Проверь статус ниже.\nВыбери действие в меню."),
|
||||
"help": ("Помощь", "Выбери раздел в сообщении выше."),
|
||||
}
|
||||
|
||||
@@ -172,6 +169,9 @@ async def enter_mode(message: Message, state: FSMContext, mode: str) -> None:
|
||||
await _close_support_case(message)
|
||||
await state.clear()
|
||||
await state.set_state(MODE_STATES[mode])
|
||||
subscription = None
|
||||
if mode == "subscription" and message.from_user is not None:
|
||||
subscription = await asyncio.to_thread(subscription_row, message.from_user.id)
|
||||
title, description = MODE_COPY[mode]
|
||||
if mode == "help":
|
||||
await send_rich_message(
|
||||
@@ -182,11 +182,17 @@ async def enter_mode(message: Message, state: FSMContext, mode: str) -> None:
|
||||
)
|
||||
await message.answer(
|
||||
f"Режим «{title}».\n{description}",
|
||||
reply_markup=mode_keyboard(mode),
|
||||
reply_markup=mode_keyboard(
|
||||
mode,
|
||||
subscription_purchased=bool(subscription and subscription["last_paid_at"]),
|
||||
),
|
||||
)
|
||||
if mode == "subscription" and message.from_user is not None:
|
||||
sub = await asyncio.to_thread(subscription_row, message.from_user.id)
|
||||
await _show_subscription_info(message, sub)
|
||||
await _show_subscription_info(message, subscription)
|
||||
elif mode == "subscription":
|
||||
await message.answer(
|
||||
"Не удалось определить пользователя.", reply_markup=mode_keyboard("subscription")
|
||||
)
|
||||
|
||||
|
||||
async def show_main_menu(message: Message, state: FSMContext) -> None:
|
||||
@@ -472,23 +478,14 @@ async def _show_subscription_info(
|
||||
text += f"\n\nПродление на 30 дней — {SUBSCRIPTION_PRICE_STARS} ⭐."
|
||||
if message.from_user is not None:
|
||||
text += f"\nID пользователя: {message.from_user.id}"
|
||||
markup = subscription_renew_keyboard()
|
||||
markup = mode_keyboard(
|
||||
"subscription", subscription_purchased=bool(sub["last_paid_at"])
|
||||
) if sub else mode_keyboard("subscription")
|
||||
else:
|
||||
markup = mode_keyboard(mode)
|
||||
await message.answer(text, reply_markup=markup)
|
||||
|
||||
|
||||
def subscription_renew_keyboard() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[[
|
||||
InlineKeyboardButton(
|
||||
text="Продлить подписку",
|
||||
callback_data="subscription:renew",
|
||||
)
|
||||
]]
|
||||
)
|
||||
|
||||
|
||||
async def balance_command(message: Message, state: FSMContext) -> None:
|
||||
if not await require_mode(message, state, "subscription"):
|
||||
return
|
||||
@@ -1439,6 +1436,10 @@ async def handle_text(message: Message, state: FSMContext) -> None:
|
||||
if button == SUPPORT_VALUE:
|
||||
await open_support(message, state)
|
||||
return
|
||||
if button == SUBSCRIPTION_ACTION_VALUE:
|
||||
if await require_mode(message, state, "subscription") and message.from_user is not None:
|
||||
await _send_subscription_invoice(message, message.from_user.id)
|
||||
return
|
||||
if button == TRAINING_RESET_VALUE:
|
||||
await clear_command(message, state)
|
||||
return
|
||||
@@ -1463,7 +1464,7 @@ async def handle_text(message: Message, state: FSMContext) -> None:
|
||||
prompts = {
|
||||
"lobby": "Выбери режим работы кнопкой ниже.",
|
||||
"training": "Пришли пост текстом, скриншотом, голосовым или аудиофайлом — профиль будет накапливаться.",
|
||||
"subscription": "Статус уже показан выше. Для продления нажми кнопку «Продлить подписку».",
|
||||
"subscription": "Статус уже показан выше. Нажми кнопку покупки или продления подписки.",
|
||||
"help": "Открой нужный раздел справки выше или выбери другой режим.",
|
||||
}
|
||||
await message.answer(
|
||||
@@ -1484,7 +1485,7 @@ async def handle_other_message(message: Message, state: FSMContext) -> None:
|
||||
elif mode == "generation":
|
||||
text = "Для генерации отправь текстовый запрос или голосовое сообщение."
|
||||
elif mode == "subscription":
|
||||
text = "Статус уже показан выше. Для продления нажми «Продлить подписку»."
|
||||
text = "Статус уже показан выше. Нажми кнопку покупки или продления подписки."
|
||||
elif mode == "help":
|
||||
text = "Выбери раздел справки или другой режим в меню."
|
||||
else:
|
||||
|
||||
+13
-1
@@ -24,6 +24,9 @@ TRAINING_RESET_VALUE = "__training_reset__"
|
||||
POST_GENERATE_BUTTON = "✍️ Придумать пост"
|
||||
SUPPORT_BUTTON = "🆘 Поддержка"
|
||||
SUPPORT_VALUE = "__support__"
|
||||
SUBSCRIPTION_ACTION_VALUE = "__subscription_action__"
|
||||
BUY_SUBSCRIPTION_BUTTON = "🛒 Купить подписку"
|
||||
RENEW_SUBSCRIPTION_BUTTON = "🔄 Продлить подписку"
|
||||
|
||||
BUTTON_TEXT_MAP: dict[str, str] = {
|
||||
**MODE_BUTTONS,
|
||||
@@ -32,6 +35,8 @@ BUTTON_TEXT_MAP: dict[str, str] = {
|
||||
TRAINING_RESET_BUTTON: TRAINING_RESET_VALUE,
|
||||
POST_GENERATE_BUTTON: "/post",
|
||||
SUPPORT_BUTTON: SUPPORT_VALUE,
|
||||
BUY_SUBSCRIPTION_BUTTON: SUBSCRIPTION_ACTION_VALUE,
|
||||
RENEW_SUBSCRIPTION_BUTTON: SUBSCRIPTION_ACTION_VALUE,
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +52,11 @@ def main_menu_keyboard() -> ReplyKeyboardMarkup:
|
||||
)
|
||||
|
||||
|
||||
def mode_keyboard(mode: str, support: bool = False) -> ReplyKeyboardMarkup:
|
||||
def mode_keyboard(
|
||||
mode: str,
|
||||
support: bool = False,
|
||||
subscription_purchased: bool = False,
|
||||
) -> ReplyKeyboardMarkup:
|
||||
"""Внутри режима показывает релевантные действия и возврат в меню.
|
||||
|
||||
В активном обращении поддержки остаётся только кнопка «Главное меню».
|
||||
@@ -65,6 +74,9 @@ def mode_keyboard(mode: str, support: bool = False) -> ReplyKeyboardMarkup:
|
||||
rows.insert(0, [KeyboardButton(text=POST_GENERATE_BUTTON)])
|
||||
elif mode == "help":
|
||||
rows.insert(0, [KeyboardButton(text=SUPPORT_BUTTON)])
|
||||
elif mode == "subscription":
|
||||
action = RENEW_SUBSCRIPTION_BUTTON if subscription_purchased else BUY_SUBSCRIPTION_BUTTON
|
||||
rows.insert(0, [KeyboardButton(text=action)])
|
||||
return ReplyKeyboardMarkup(
|
||||
keyboard=rows,
|
||||
resize_keyboard=True,
|
||||
|
||||
+51
-8
@@ -49,6 +49,13 @@ class ButtonTextTests(unittest.TestCase):
|
||||
def test_generate_post_button_maps_to_post_command(self):
|
||||
self.assertEqual(keyboards.resolve_button_text(keyboards.POST_GENERATE_BUTTON), "/post")
|
||||
|
||||
def test_subscription_actions_map_to_internal_action(self):
|
||||
for label in (keyboards.BUY_SUBSCRIPTION_BUTTON, keyboards.RENEW_SUBSCRIPTION_BUTTON):
|
||||
self.assertEqual(
|
||||
keyboards.resolve_button_text(label),
|
||||
keyboards.SUBSCRIPTION_ACTION_VALUE,
|
||||
)
|
||||
|
||||
def test_regular_text_is_not_treated_as_button(self):
|
||||
self.assertIsNone(keyboards.resolve_button_text("Напиши пост про запуск"))
|
||||
self.assertIsNone(keyboards.resolve_button_text("привет"))
|
||||
@@ -74,8 +81,18 @@ class KeyboardLayoutTests(unittest.TestCase):
|
||||
expected.insert(0, keyboards.POST_GENERATE_BUTTON)
|
||||
elif mode == "help":
|
||||
expected.insert(0, keyboards.SUPPORT_BUTTON)
|
||||
elif mode == "subscription":
|
||||
expected.insert(0, keyboards.BUY_SUBSCRIPTION_BUTTON)
|
||||
self.assertEqual(labels, expected, mode)
|
||||
|
||||
def test_subscription_keyboard_uses_renew_label_after_paid_subscription(self):
|
||||
labels = [
|
||||
button.text
|
||||
for row in keyboards.mode_keyboard("subscription", subscription_purchased=True).keyboard
|
||||
for button in row
|
||||
]
|
||||
self.assertEqual(labels, [keyboards.RENEW_SUBSCRIPTION_BUTTON, keyboards.MAIN_MENU_BUTTON])
|
||||
|
||||
def test_support_keyboard_keeps_only_main_menu(self):
|
||||
for mode in bot.MODE_STATES:
|
||||
labels = [
|
||||
@@ -211,7 +228,7 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase):
|
||||
await bot.handle_text(message, state)
|
||||
self.assertEqual(await state.get_state(), bot.BotMode.help.state)
|
||||
|
||||
async def test_subscription_entry_shows_status_and_renew_button(self):
|
||||
async def test_subscription_entry_shows_buy_reply_button_without_purchase(self):
|
||||
message = _StubMessage("💳 Подписка")
|
||||
state = _StubState("lobby")
|
||||
with (
|
||||
@@ -222,14 +239,40 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(await state.get_state(), bot.BotMode.subscription.state)
|
||||
self.assertEqual(len(message.answers), 2)
|
||||
mode_keyboard = message.answers[0][1]["reply_markup"]
|
||||
self.assertEqual(message.answers[0][1]["reply_markup"].keyboard[0][0].text, keyboards.MAIN_MENU_BUTTON)
|
||||
initial_keyboard = message.answers[0][1]["reply_markup"]
|
||||
self.assertEqual(initial_keyboard.keyboard[0][0].text, keyboards.BUY_SUBSCRIPTION_BUTTON)
|
||||
self.assertIn("Подписка не активирована", message.answers[1][0])
|
||||
renew_keyboard = message.answers[1][1]["reply_markup"]
|
||||
self.assertEqual(
|
||||
renew_keyboard.inline_keyboard[0][0].callback_data,
|
||||
"subscription:renew",
|
||||
)
|
||||
buy_keyboard = message.answers[1][1]["reply_markup"]
|
||||
self.assertFalse(hasattr(buy_keyboard, "inline_keyboard"))
|
||||
self.assertEqual(buy_keyboard.keyboard[0][0].text, keyboards.BUY_SUBSCRIPTION_BUTTON)
|
||||
|
||||
async def test_subscription_status_uses_renew_button_after_previous_payment(self):
|
||||
message = _StubMessage()
|
||||
sub = {"last_paid_at": "2026-01-01T00:00:00", "expires_at": "2026-02-01T00:00:00", "is_active": 1}
|
||||
with patch.object(bot.user_store, "_days_until", return_value=0):
|
||||
await bot._show_subscription_info(cast(Any, message), cast(Any, sub))
|
||||
markup = message.answers[0][1]["reply_markup"]
|
||||
self.assertFalse(hasattr(markup, "inline_keyboard"))
|
||||
self.assertEqual(markup.keyboard[0][0].text, keyboards.RENEW_SUBSCRIPTION_BUTTON)
|
||||
|
||||
async def test_subscription_action_button_sends_invoice_in_subscription_mode(self):
|
||||
message = _StubMessage(keyboards.BUY_SUBSCRIPTION_BUTTON)
|
||||
with (
|
||||
patch.object(bot, "notify_development_deploy", new=AsyncMock()),
|
||||
patch.object(bot, "_send_subscription_invoice", new=AsyncMock()) as send_invoice,
|
||||
):
|
||||
await bot.handle_text(message, _StubState("subscription"))
|
||||
send_invoice.assert_awaited_once_with(message, 42)
|
||||
|
||||
async def test_subscription_action_button_is_rejected_outside_subscription_mode(self):
|
||||
message = _StubMessage(keyboards.RENEW_SUBSCRIPTION_BUTTON)
|
||||
with (
|
||||
patch.object(bot, "notify_development_deploy", new=AsyncMock()),
|
||||
patch.object(bot, "_send_subscription_invoice", new=AsyncMock()) as send_invoice,
|
||||
):
|
||||
await bot.handle_text(message, _StubState("generation"))
|
||||
send_invoice.assert_not_awaited()
|
||||
self.assertIn("только в режиме «Подписка»", message.answers[0][0])
|
||||
|
||||
async def test_renew_callback_sends_invoice_without_intermediate_message(self):
|
||||
message_bot = AsyncMock()
|
||||
|
||||
@@ -1016,8 +1016,8 @@ class SubscriptionTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn("пробный период", message.answer.call_args.args[0].lower())
|
||||
renew_markup = message.answer.call_args.kwargs["reply_markup"]
|
||||
self.assertEqual(
|
||||
renew_markup.inline_keyboard[0][0].callback_data,
|
||||
"subscription:renew",
|
||||
renew_markup.keyboard[0][0].text,
|
||||
"🛒 Купить подписку",
|
||||
)
|
||||
|
||||
async def test_blocked_when_subscription_expired(self):
|
||||
|
||||
Reference in new issue
Block a user