diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index c2559faa..df91e973 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -1,5 +1,4 @@ -import uuid -from urllib.parse import urlencode +import hashlib from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -7,12 +6,13 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web +from loguru import logger +from robokassa import HashAlgorithm, Robokassa from bot import bot from config import ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE from database import add_connection, check_connection_exists, get_key_count, update_balance from handlers.texts import PAYMENT_OPTIONS -from logger import logger router = Router() @@ -20,44 +20,45 @@ router = Router() class ReplenishBalanceState(StatesGroup): choosing_amount_robokassa = State() waiting_for_payment_confirmation_robokassa = State() - entering_custom_amount_robokassa = State() -def calculate_signature(*args) -> str: - """Создание подписи SHA-256 для Робокассы.""" - import hashlib +robokassa = Robokassa( + merchant_login=ROBOKASSA_LOGIN, + password1=ROBOKASSA_PASSWORD1, + password2=ROBOKASSA_PASSWORD2, + algorithm=HashAlgorithm.md5, + is_test=ROBOKASSA_TEST_MODE, +) - data = ":".join(map(str, args)) - return hashlib.sha256(data.encode("utf-8")).hexdigest() +logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN) -def generate_payment_link( - merchant_login, password1, cost, inv_id, description, is_test -): - """Генерация ссылки для оплаты через Робокассу.""" - signature = calculate_signature(merchant_login, cost, inv_id, password1) - payment_url = "https://auth.robokassa.ru/Merchant/Index.aspx" - params = { - "MerchantLogin": merchant_login, - "OutSum": cost, - "InvId": inv_id, - "Description": description, - "SignatureValue": signature, - "IsTest": int(is_test), - } - return f"{payment_url}?{urlencode(params)}" +def generate_payment_link(amount, inv_id, description): + """Генерация ссылки на оплату.""" + 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="description", + ) + logger.info(f"Generated payment link: {payment_link}") + return payment_link async def send_message_with_deletion( chat_id, text, reply_markup=None, state=None, message_key="last_message_id" ): - """Отправка сообщения с удалением предыдущего.""" if state: try: state_data = await state.get_data() previous_message_id = state_data.get(message_key) if previous_message_id: + logger.debug( + f"Deleting previous message with ID: {previous_message_id}" + ) await bot.delete_message( chat_id=chat_id, message_id=previous_message_id ) @@ -67,6 +68,7 @@ async def send_message_with_deletion( ) await state.update_data({message_key: sent_message.message_id}) + logger.debug(f"Sent new message with ID: {sent_message.message_id}") except Exception as e: logger.error(f"Ошибка при удалении/отправке сообщения: {e}") return None @@ -79,6 +81,7 @@ async def process_callback_pay_robokassa( callback_query: types.CallbackQuery, state: FSMContext ): tg_id = callback_query.from_user.id + logger.info(f"User {tg_id} initiated Robokassa payment.") builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): @@ -86,18 +89,18 @@ async def process_callback_pay_robokassa( builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], - callback_data=f'robokassa_{PAYMENT_OPTIONS[i]["callback_data"]}', + callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', ), InlineKeyboardButton( text=PAYMENT_OPTIONS[i + 1]["text"], - callback_data=f'robokassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]}', + callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', ), ) else: builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], - callback_data=f'robokassa_{PAYMENT_OPTIONS[i]["callback_data"]}', + callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', ) ) builder.row( @@ -113,11 +116,13 @@ async def process_callback_pay_robokassa( exists = await check_connection_exists(tg_id) if not exists: await add_connection(tg_id, balance=0.0, trial=0) + logger.info(f"Created new connection for user {tg_id} with balance 0.0.") try: await bot.delete_message( chat_id=tg_id, message_id=callback_query.message.message_id ) + logger.debug(f"Deleted message with ID: {callback_query.message.message_id}") except Exception as e: logger.error(f"Не удалось удалить сообщение: {e}") @@ -127,6 +132,7 @@ async def process_callback_pay_robokassa( reply_markup=builder.as_markup(), ) await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) + logger.info(f"Displayed amount selection for user {tg_id}.") await callback_query.answer() @@ -134,39 +140,50 @@ async def process_callback_pay_robokassa( async def process_amount_selection( callback_query: types.CallbackQuery, state: FSMContext ): - data = callback_query.data.split("|", 1) - if len(data) != 2: + 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 send_message_with_deletion( - callback_query.from_user.id, - "Неверные данные для выбора суммы.", + chat_id=callback_query.from_user.id, + text="Неверные данные для выбора суммы.", state=state, ) + await callback_query.answer("Ошибка: данные повреждены.") return - amount_str = data[1] + amount_str = data[2] try: amount = int(amount_str) - except ValueError: + if amount <= 0: + raise ValueError("Сумма должна быть положительным числом.") + except ValueError as e: + logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}") await send_message_with_deletion( - callback_query.from_user.id, "Некорректная сумма.", state=state + chat_id=callback_query.from_user.id, + text="Некорректная сумма. Попробуйте снова.", + state=state, ) + await callback_query.answer("Некорректная сумма.") return await state.update_data(amount=amount) - inv_id = uuid.uuid4().hex[:8] + logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.") + + tg_id = callback_query.from_user.id payment_url = generate_payment_link( - ROBOKASSA_LOGIN, - ROBOKASSA_PASSWORD1, amount, - inv_id, + tg_id, "Пополнение баланса", - ROBOKASSA_TEST_MODE, ) + logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}") + confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="Оплатить", url=payment_url)], - [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")], ] ) @@ -174,31 +191,58 @@ async def process_amount_selection( text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", reply_markup=confirm_keyboard, ) + logger.info(f"Payment link sent to user {callback_query.from_user.id}.") await callback_query.answer() async def robokassa_webhook(request): - """Обработка уведомлений от Робокассы.""" - params = dict(await request.post()) - logger.debug(f"Webhook event received: {params}") + """Обработка webhook-уведомлений от Robokassa.""" + try: + params = await request.post() + logger.debug(f"Received webhook params: {params}") - received_signature = params.get("SignatureValue") - amount = params.get("OutSum") - inv_id = params.get("InvId") + if not check_payment_signature(params): + logger.error("Неверная подпись или данные запроса.") + return web.Response(status=400) - signature = calculate_signature(amount, inv_id, ROBOKASSA_PASSWORD2) + amount = params.get("OutSum") + inv_id = params.get("InvId") - if signature.lower() == received_signature.lower(): - user_id = params.get("Shp_user_id") - await update_balance(int(user_id), float(amount)) - await send_payment_success_notification(user_id, float(amount)) + if not amount or not inv_id: + logger.error("Отсутствуют обязательные параметры.") + return web.Response(status=400) + + tg_id = inv_id + + await update_balance(int(tg_id), float(amount)) + await send_payment_success_notification(tg_id, float(amount)) + + logger.info(f"Payment successful. Balance updated for user {tg_id}.") return web.Response(text=f"OK{inv_id}") - else: - return web.Response(status=400) + except Exception as e: + logger.error(f"Error processing webhook: {e}") + return web.Response(status=500) + + +def check_payment_signature(params): + """Проверка подписи запроса от Robokassa.""" + out_sum = params.get("OutSum") + inv_id = params.get("InvId") + signature_value = params.get("SignatureValue") + + signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}" + + expected_signature = ( + hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() + ) + + logger.debug(f"Expected signature: {expected_signature}") + logger.debug(f"Received signature: {signature_value}") + + return signature_value == expected_signature async def send_payment_success_notification(user_id: int, amount: float): - """Уведомление о успешном пополнении.""" builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile") @@ -209,3 +253,59 @@ async def send_payment_success_notification(user_id: int, amount: float): text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", reply_markup=builder.as_markup(), ) + logger.info(f"Sent payment success notification to user {user_id}.") + + +@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.from_user.id + logger.info(f"User {tg_id} chose to enter a custom amount.") + + await callback_query.message.edit_text( + text="Пожалуйста, введите сумму пополнения в рублях (например, 150):" + ) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_robokassa + ) + await callback_query.answer() + + +@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) +async def handle_custom_amount_input(message: types.Message, state: FSMContext): + tg_id = message.from_user.id + logger.info(f"User {tg_id} entered custom amount: {message.text}") + + try: + amount = int(message.text) + if amount <= 0: + raise ValueError("Сумма должна быть положительным числом.") + + await state.update_data(amount=amount) + + payment_url = generate_payment_link( + amount, + tg_id, + "Пополнение баланса", + ) + + logger.info(f"Generated payment link for user {tg_id}: {payment_url}") + + confirm_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Оплатить", url=payment_url)], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")], + ] + ) + + await message.answer( + text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", + reply_markup=confirm_keyboard, + ) + await state.clear() + except ValueError as e: + logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}") + await message.answer( + text="Введите корректную сумму в рублях (целое положительное число)." + )