import hashlib from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web from robokassa import HashAlgorithm, Robokassa from sqlalchemy.ext.asyncio import AsyncSession from config import ( ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE, ) from database import ( add_payment, add_user, async_session_maker, check_user_exists, get_key_count, get_temporary_data, update_balance, ) from handlers.buttons import BACK, PAY_2 from handlers.payments.utils import send_payment_success_notification from handlers.texts import DEFAULT_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS from handlers.utils import edit_or_send_message from logger import logger router = Router() class ReplenishBalanceState(StatesGroup): choosing_amount_robokassa = State() waiting_for_payment_confirmation_robokassa = State() if ROBOKASSA_ENABLE: robokassa = Robokassa( merchant_login=ROBOKASSA_LOGIN, password1=ROBOKASSA_PASSWORD1, password2=ROBOKASSA_PASSWORD2, algorithm=HashAlgorithm.md5, is_test=ROBOKASSA_TEST_MODE, ) logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN) def generate_payment_link(amount, inv_id, description, tg_id): """Генерация ссылки на оплату.""" logger.debug( f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}" ) payment_link = robokassa._payment.link.generate_by_script( out_sum=amount, inv_id=inv_id, description="пополнение баланса", id=f"{tg_id}", ) logger.info(f"Generated payment link: {payment_link}") return payment_link @router.callback_query(F.data == "pay_robokassa") async def process_callback_pay_robokassa( callback_query: types.CallbackQuery, state: FSMContext, session: Any ): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', ), InlineKeyboardButton( text=PAYMENT_OPTIONS[i + 1]["text"], callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', ), ) else: builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', ) ) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) key_count = await get_key_count(session, tg_id) if key_count == 0: exists = await check_user_exists(session, tg_id) if not exists: from_user = callback_query.from_user await add_user( tg_id=from_user.id, username=from_user.username, first_name=from_user.first_name, last_name=from_user.last_name, language_code=from_user.language_code, is_bot=from_user.is_bot, session=session, ) logger.info(f"[DB] Новый пользователь {tg_id} создан через Robokassa.") await callback_query.message.delete() new_message = await callback_query.message.answer( text="Выберите сумму пополнения:", reply_markup=builder.as_markup(), ) await state.update_data( message_id=new_message.message_id, chat_id=new_message.chat.id ) await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) logger.info(f"Displayed amount selection for user {tg_id}.") @router.callback_query(F.data.startswith("robokassa_amount|")) async def process_amount_selection( callback_query: types.CallbackQuery, state: FSMContext ): logger.info(f"Получены данные callback_data: {callback_query.data}") data = callback_query.data.split("|") if len(data) != 3 or data[1] != "amount": logger.error("Ошибка: callback_data не соответствует формату.") await edit_or_send_message( target_message=callback_query.message, text="Ошибка: данные повреждены.", reply_markup=types.InlineKeyboardMarkup(), force_text=True, ) return amount_str = data[2] try: amount = int(amount_str) if amount <= 0: raise ValueError("Сумма должна быть положительным числом.") except ValueError as e: logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}") await edit_or_send_message( target_message=callback_query.message, text="Некорректная сумма.", reply_markup=types.InlineKeyboardMarkup(), force_text=True, ) return await state.update_data(amount=amount) logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.") inv_id = 0 tg_id = callback_query.message.chat.id payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id) logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}") confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text=PAY_2, url=payment_url)], [InlineKeyboardButton(text=BACK, callback_data="pay_robokassa")], ] ) await edit_or_send_message( target_message=callback_query.message, text=DEFAULT_PAYMENT_MESSAGE.format(amount=amount), reply_markup=confirm_keyboard, force_text=True, ) logger.info(f"Payment link sent to user {callback_query.message.chat.id}.") async def robokassa_webhook(request: web.Request): try: params = await request.post() logger.info(f"Received webhook params: {params}") amount = params.get("OutSum") inv_id = params.get("InvId") shp_id = params.get("shp_id") signature_value = params.get("SignatureValue") logger.info( f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}" ) if not check_payment_signature(params): logger.error("Неверная подпись или данные запроса.") return web.Response(status=400) if not amount or not inv_id or not shp_id: logger.error("Отсутствуют обязательные параметры.") return web.Response(status=400) tg_id = shp_id logger.info(f"Processing payment for user {tg_id} with amount {amount}.") async with async_session_maker() as session: await update_balance(session, int(tg_id), float(amount)) await send_payment_success_notification(tg_id, float(amount), session) await add_payment(session, int(tg_id), float(amount), "robokassa") logger.info(f"Payment successful. Balance updated for user {tg_id}.") return web.Response(text=f"OK{inv_id}") except Exception as e: logger.error(f"Error processing webhook: {e}") return web.Response(status=500) def check_payment_signature(params): """Проверка подписи запроса от Robokassa с учетом shp_id.""" out_sum = params.get("OutSum") inv_id = params.get("InvId") signature_value = params.get("SignatureValue") shp_id = params.get("shp_id") signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}:shp_id={shp_id}" logger.info(f"Signature string before hashing: {signature_string}") expected_signature = ( hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() ) logger.info(f"Expected signature: {expected_signature}") logger.info(f"Received signature: {signature_value}") return signature_value.upper() == expected_signature.upper() @router.callback_query(F.data == "enter_custom_amount_robokassa") async def process_custom_amount_selection( callback_query: types.CallbackQuery, state: FSMContext ): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_robokassa")) await edit_or_send_message( target_message=callback_query.message, text=ENTER_SUM, reply_markup=builder.as_markup(), force_text=True, ) await state.set_state( ReplenishBalanceState.waiting_for_payment_confirmation_robokassa ) @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) async def handle_custom_amount_input( message: types.Message | types.CallbackQuery, state: FSMContext = None, session: AsyncSession = None, ): if isinstance(message, types.CallbackQuery): tg_id = message.message.chat.id target_message = message.message else: tg_id = message.chat.id target_message = message logger.info(f"User {tg_id} initiated payment through ROBOKASSA") inv_id = 0 try: user_data = await get_temporary_data(session, tg_id) if not user_data: await edit_or_send_message( target_message=target_message, text="Данные для оплаты не найдены. Попробуйте снова.", reply_markup=types.InlineKeyboardMarkup(), ) return state_type = user_data["state"] amount = user_data["data"].get("required_amount", 0) if amount <= 0: await edit_or_send_message( target_message=target_message, text="Недостаточная сумма для пополнения.", reply_markup=types.InlineKeyboardMarkup(), ) return payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id) logger.info(f"Generated payment link for user {tg_id}: {payment_url}") builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="💳 Оплатить", url=payment_url)) builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_robokassa")) if state_type == "waiting_for_payment": message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:" elif state_type == "waiting_for_renewal_payment": message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:" else: await edit_or_send_message( target_message=target_message, text="Некорректное состояние данных. Попробуйте снова.", reply_markup=types.InlineKeyboardMarkup(), ) return await edit_or_send_message( target_message=target_message, text=message_text, reply_markup=builder.as_markup(), ) if isinstance(state, FSMContext): await state.clear() except Exception as e: logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}") await edit_or_send_message( target_message=target_message, text="Произошла ошибка при создании платежа. Попробуйте позже.", reply_markup=types.InlineKeyboardMarkup(), )