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/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