From 473c3704cf5e3213b1b9719779a7e66742a94fee Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 21 Nov 2025 04:02:22 +0300 Subject: [PATCH] Clean up Platega and YooKassa prompts --- app/handlers/balance/platega.py | 29 ++++++++++- app/handlers/balance/stars.py | 66 +++++++++++++++++-------- app/handlers/balance/yookassa.py | 84 ++++++++++++++++++++++++++------ app/handlers/stars_payments.py | 46 ++++++++++++++--- app/services/payment/stars.py | 30 ------------ 5 files changed, 183 insertions(+), 72 deletions(-) diff --git a/app/handlers/balance/platega.py b/app/handlers/balance/platega.py index 40e8c45f..7792a225 100644 --- a/app/handlers/balance/platega.py +++ b/app/handlers/balance/platega.py @@ -98,6 +98,10 @@ async def _prompt_amount( ) await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data( + platega_prompt_message_id=message.message_id, + platega_prompt_chat_id=message.chat.id, + ) @error_handler @@ -300,7 +304,25 @@ async def process_platega_payment_amount( ), ) - await message.answer( + state_data = await state.get_data() + prompt_message_id = state_data.get("platega_prompt_message_id") + prompt_chat_id = state_data.get("platega_prompt_chat_id", message.chat.id) + + try: + await message.delete() + except Exception as delete_error: # pragma: no cover - зависит от прав бота + logger.warning("Не удалось удалить сообщение с суммой Platega: %s", delete_error) + + if prompt_message_id: + try: + await message.bot.delete_message(prompt_chat_id, prompt_message_id) + except Exception as delete_error: # pragma: no cover - диагностический лог + logger.warning( + "Не удалось удалить сообщение с запросом суммы Platega: %s", + delete_error, + ) + + invoice_message = await message.answer( instructions_template.format( method=method_title, amount=settings.format_price(amount_kopeks), @@ -311,6 +333,11 @@ async def process_platega_payment_amount( parse_mode="HTML", ) + await state.update_data( + platega_invoice_message_id=invoice_message.message_id, + platega_invoice_chat_id=invoice_message.chat.id, + ) + await state.clear() diff --git a/app/handlers/balance/stars.py b/app/handlers/balance/stars.py index 0dcf0031..a8cac0f8 100644 --- a/app/handlers/balance/stars.py +++ b/app/handlers/balance/stars.py @@ -1,11 +1,10 @@ import logging from aiogram import types from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import User -from app.keyboards.inline import get_back_keyboard, get_payment_methods_keyboard +from app.keyboards.inline import get_back_keyboard from app.localization.texts import get_texts from app.services.payment_service import PaymentService from app.states import BalanceStates @@ -22,11 +21,11 @@ async def start_stars_payment( state: FSMContext ): texts = get_texts(db_user.language) - + if not settings.TELEGRAM_STARS_ENABLED: await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True) return - + # Формируем текст сообщения в зависимости от настройки if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: message_text = ( @@ -35,10 +34,10 @@ async def start_stars_payment( ) else: message_text = texts.TOP_UP_AMOUNT - + # Создаем клавиатуру keyboard = get_back_keyboard(db_user.language) - + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: from .main import get_quick_amount_buttons @@ -46,12 +45,17 @@ async def start_stars_payment( if quick_amount_buttons: # Вставляем кнопки быстрого выбора перед кнопкой "Назад" keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - + await callback.message.edit_text( message_text, reply_markup=keyboard ) - + + await state.update_data( + stars_prompt_message_id=callback.message.message_id, + stars_prompt_chat_id=callback.message.chat.id, + ) + await state.set_state(BalanceStates.waiting_for_amount) await state.update_data(payment_method="stars") await callback.answer() @@ -65,29 +69,48 @@ async def process_stars_payment_amount( state: FSMContext ): texts = get_texts(db_user.language) - + if not settings.TELEGRAM_STARS_ENABLED: await message.answer("⚠️ Оплата Stars временно недоступна") return - + try: amount_rubles = amount_kopeks / 100 stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles) - stars_rate = settings.get_stars_rate() - + stars_rate = settings.get_stars_rate() + payment_service = PaymentService(message.bot) invoice_link = await payment_service.create_stars_invoice( amount_kopeks=amount_kopeks, description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}", payload=f"balance_{db_user.id}_{amount_kopeks}" ) - + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)], [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] ]) - - await message.answer( + + state_data = await state.get_data() + + prompt_message_id = state_data.get("stars_prompt_message_id") + prompt_chat_id = state_data.get("stars_prompt_chat_id", message.chat.id) + + try: + await message.delete() + except Exception as delete_error: # pragma: no cover - зависит от прав бота + logger.warning("Не удалось удалить сообщение с суммой Stars: %s", delete_error) + + if prompt_message_id: + try: + await message.bot.delete_message(prompt_chat_id, prompt_message_id) + except Exception as delete_error: # pragma: no cover - диагностический лог + logger.warning( + "Не удалось удалить сообщение с запросом суммы Stars: %s", + delete_error, + ) + + invoice_message = await message.answer( f"⭐ Оплата через Telegram Stars\n\n" f"💰 Сумма: {texts.format_price(amount_kopeks)}\n" f"⭐ К оплате: {stars_amount} звезд\n" @@ -96,9 +119,14 @@ async def process_stars_payment_amount( reply_markup=keyboard, parse_mode="HTML" ) - - await state.clear() - + + await state.update_data( + stars_invoice_message_id=invoice_message.message_id, + stars_invoice_chat_id=invoice_message.chat.id, + ) + + await state.set_state(None) + except Exception as e: logger.error(f"Ошибка создания Stars invoice: {e}") - await message.answer("⚠️ Ошибка создания платежа") \ No newline at end of file + await message.answer("⚠️ Ошибка создания платежа") diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py index 607f62bd..aad6acad 100644 --- a/app/handlers/balance/yookassa.py +++ b/app/handlers/balance/yookassa.py @@ -58,9 +58,13 @@ async def start_yookassa_payment( reply_markup=keyboard, parse_mode="HTML" ) - + await state.set_state(BalanceStates.waiting_for_amount) await state.update_data(payment_method="yookassa") + await state.update_data( + yookassa_prompt_message_id=callback.message.message_id, + yookassa_prompt_chat_id=callback.message.chat.id, + ) await callback.answer() @@ -108,9 +112,13 @@ async def start_yookassa_sbp_payment( reply_markup=keyboard, parse_mode="HTML" ) - + await state.set_state(BalanceStates.waiting_for_amount) await state.update_data(payment_method="yookassa_sbp") + await state.update_data( + yookassa_prompt_message_id=callback.message.message_id, + yookassa_prompt_chat_id=callback.message.chat.id, + ) await callback.answer() @@ -172,7 +180,25 @@ async def process_yookassa_payment_amount( [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] ]) - await message.answer( + state_data = await state.get_data() + prompt_message_id = state_data.get("yookassa_prompt_message_id") + prompt_chat_id = state_data.get("yookassa_prompt_chat_id", message.chat.id) + + try: + await message.delete() + except Exception as delete_error: # pragma: no cover - зависит от прав бота + logger.warning("Не удалось удалить сообщение с суммой YooKassa: %s", delete_error) + + if prompt_message_id: + try: + await message.bot.delete_message(prompt_chat_id, prompt_message_id) + except Exception as delete_error: # pragma: no cover - диагностический лог + logger.warning( + "Не удалось удалить сообщение с запросом суммы YooKassa: %s", + delete_error, + ) + + invoice_message = await message.answer( f"💳 Оплата банковской картой\n\n" f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" @@ -187,9 +213,13 @@ async def process_yookassa_payment_amount( reply_markup=keyboard, parse_mode="HTML" ) - + + await state.update_data( + yookassa_invoice_message_id=invoice_message.message_id, + yookassa_invoice_chat_id=invoice_message.chat.id, + ) + await state.clear() - logger.info(f"Создан платеж YooKassa для пользователя {db_user.telegram_id}: " f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") @@ -310,27 +340,45 @@ async def process_yookassa_sbp_payment_amount( # Создаем клавиатуру с кнопками для оплаты по ссылке и проверки статуса keyboard_buttons = [] - + # Добавляем кнопку оплаты, если доступна ссылка if confirmation_url: keyboard_buttons.append([types.InlineKeyboardButton(text="🔗 Перейти к оплате", url=confirmation_url)]) else: # Если ссылка недоступна, предлагаем оплатить через ID платежа в приложении банка keyboard_buttons.append([types.InlineKeyboardButton(text="📱 Оплатить в приложении банка", callback_data="temp_disabled")]) - + # Добавляем общие кнопки keyboard_buttons.append([types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")]) keyboard_buttons.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]) - + keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_buttons) - + + state_data = await state.get_data() + prompt_message_id = state_data.get("yookassa_prompt_message_id") + prompt_chat_id = state_data.get("yookassa_prompt_chat_id", message.chat.id) + + try: + await message.delete() + except Exception as delete_error: # pragma: no cover - зависит от прав бота + logger.warning("Не удалось удалить сообщение с суммой YooKassa (СБП): %s", delete_error) + + if prompt_message_id: + try: + await message.bot.delete_message(prompt_chat_id, prompt_message_id) + except Exception as delete_error: # pragma: no cover - диагностический лог + logger.warning( + "Не удалось удалить сообщение с запросом суммы YooKassa (СБП): %s", + delete_error, + ) + # Подготавливаем текст сообщения message_text = ( f"🔗 Оплата через СБП\n\n" f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" ) - + # Добавляем инструкции в зависимости от доступных способов оплаты if not confirmation_url: message_text += ( @@ -341,18 +389,18 @@ async def process_yookassa_sbp_payment_amount( f"4. Подтвердите платеж в приложении банка\n" f"5. Деньги поступят на баланс автоматически\n\n" ) - + message_text += ( f"🔒 Оплата происходит через защищенную систему YooKassa\n" f"✅ Принимаем СБП от всех банков-участников\n\n" f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}" ) - + # Отправляем сообщение с инструкциями и клавиатурой # Если есть QR-код, отправляем его как медиа-сообщение if qr_photo: # Используем метод отправки медиа-группы или фото с описанием - await message.answer_photo( + invoice_message = await message.answer_photo( photo=qr_photo, caption=message_text, reply_markup=keyboard, @@ -360,12 +408,18 @@ async def process_yookassa_sbp_payment_amount( ) else: # Если QR-код недоступен, отправляем обычное текстовое сообщение - await message.answer( + invoice_message = await message.answer( message_text, reply_markup=keyboard, parse_mode="HTML" ) - + + await state.update_data( + yookassa_invoice_message_id=invoice_message.message_id, + yookassa_invoice_chat_id=invoice_message.chat.id, + ) + + await state.clear() logger.info(f"Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: " f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py index 4b49f51f..48356450 100644 --- a/app/handlers/stars_payments.py +++ b/app/handlers/stars_payments.py @@ -1,10 +1,10 @@ import logging from decimal import Decimal, ROUND_HALF_UP from aiogram import Dispatcher, types, F +from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings -from app.database.models import User from app.services.payment_service import PaymentService from app.external.telegram_stars import TelegramStarsService from app.database.crud.user import get_user_by_telegram_id @@ -18,7 +18,9 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery): texts = get_texts(DEFAULT_LANGUAGE) try: - logger.info(f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}") + logger.info( + f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}" + ) allowed_prefixes = ("balance_", "admin_stars_test_", "simple_sub_") @@ -35,6 +37,7 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery): try: from app.database.database import get_db + async for db in get_db(): user = await get_user_by_telegram_id(db, query.from_user.id) if not user: @@ -77,6 +80,7 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery): async def handle_successful_payment( message: types.Message, db: AsyncSession, + state: FSMContext, **kwargs ): texts = get_texts(DEFAULT_LANGUAGE) @@ -106,6 +110,27 @@ async def handle_successful_payment( return payment_service = PaymentService(message.bot) + + state_data = await state.get_data() + prompt_message_id = state_data.get("stars_prompt_message_id") + prompt_chat_id = state_data.get("stars_prompt_chat_id", message.chat.id) + invoice_message_id = state_data.get("stars_invoice_message_id") + invoice_chat_id = state_data.get("stars_invoice_chat_id", message.chat.id) + + for chat_id, message_id, label in [ + (prompt_chat_id, prompt_message_id, "запрос суммы"), + (invoice_chat_id, invoice_message_id, "инвойс Stars"), + ]: + if message_id: + try: + await message.bot.delete_message(chat_id, message_id) + except Exception as delete_error: # pragma: no cover - зависит от прав бота + logger.warning( + "Не удалось удалить сообщение %s после оплаты Stars: %s", + label, + delete_error, + ) + success = await payment_service.process_stars_payment( db=db, user_id=user.id, @@ -113,7 +138,14 @@ async def handle_successful_payment( payload=payment.invoice_payload, telegram_payment_charge_id=payment.telegram_payment_charge_id ) - + + await state.update_data( + stars_prompt_message_id=None, + stars_prompt_chat_id=None, + stars_invoice_message_id=None, + stars_invoice_chat_id=None, + ) + if success: rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount) amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP)) @@ -172,15 +204,15 @@ async def handle_successful_payment( def register_stars_handlers(dp: Dispatcher): - + dp.pre_checkout_query.register( handle_pre_checkout_query, - F.currency == "XTR" + F.currency == "XTR" ) - + dp.message.register( handle_successful_payment, F.successful_payment ) - + logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей") diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index b7fd5942..3f1022d7 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -515,36 +515,6 @@ class TelegramStarsMixin: exc_info=True, ) - if getattr(self, "bot", None): - try: - keyboard = await self.build_topup_success_keyboard(user) - - charge_id_short = (telegram_payment_charge_id or getattr(transaction, "external_id", ""))[:8] - - await self.bot.send_message( - user.telegram_id, - ( - "✅ Пополнение успешно!\n\n" - f"⭐ Звезд: {stars_amount}\n" - f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" - "🦊 Способ: Telegram Stars\n" - f"🆔 Транзакция: {charge_id_short}...\n\n" - "Баланс пополнен автоматически!" - ), - parse_mode="HTML", - reply_markup=keyboard, - ) - logger.info( - "✅ Отправлено уведомление пользователю %s о пополнении на %s", - user.telegram_id, - settings.format_price(amount_kopeks), - ) - except Exception as error: # pragma: no cover - диагностический лог - logger.error( - "Ошибка отправки уведомления о пополнении Stars: %s", - error, - ) - # Проверяем наличие сохраненной корзины для возврата к оформлению подписки try: from aiogram import types