diff --git a/app/database/crud/mulenpay.py b/app/database/crud/mulenpay.py index 062dcaab..17648f4a 100644 --- a/app/database/crud/mulenpay.py +++ b/app/database/crud/mulenpay.py @@ -3,7 +3,6 @@ from datetime import datetime from typing import Optional from sqlalchemy import select -from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -91,7 +90,6 @@ async def update_mulenpay_payment_status( paid_at: Optional[datetime] = None, callback_payload: Optional[dict] = None, mulen_payment_id: Optional[int] = None, - metadata: Optional[dict] = None, ) -> MulenPayPayment: payment.status = status if is_paid is not None: @@ -102,8 +100,6 @@ async def update_mulenpay_payment_status( payment.callback_payload = callback_payload if mulen_payment_id is not None and not payment.mulen_payment_id: payment.mulen_payment_id = mulen_payment_id - if metadata is not None: - payment.metadata_json = metadata payment.updated_at = datetime.utcnow() await db.commit() @@ -111,19 +107,6 @@ async def update_mulenpay_payment_status( return payment -async def update_mulenpay_payment_metadata( - db: AsyncSession, - *, - payment: MulenPayPayment, - metadata: dict, -) -> MulenPayPayment: - payment.metadata_json = metadata - payment.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(payment) - return payment - - async def link_mulenpay_payment_to_transaction( db: AsyncSession, *, diff --git a/app/database/crud/pal24.py b/app/database/crud/pal24.py index 35cffa61..a88c46b4 100644 --- a/app/database/crud/pal24.py +++ b/app/database/crud/pal24.py @@ -96,7 +96,6 @@ async def update_pal24_payment_status( balance_currency: Optional[str] = None, payer_account: Optional[str] = None, callback_payload: Optional[Dict[str, Any]] = None, - metadata: Optional[Dict[str, Any]] = None, ) -> Pal24Payment: update_values: Dict[str, Any] = { "status": status, @@ -122,8 +121,6 @@ async def update_pal24_payment_status( update_values["payer_account"] = payer_account if callback_payload is not None: update_values["callback_payload"] = callback_payload - if metadata is not None: - update_values["metadata_json"] = metadata update_values["last_status"] = status diff --git a/app/external/yookassa_webhook.py b/app/external/yookassa_webhook.py index 6ea1811d..0c6485b5 100644 --- a/app/external/yookassa_webhook.py +++ b/app/external/yookassa_webhook.py @@ -254,16 +254,16 @@ class YooKassaWebhookHandler: logger.info(f"📊 Обработка webhook YooKassa: {webhook_data.get('event', 'unknown_event')}") logger.debug(f"🔍 Полные данные webhook: {webhook_data}") - event_type = webhook_data.get("event") - if not event_type: - logger.warning("⚠️ Webhook YooKassa без типа события") - return web.Response(status=400, text="No event type") - # Извлекаем ID платежа из вебхука для предотвращения дублирования yookassa_payment_id = webhook_data.get("object", {}).get("id") if not yookassa_payment_id: logger.warning("⚠️ Webhook YooKassa без ID платежа") - return web.Response(status=400, text="No payment id") + return web.Response(status=400, text="No payment ID") + + event_type = webhook_data.get("event") + if not event_type: + logger.warning("⚠️ Webhook YooKassa без типа события") + return web.Response(status=400, text="No event type") if event_type not in YOOKASSA_ALLOWED_EVENTS: logger.info(f"ℹ️ Игнорируем событие YooKassa: {event_type}") @@ -274,10 +274,8 @@ class YooKassaWebhookHandler: # Проверяем, не обрабатывается ли этот платеж уже (защита от дублирования) from app.database.models import PaymentMethod from app.database.crud.transaction import get_transaction_by_external_id - existing_transaction = None - if yookassa_payment_id and hasattr(db, "execute"): - existing_transaction = await get_transaction_by_external_id(db, yookassa_payment_id, PaymentMethod.YOOKASSA) - + existing_transaction = await get_transaction_by_external_id(db, yookassa_payment_id, PaymentMethod.YOOKASSA) + if existing_transaction and event_type == "payment.succeeded": logger.info(f"ℹ️ Платеж YooKassa {yookassa_payment_id} уже был обработан. Пропускаем дублирующий вебхук.") return web.Response(status=200, text="OK") diff --git a/app/handlers/balance/heleket.py b/app/handlers/balance/heleket.py index f97aa1b2..37ac4b53 100644 --- a/app/handlers/balance/heleket.py +++ b/app/handlers/balance/heleket.py @@ -1,10 +1,8 @@ import logging -from datetime import datetime from typing import Optional from aiogram import types from aiogram.fsm.context import FSMContext -from sqlalchemy import update from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -68,11 +66,7 @@ async def start_heleket_payment( ) await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data( - payment_method="heleket", - heleket_prompt_message_id=callback.message.message_id, - heleket_prompt_chat_id=callback.message.chat.id, - ) + await state.update_data(payment_method="heleket") await callback.answer() @@ -187,52 +181,7 @@ async def process_heleket_payment_amount( [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")], ]) - state_data = await state.get_data() - prompt_message_id = state_data.get("heleket_prompt_message_id") - prompt_chat_id = state_data.get("heleket_prompt_chat_id", message.chat.id) - - try: - await message.delete() - except Exception as delete_error: # pragma: no cover - depends on bot rights - logger.warning("Не удалось удалить сообщение с суммой Heleket: %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 - diagnostic - logger.warning( - "Не удалось удалить сообщение с запросом суммы Heleket: %s", - delete_error, - ) - - invoice_message = await message.answer( - "\n".join(details), parse_mode="HTML", reply_markup=keyboard - ) - - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_heleket_payment_by_id(db, result["local_payment_id"]) - if payment: - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await db.execute( - update(payment.__class__) - .where(payment.__class__.id == payment.id) - .values(metadata_json=metadata, updated_at=datetime.utcnow()) - ) - await db.commit() - except Exception as error: # pragma: no cover - diagnostics - logger.warning("Не удалось сохранить сообщение Heleket: %s", error) - - await state.update_data( - heleket_invoice_message_id=invoice_message.message_id, - heleket_invoice_chat_id=invoice_message.chat.id, - ) - + await message.answer("\n".join(details), parse_mode="HTML", reply_markup=keyboard) await state.clear() diff --git a/app/handlers/balance/mulenpay.py b/app/handlers/balance/mulenpay.py index f345abeb..840ac469 100644 --- a/app/handlers/balance/mulenpay.py +++ b/app/handlers/balance/mulenpay.py @@ -59,11 +59,7 @@ async def start_mulenpay_payment( ) await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data( - payment_method="mulenpay", - mulenpay_prompt_message_id=callback.message.message_id, - mulenpay_prompt_chat_id=callback.message.chat.id, - ) + await state.update_data(payment_method="mulenpay") await callback.answer() @@ -97,26 +93,6 @@ async def process_mulenpay_payment_amount( amount_rubles = amount_kopeks / 100 - state_data = await state.get_data() - prompt_message_id = state_data.get("mulenpay_prompt_message_id") - prompt_chat_id = state_data.get("mulenpay_prompt_chat_id", message.chat.id) - - try: - await message.delete() - except Exception as delete_error: # pragma: no cover - depends on bot permissions - logger.warning( - "Не удалось удалить сообщение с суммой MulenPay: %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 - diagnostic - logger.warning( - "Не удалось удалить сообщение с запросом суммы MulenPay: %s", - delete_error, - ) - try: payment_service = PaymentService(message.bot) payment_result = await payment_service.create_mulenpay_payment( @@ -187,39 +163,12 @@ async def process_mulenpay_payment_amount( mulenpay_name_html=mulenpay_name_html, ) - invoice_message = await message.answer( + await message.answer( message_text, reply_markup=keyboard, parse_mode="HTML", ) - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_mulenpay_payment_by_local_id( - db, local_payment_id - ) - if payment: - payment_metadata = dict( - getattr(payment, "metadata_json", {}) or {} - ) - payment_metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await payment_module.update_mulenpay_payment_metadata( - db, - payment=payment, - metadata=payment_metadata, - ) - except Exception as error: # pragma: no cover - diagnostic logging only - logger.warning("Не удалось сохранить данные сообщения MulenPay: %s", error) - - await state.update_data( - mulenpay_invoice_message_id=invoice_message.message_id, - mulenpay_invoice_chat_id=invoice_message.chat.id, - ) - await state.clear() logger.info( diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py index 28eda7f9..0a66b20f 100644 --- a/app/handlers/balance/pal24.py +++ b/app/handlers/balance/pal24.py @@ -1,12 +1,10 @@ import html import logging -from datetime import datetime from typing import Any, Optional from aiogram import types from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext -from sqlalchemy import update from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -206,36 +204,12 @@ async def _send_pal24_payment_message( support=settings.get_support_contact_display_html(), ) - invoice_message = await message.answer( + await message.answer( message_text, reply_markup=keyboard, parse_mode="HTML", ) - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_pal24_payment_by_id(db, local_payment_id) - if payment: - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await db.execute( - update(payment.__class__) - .where(payment.__class__.id == payment.id) - .values(metadata_json=metadata, updated_at=datetime.utcnow()) - ) - await db.commit() - except Exception as error: # pragma: no cover - diagnostics - logger.warning("Не удалось сохранить сообщение PayPalych: %s", error) - - await state.update_data( - pal24_invoice_message_id=invoice_message.message_id, - pal24_invoice_chat_id=invoice_message.chat.id, - ) - await state.clear() logger.info( @@ -303,11 +277,7 @@ async def start_pal24_payment( ) await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data( - payment_method="pal24", - pal24_prompt_message_id=callback.message.message_id, - pal24_prompt_chat_id=callback.message.chat.id, - ) + await state.update_data(payment_method="pal24") await callback.answer() @@ -339,24 +309,6 @@ async def process_pal24_payment_amount( available_methods = _get_available_pal24_methods() - state_data = await state.get_data() - prompt_message_id = state_data.get("pal24_prompt_message_id") - prompt_chat_id = state_data.get("pal24_prompt_chat_id", message.chat.id) - - try: - await message.delete() - except Exception as delete_error: # pragma: no cover - depends on bot rights - logger.warning("Не удалось удалить сообщение с суммой PayPalych: %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 - diagnostic - logger.warning( - "Не удалось удалить сообщение с запросом суммы PayPalych: %s", - delete_error, - ) - if len(available_methods) == 1: await _send_pal24_payment_message( message, diff --git a/app/handlers/balance/platega.py b/app/handlers/balance/platega.py index def18f65..40e8c45f 100644 --- a/app/handlers/balance/platega.py +++ b/app/handlers/balance/platega.py @@ -98,10 +98,6 @@ 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 @@ -304,25 +300,7 @@ async def process_platega_payment_amount( ), ) - 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( + await message.answer( instructions_template.format( method=method_title, amount=settings.format_price(amount_kopeks), @@ -333,29 +311,6 @@ async def process_platega_payment_amount( parse_mode="HTML", ) - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_platega_payment_by_id(db, local_payment_id) - if payment: - payment_metadata = dict(getattr(payment, "metadata_json", {}) or {}) - payment_metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await payment_module.update_platega_payment( - db, - payment=payment, - metadata=payment_metadata, - ) - except Exception as error: # pragma: no cover - диагностический лог - logger.warning("Не удалось сохранить данные сообщения Platega: %s", error) - - 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 a8cac0f8..0dcf0031 100644 --- a/app/handlers/balance/stars.py +++ b/app/handlers/balance/stars.py @@ -1,10 +1,11 @@ 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 +from app.keyboards.inline import get_back_keyboard, get_payment_methods_keyboard from app.localization.texts import get_texts from app.services.payment_service import PaymentService from app.states import BalanceStates @@ -21,11 +22,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 = ( @@ -34,10 +35,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 @@ -45,17 +46,12 @@ 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() @@ -69,48 +65,29 @@ 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")] ]) - - 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( + + await message.answer( f"⭐ Оплата через Telegram Stars\n\n" f"💰 Сумма: {texts.format_price(amount_kopeks)}\n" f"⭐ К оплате: {stars_amount} звезд\n" @@ -119,14 +96,9 @@ async def process_stars_payment_amount( reply_markup=keyboard, parse_mode="HTML" ) - - 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) - + + await state.clear() + except Exception as e: logger.error(f"Ошибка создания Stars invoice: {e}") - await message.answer("⚠️ Ошибка создания платежа") + await message.answer("⚠️ Ошибка создания платежа") \ No newline at end of file diff --git a/app/handlers/balance/tribute.py b/app/handlers/balance/tribute.py index f96538a5..e6d24015 100644 --- a/app/handlers/balance/tribute.py +++ b/app/handlers/balance/tribute.py @@ -13,55 +13,47 @@ logger = logging.getLogger(__name__) @error_handler async def start_tribute_payment( callback: types.CallbackQuery, - db_user: User, + db_user: User ): texts = get_texts(db_user.language) - + if not settings.TRIBUTE_ENABLED: await callback.answer("❌ Оплата картой временно недоступна", show_alert=True) return - + try: from app.services.tribute_service import TributeService - + tribute_service = TributeService(callback.bot) payment_url = await tribute_service.create_payment_link( user_id=db_user.telegram_id, amount_kopeks=0, - description="Пополнение баланса VPN", + description="Пополнение баланса VPN" ) - + if not payment_url: await callback.answer("❌ Ошибка создания платежа", show_alert=True) return - - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)], - [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")], - ] - ) - + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + await callback.message.edit_text( - f"💳 Пополнение банковской картой\n\n", - f"• Введите любую сумму от 100₽\n", - f"• Безопасная оплата через Tribute\n", - f"• Мгновенное зачисление на баланс\n", - f"• Принимаем карты Visa, MasterCard, МИР\n\n", - f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n", + f"💳 Пополнение банковской картой\n\n" + f"• Введите любую сумму от 100₽\n" + f"• Безопасная оплата через Tribute\n" + f"• Мгновенное зачисление на баланс\n" + f"• Принимаем карты Visa, MasterCard, МИР\n\n" + f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n" f"Нажмите кнопку для перехода к оплате:", reply_markup=keyboard, - parse_mode="HTML", + parse_mode="HTML" ) - - TributeService.remember_invoice_message( - db_user.telegram_id, - callback.message.chat.id, - callback.message.message_id, - ) - + except Exception as e: logger.error(f"Ошибка создания Tribute платежа: {e}") await callback.answer("❌ Ошибка создания платежа", show_alert=True) - - await callback.answer() + + await callback.answer() \ No newline at end of file diff --git a/app/handlers/balance/wata.py b/app/handlers/balance/wata.py index fdd2327e..259d559e 100644 --- a/app/handlers/balance/wata.py +++ b/app/handlers/balance/wata.py @@ -1,10 +1,8 @@ import logging -from datetime import datetime from typing import Dict from aiogram import types from aiogram.fsm.context import FSMContext -from sqlalchemy import update from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -58,11 +56,7 @@ async def start_wata_payment( ) await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data( - payment_method="wata", - wata_prompt_message_id=callback.message.message_id, - wata_prompt_chat_id=callback.message.chat.id, - ) + await state.update_data(payment_method="wata") await callback.answer() @@ -165,54 +159,12 @@ async def process_wata_payment_amount( support=settings.get_support_contact_display_html(), ) - state_data = await state.get_data() - prompt_message_id = state_data.get("wata_prompt_message_id") - prompt_chat_id = state_data.get("wata_prompt_chat_id", message.chat.id) - - try: - await message.delete() - except Exception as delete_error: # pragma: no cover - depends on bot rights - logger.warning("Не удалось удалить сообщение с суммой WATA: %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 - diagnostic - logger.warning( - "Не удалось удалить сообщение с запросом суммы WATA: %s", - delete_error, - ) - - invoice_message = await message.answer( + await message.answer( message_text, reply_markup=keyboard, parse_mode="HTML", ) - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_wata_payment_by_local_id(db, local_payment_id) - if payment: - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await db.execute( - update(payment.__class__) - .where(payment.__class__.id == payment.id) - .values(metadata_json=metadata, updated_at=datetime.utcnow()) - ) - await db.commit() - except Exception as error: # pragma: no cover - diagnostics - logger.warning("Не удалось сохранить сообщение WATA: %s", error) - - await state.update_data( - wata_invoice_message_id=invoice_message.message_id, - wata_invoice_chat_id=invoice_message.chat.id, - ) - await state.clear() logger.info( diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py index fb6a345c..607f62bd 100644 --- a/app/handlers/balance/yookassa.py +++ b/app/handlers/balance/yookassa.py @@ -1,9 +1,6 @@ import logging -from datetime import datetime - from aiogram import types from aiogram.fsm.context import FSMContext -from sqlalchemy import update from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -61,13 +58,9 @@ 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() @@ -115,13 +108,9 @@ 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() @@ -183,25 +172,7 @@ async def process_yookassa_payment_amount( [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] ]) - 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( + await message.answer( f"💳 Оплата банковской картой\n\n" f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" @@ -216,34 +187,9 @@ async def process_yookassa_payment_amount( reply_markup=keyboard, parse_mode="HTML" ) - - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_yookassa_payment_by_local_id( - db, payment_result["local_payment_id"] - ) - if payment: - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await db.execute( - update(payment.__class__) - .where(payment.__class__.id == payment.id) - .values(metadata_json=metadata, updated_at=datetime.utcnow()) - ) - await db.commit() - except Exception as error: # pragma: no cover - диагностический лог - logger.warning("Не удалось сохранить сообщение YooKassa: %s", error) - - 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']}") @@ -364,45 +310,27 @@ 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 += ( @@ -413,18 +341,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: # Используем метод отправки медиа-группы или фото с описанием - invoice_message = await message.answer_photo( + await message.answer_photo( photo=qr_photo, caption=message_text, reply_markup=keyboard, @@ -432,39 +360,12 @@ async def process_yookassa_sbp_payment_amount( ) else: # Если QR-код недоступен, отправляем обычное текстовое сообщение - invoice_message = await message.answer( + await message.answer( message_text, reply_markup=keyboard, parse_mode="HTML" ) - - try: - from app.services import payment_service as payment_module - - payment = await payment_module.get_yookassa_payment_by_local_id( - db, payment_result["local_payment_id"] - ) - if payment: - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - metadata["invoice_message"] = { - "chat_id": invoice_message.chat.id, - "message_id": invoice_message.message_id, - } - await db.execute( - update(payment.__class__) - .where(payment.__class__.id == payment.id) - .values(metadata_json=metadata, updated_at=datetime.utcnow()) - ) - await db.commit() - except Exception as error: # pragma: no cover - диагностический лог - logger.warning("Не удалось сохранить сообщение YooKassa (СБП): %s", error) - - 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 48356450..4b49f51f 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,9 +18,7 @@ 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_") @@ -37,7 +35,6 @@ 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: @@ -80,7 +77,6 @@ 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) @@ -110,27 +106,6 @@ 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, @@ -138,14 +113,7 @@ 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)) @@ -204,15 +172,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/localization/locales/en.json b/app/localization/locales/en.json index c347cddf..dc474150 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -1023,6 +1023,7 @@ "PAL24_INSTRUCTION_FOLLOW": "{step}. Follow the payment page instructions", "PAL24_PAYMENT_ERROR": "❌ Failed to create a PayPalych payment. Please try again later or contact support.", "PAL24_PAYMENT_INSTRUCTIONS": "🏦 PayPalych (SBP) payment\n\n💰 Amount: {amount}\n🆔 Invoice ID: {bill_id}\n\n📱 How to pay:\n1. Press ‘Pay with PayPalych (SBP)’\n2. Follow the system prompts\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}", + "PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)", "PAL24_SBP_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)", "PAL24_SELECT_PAYMENT_METHOD": "Choose a PayPalych payment method:", "PAL24_TOPUP_PROMPT": "🏦 PayPalych (SBP) payment\n\nEnter an amount between 100 and 1,000,000 ₽.\nThe payment is processed via the PayPalych Faster Payments System.", @@ -1060,8 +1061,12 @@ "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ Automated payment methods are temporarily unavailable. Contact support to top up your balance.", "PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "via CryptoBot", "PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Cryptocurrency", + "PAYMENT_METHOD_HELEKET_DESCRIPTION": "via Heleket", + "PAYMENT_METHOD_HELEKET_NAME": "🪙 Cryptocurrency (Heleket)", "PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via {mulenpay_name}", "PAYMENT_METHOD_MULENPAY_NAME": "💳 Bank card ({mulenpay_name})", + "PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System", + "PAYMENT_METHOD_PAL24_NAME": "🏦 SBP (PayPalych)", "PAYMENT_METHOD_PLATEGA_DESCRIPTION": "via Platega (cards + SBP)", "PAYMENT_METHOD_PLATEGA_NAME": "💳 Bank card (Platega)", "PAYMENT_METHOD_STARS_DESCRIPTION": "fast and convenient", @@ -1070,6 +1075,8 @@ "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Support team", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "via Tribute", "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Bank card", + "PAYMENT_METHOD_WATA_DESCRIPTION": "via WATA", + "PAYMENT_METHOD_WATA_NAME": "💳 Bank card (WATA)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "via YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Bank card", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "via YooKassa Fast Payment System", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index a77f47fb..da8b89f9 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -1043,6 +1043,7 @@ "PAL24_INSTRUCTION_FOLLOW": "{step}. Следуйте подсказкам платёжной системы", "PAL24_PAYMENT_ERROR": "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", "PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 Инструкция:\n1. Нажмите кнопку ‘Оплатить через PayPalych (СБП)’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}", + "PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)", "PAL24_SBP_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)", "PAL24_SELECT_PAYMENT_METHOD": "Выберите способ оплаты PayPalych:", "PAL24_TOPUP_PROMPT": "🏦 Оплата через PayPalych (СБП)\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.", @@ -1080,8 +1081,12 @@ "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.", "PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot", "PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Криптовалюта", + "PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket", + "PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)", "PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}", "PAYMENT_METHOD_MULENPAY_NAME": "💳 Банковская карта ({mulenpay_name})", + "PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей", + "PAYMENT_METHOD_PAL24_NAME": "🏦 СБП (PayPalych)", "PAYMENT_METHOD_PLATEGA_DESCRIPTION": "через Platega (карты + СБП)", "PAYMENT_METHOD_PLATEGA_NAME": "💳 Банковская карта (Platega)", "PAYMENT_METHOD_STARS_DESCRIPTION": "быстро и удобно", @@ -1090,6 +1095,8 @@ "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через поддержку", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute", "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банковская карта", + "PAYMENT_METHOD_WATA_DESCRIPTION": "через WATA", + "PAYMENT_METHOD_WATA_NAME": "💳 Банковская карта (WATA)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банковская карта", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему быстрых платежей YooKassa", diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json index 68897290..4c8fb4b9 100644 --- a/app/localization/locales/ua.json +++ b/app/localization/locales/ua.json @@ -1039,11 +1039,12 @@ "PAL24_INSTRUCTION_BUTTON": "{step}. Натисніть кнопку «{button}»", "PAL24_INSTRUCTION_COMPLETE": "{step}. Кошти зарахуються автоматично", "PAL24_INSTRUCTION_CONFIRM": "{step}. Підтвердіть переказ", - "PAL24_INSTRUCTION_FOLLOW": "{step}. Дотримуйтесь підказок платіжної системи", - "PAL24_PAYMENT_ERROR": "❌ Помилка створення платежу PayPalych. Спробуйте пізніше або зверніться до підтримки.", - "PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сума: {amount}\n🆔 ID рахунку: {bill_id}\n\n📱 Інструкція:\n1. Натисніть кнопку ‘Оплатити через PayPalych (СБП)’\n2. Дотримуйтесь підказок платіжної системи\n3. Підтвердіть переказ\n4. Кошти зарахуються автоматично\n\n❓ Якщо виникнуть проблеми, зверніться до {support}", - "PAL24_SBP_PAY_BUTTON": "🏦 Оплатити через PayPalych (СБП)", - "PAL24_SELECT_PAYMENT_METHOD": "Оберіть спосіб оплати PayPalych:", + "PAL24_INSTRUCTION_FOLLOW": "{step}. Дотримуйтесь підказок платіжної системи", + "PAL24_PAYMENT_ERROR": "❌ Помилка створення платежу PayPalych. Спробуйте пізніше або зверніться до підтримки.", + "PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сума: {amount}\n🆔 ID рахунку: {bill_id}\n\n📱 Інструкція:\n1. Натисніть кнопку ‘Оплатити через PayPalych (СБП)’\n2. Дотримуйтесь підказок платіжної системи\n3. Підтвердіть переказ\n4. Кошти зарахуються автоматично\n\n❓ Якщо виникнуть проблеми, зверніться до {support}", + "PAL24_PAY_BUTTON": "🏦 Оплатити через PayPalych (СБП)", + "PAL24_SBP_PAY_BUTTON": "🏦 Оплатити через PayPalych (СБП)", + "PAL24_SELECT_PAYMENT_METHOD": "Оберіть спосіб оплати PayPalych:", "PAL24_TOPUP_PROMPT": "🏦 Оплата через PayPalych (СБП)\n\nВведіть суму для поповнення від 100 до 1 000 000 ₽.\nОплата проходить через систему швидких платежів PayPalych.", "PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Способи оплати тимчасово недоступні", "PAYMENT_CARD_MULENPAY": "💳 Банківська картка ({mulenpay_name})", @@ -1079,18 +1080,24 @@ "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ На даний момент автоматичні способи оплати тимчасово недоступні. Для поповнення балансу зверніться до техпідтримки.", "PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot", "PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Криптовалюта", -"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}", -"PAYMENT_METHOD_MULENPAY_NAME": "💳 Банківська картка ({mulenpay_name})", -"PAYMENT_METHOD_PLATEGA_DESCRIPTION": "через Platega (картки + СБП)", -"PAYMENT_METHOD_PLATEGA_NAME": "💳 Банківська картка (Platega)", + "PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket", + "PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)", + "PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}", + "PAYMENT_METHOD_MULENPAY_NAME": "💳 Банківська картка ({mulenpay_name})", + "PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему швидких платежів", + "PAYMENT_METHOD_PAL24_NAME": "🏦 СБП (PayPalych)", + "PAYMENT_METHOD_PLATEGA_DESCRIPTION": "через Platega (картки + СБП)", + "PAYMENT_METHOD_PLATEGA_NAME": "💳 Банківська картка (Platega)", "PAYMENT_METHOD_STARS_DESCRIPTION": "швидко та зручно", "PAYMENT_METHOD_STARS_NAME": "⭐ Telegram Stars", "PAYMENT_METHOD_SUPPORT_DESCRIPTION": "інші способи", "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через підтримку", -"PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute", -"PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банківська картка", -"PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa", -"PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банківська картка", + "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute", + "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банківська картка", + "PAYMENT_METHOD_WATA_DESCRIPTION": "через WATA", + "PAYMENT_METHOD_WATA_NAME": "💳 Банківська картка (WATA)", + "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa", + "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банківська картка", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему швидких платежів YooKassa", "PAYMENT_METHOD_YOOKASSA_SBP_NAME": "🏦 СБП (YooKassa)", "PAYMENT_HELEKET_MARKUP_LABEL": "Націнка провайдера", diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json index 3c0b906f..84210ac8 100644 --- a/app/localization/locales/zh.json +++ b/app/localization/locales/zh.json @@ -1042,6 +1042,7 @@ "PAL24_INSTRUCTION_FOLLOW":"{step}.按照支付系统提示操作", "PAL24_PAYMENT_ERROR":"❌创建PayPalych付款失败。请稍后再试或联系支持。", "PAL24_PAYMENT_INSTRUCTIONS":"🏦通过PayPalych(SBP)付款\n\n💰金额:{amount}\n🆔账单ID:{bill_id}\n\n📱说明:\n1.点击“通过PayPalych(SBP)付款”按钮\n2.按照支付系统提示操作\n3.确认转账\n4.资金将自动到账\n\n❓如果遇到问题,请联系{support}", +"PAL24_PAY_BUTTON":"🏦通过PayPalych(SBP)付款", "PAL24_SBP_PAY_BUTTON":"🏦通过PayPalych(SBP)付款", "PAL24_SELECT_PAYMENT_METHOD":"请选择PayPalych支付方式:", "PAL24_TOPUP_PROMPT":"🏦通过PayPalych(SBP)付款\n\n请输入充值金额,范围100至1000000₽。\n付款通过PayPalych快速支付系统进行。", @@ -1079,8 +1080,12 @@ "PAYMENT_METHODS_UNAVAILABLE_ALERT":"⚠️目前自动支付方式暂时不可用。如需充值余额,请联系技术支持。", "PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION":"通过CryptoBot", "PAYMENT_METHOD_CRYPTOBOT_NAME":"🪙加密货币", +"PAYMENT_METHOD_HELEKET_DESCRIPTION":"通过Heleket", +"PAYMENT_METHOD_HELEKET_NAME":"🪙加密货币(Heleket)", "PAYMENT_METHOD_MULENPAY_DESCRIPTION":"通过{mulenpay_name}", "PAYMENT_METHOD_MULENPAY_NAME":"💳银行卡({mulenpay_name})", +"PAYMENT_METHOD_PAL24_DESCRIPTION":"通过快速支付系统", +"PAYMENT_METHOD_PAL24_NAME":"🏦SBP(PayPalych)", "PAYMENT_METHOD_PLATEGA_DESCRIPTION":"通过Platega(银行卡+SBP)", "PAYMENT_METHOD_PLATEGA_NAME":"💳银行卡(Platega)", "PAYMENT_METHOD_STARS_DESCRIPTION":"快速便捷", @@ -1089,6 +1094,8 @@ "PAYMENT_METHOD_SUPPORT_NAME":"🛠️通过支持", "PAYMENT_METHOD_TRIBUTE_DESCRIPTION":"通过Tribute", "PAYMENT_METHOD_TRIBUTE_NAME":"💳银行卡", +"PAYMENT_METHOD_WATA_DESCRIPTION":"通过WATA", +"PAYMENT_METHOD_WATA_NAME":"💳银行卡(WATA)", "PAYMENT_METHOD_YOOKASSA_DESCRIPTION":"通过YooKassa", "PAYMENT_METHOD_YOOKASSA_NAME":"💳银行卡", "PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION":"通过YooKassa快速支付系统", diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py index 27d24a2b..c032f990 100644 --- a/app/services/payment/heleket.py +++ b/app/services/payment/heleket.py @@ -255,42 +255,6 @@ class HeleketPaymentMixin: if updated_payment is None: return None - metadata = dict(getattr(updated_payment, "metadata_json", {}) or {}) - invoice_message = metadata.get("invoice_message") or {} - invoice_message_removed = False - - if getattr(self, "bot", None) and invoice_message: - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if chat_id and message_id: - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as delete_error: # pragma: no cover - depends on rights - logger.warning( - "Не удалось удалить счёт Heleket %s: %s", - message_id, - delete_error, - ) - else: - metadata.pop("invoice_message", None) - invoice_message_removed = True - - if invoice_message_removed: - try: - from app.database.crud import heleket as heleket_crud - - await heleket_crud.update_heleket_payment( - db, - updated_payment.uuid, - metadata=metadata, - ) - updated_payment.metadata_json = metadata - except Exception as error: # pragma: no cover - diagnostics - logger.warning( - "Не удалось обновить метаданные Heleket после удаления счёта: %s", - error, - ) - if updated_payment.transaction_id: logger.info( "Heleket платеж %s уже связан с транзакцией %s", diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py index 27dea36d..7b4ffcf3 100644 --- a/app/services/payment/mulenpay.py +++ b/app/services/payment/mulenpay.py @@ -182,43 +182,7 @@ class MulenPayPaymentMixin: ) return False - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - invoice_message = metadata.get("invoice_message") or {} - - invoice_message_removed = False - - if getattr(self, "bot", None): - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if chat_id and message_id: - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as delete_error: # pragma: no cover - depends on bot rights - logger.warning( - "Не удалось удалить %s счёт %s: %s", - display_name, - message_id, - delete_error, - ) - else: - metadata.pop("invoice_message", None) - invoice_message_removed = True - if payment.is_paid: - if invoice_message_removed: - try: - await payment_module.update_mulenpay_payment_metadata( - db, - payment=payment, - metadata=metadata, - ) - except Exception as error: # pragma: no cover - diagnostics - logger.warning( - "Не удалось обновить метаданные %s после удаления счёта: %s", - display_name, - error, - ) - logger.info( "%s платеж %s уже обработан, игнорируем повторный callback", display_name, @@ -233,7 +197,6 @@ class MulenPayPaymentMixin: status="success", callback_payload=callback_data, mulen_payment_id=mulen_payment_id_int, - metadata=metadata, ) if payment.transaction_id: diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 460e0e31..6dbe3dc5 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -333,41 +333,6 @@ class Pal24PaymentMixin: payment_module = import_module("app.services.payment_service") - metadata = dict(getattr(payment, "metadata_json", {}) or {}) - invoice_message = metadata.get("invoice_message") or {} - invoice_message_removed = False - - if getattr(self, "bot", None) and invoice_message: - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if chat_id and message_id: - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as delete_error: # pragma: no cover - depends on rights - logger.warning( - "Не удалось удалить счёт PayPalych %s: %s", - message_id, - delete_error, - ) - else: - metadata.pop("invoice_message", None) - invoice_message_removed = True - - if invoice_message_removed: - try: - await payment_module.update_pal24_payment_status( - db, - payment, - status=payment.status, - metadata=metadata, - ) - payment.metadata_json = metadata - except Exception as error: # pragma: no cover - diagnostics - logger.warning( - "Не удалось обновить метаданные PayPalych после удаления счёта: %s", - error, - ) - if payment.transaction_id: logger.info( "Pal24 платеж %s уже привязан к транзакции (trigger=%s)", diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py index 2d08997a..fc7fe4d8 100644 --- a/app/services/payment/platega.py +++ b/app/services/payment/platega.py @@ -306,22 +306,6 @@ class PlategaPaymentMixin: metadata = dict(getattr(payment, "metadata_json", {}) or {}) balance_already_credited = bool(metadata.get("balance_credited")) - invoice_message = metadata.get("invoice_message") or {} - if getattr(self, "bot", None): - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if chat_id and message_id: - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as delete_error: # pragma: no cover - depends on bot rights - logger.warning( - "Не удалось удалить Platega счёт %s: %s", - message_id, - delete_error, - ) - else: - metadata.pop("invoice_message", None) - if payment.transaction_id: logger.info( "Platega платеж %s уже связан с транзакцией %s", diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index 3f1022d7..b7fd5942 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -515,6 +515,36 @@ 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 diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py index 7072d096..d8bc789a 100644 --- a/app/services/payment/wata.py +++ b/app/services/payment/wata.py @@ -415,25 +415,6 @@ class WataPaymentMixin: if not paid_at and getattr(payment, "paid_at", None): paid_at = payment.paid_at existing_metadata = dict(getattr(payment, "metadata_json", {}) or {}) - - invoice_message = existing_metadata.get("invoice_message") or {} - invoice_message_removed = False - if getattr(self, "bot", None) and invoice_message: - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if chat_id and message_id: - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as delete_error: # pragma: no cover - depends on rights - logger.warning( - "Не удалось удалить счёт WATA %s: %s", - message_id, - delete_error, - ) - else: - invoice_message_removed = True - existing_metadata.pop("invoice_message", None) - existing_metadata["transaction"] = transaction_payload await payment_module.update_wata_payment_status( diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 465664a6..181ab94d 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -383,19 +383,11 @@ class YooKassaPaymentMixin: payment_module = import_module("app.services.payment_service") # Проверяем, не обрабатывается ли уже этот платеж (защита от дублирования) - get_transaction_by_external_id = getattr( - payment_module, "get_transaction_by_external_id", None + existing_transaction = await payment_module.get_transaction_by_external_id( # type: ignore[attr-defined] + db, + payment.yookassa_payment_id, + PaymentMethod.YOOKASSA, ) - existing_transaction = None - if get_transaction_by_external_id: - try: - existing_transaction = await get_transaction_by_external_id( # type: ignore[attr-defined] - db, - payment.yookassa_payment_id, - PaymentMethod.YOOKASSA, - ) - except AttributeError: - logger.debug("get_transaction_by_external_id недоступен, пропускаем проверку дубликатов") if existing_transaction: # Если транзакция уже существует, просто завершаем обработку @@ -445,22 +437,6 @@ class YooKassaPaymentMixin: except Exception as parse_error: logger.error(f"Ошибка парсинга метаданных платежа: {parse_error}") - invoice_message = payment_metadata.get("invoice_message") or {} - if getattr(self, "bot", None): - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if chat_id and message_id: - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as delete_error: # pragma: no cover - depends on bot rights - logger.warning( - "Не удалось удалить сообщение YooKassa %s: %s", - message_id, - delete_error, - ) - else: - payment_metadata.pop("invoice_message", None) - processing_completed = bool(payment_metadata.get("processing_completed")) transaction = None @@ -496,20 +472,11 @@ class YooKassaPaymentMixin: ) if transaction is None: - get_transaction_by_external_id = getattr( - payment_module, "get_transaction_by_external_id", None + existing_transaction = await payment_module.get_transaction_by_external_id( # type: ignore[attr-defined] + db, + payment.yookassa_payment_id, + PaymentMethod.YOOKASSA, ) - existing_transaction = None - - if get_transaction_by_external_id: - try: - existing_transaction = await get_transaction_by_external_id( # type: ignore[attr-defined] - db, - payment.yookassa_payment_id, - PaymentMethod.YOOKASSA, - ) - except AttributeError: - logger.debug("get_transaction_by_external_id недоступен, пропускаем проверку дубликатов") if existing_transaction: # Если транзакция уже существует, пропускаем обработку diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 949f7b23..9f417d73 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -112,11 +112,6 @@ async def update_mulenpay_payment_status(*args, **kwargs): return await mulenpay_crud.update_mulenpay_payment_status(*args, **kwargs) -async def update_mulenpay_payment_metadata(*args, **kwargs): - mulenpay_crud = import_module("app.database.crud.mulenpay") - return await mulenpay_crud.update_mulenpay_payment_metadata(*args, **kwargs) - - async def link_mulenpay_payment_to_transaction(*args, **kwargs): mulenpay_crud = import_module("app.database.crud.mulenpay") return await mulenpay_crud.link_mulenpay_payment_to_transaction(*args, **kwargs) diff --git a/app/services/tribute_service.py b/app/services/tribute_service.py index 3a6d8c07..bef09a91 100644 --- a/app/services/tribute_service.py +++ b/app/services/tribute_service.py @@ -23,30 +23,10 @@ logger = logging.getLogger(__name__) class TributeService: - _invoice_messages: Dict[int, Dict[str, int]] = {} def __init__(self, bot: Bot): self.bot = bot self.tribute_api = TributeAPI() - - @classmethod - def remember_invoice_message(cls, user_id: int, chat_id: int, message_id: int) -> None: - cls._invoice_messages[user_id] = {"chat_id": chat_id, "message_id": message_id} - - async def _cleanup_invoice_message(self, user_id: int) -> None: - invoice_message = self._invoice_messages.pop(user_id, None) - if not invoice_message or not getattr(self, "bot", None): - return - - chat_id = invoice_message.get("chat_id") - message_id = invoice_message.get("message_id") - if not chat_id or not message_id: - return - - try: - await self.bot.delete_message(chat_id, message_id) - except Exception as error: # pragma: no cover - depends on bot rights - logger.warning("Не удалось удалить Tribute счёт %s: %s", message_id, error) async def create_payment_link( self, @@ -194,8 +174,7 @@ class TributeService: ) except Exception as e: logger.error(f"Ошибка отправки уведомления о Tribute пополнении: {e}") - - await self._cleanup_invoice_message(user_telegram_id) + await self._send_success_notification(user_telegram_id, amount_kopeks) logger.info(f"🎉 Успешно обработан Tribute платеж: {amount_kopeks/100}₽ для пользователя {user_telegram_id}") diff --git a/tests/external/test_yookassa_webhook.py b/tests/external/test_yookassa_webhook.py index 3ae1f6ff..03b98927 100644 --- a/tests/external/test_yookassa_webhook.py +++ b/tests/external/test_yookassa_webhook.py @@ -134,9 +134,9 @@ async def test_handle_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None: status = response.status text = await response.text() - assert status == 400 - assert text == "No payment id" - process_mock.assert_not_awaited() + assert status == 200 + assert text == "OK" + process_mock.assert_awaited_once() @pytest.mark.asyncio @@ -160,9 +160,9 @@ async def test_handle_webhook_trusts_cf_connecting_ip(monkeypatch: pytest.Monkey status = response.status text = await response.text() - assert status == 400 - assert text == "No payment id" - process_mock.assert_not_awaited() + assert status == 200 + assert text == "OK" + process_mock.assert_awaited_once() @pytest.mark.asyncio @@ -184,9 +184,9 @@ async def test_handle_webhook_with_optional_signature(monkeypatch: pytest.Monkey status = response.status text = await response.text() - assert status == 400 - assert text == "No payment id" - process_mock.assert_not_awaited() + assert status == 200 + assert text == "OK" + process_mock.assert_awaited_once() @pytest.mark.asyncio