From c12e61fb41d4f2240ec662b7cf9ade372cbdd88c Mon Sep 17 00:00:00 2001 From: Boris Kovalskii <36034823+JustYay@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:42:23 +1000 Subject: [PATCH] Clear code --- handlers/pay.py | 14 ++++---- handlers/payments/kassai.py | 56 +++++++------------------------ web/kassai_payment.py | 66 ++++++++----------------------------- 3 files changed, 31 insertions(+), 105 deletions(-) diff --git a/handlers/pay.py b/handlers/pay.py index 152b7b59..6055e957 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -65,6 +65,10 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: payment_handlers.append(process_callback_pay_yookassa) if YOOMONEY_ENABLE: payment_handlers.append(process_callback_pay_yoomoney) + if KASSAI_ENABLE: + payment_handlers.append(process_callback_pay_kassai) + if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE: + payment_handlers.append(process_callback_pay_wata) if CRYPTO_BOT_ENABLE: payment_handlers.append(process_callback_pay_cryptobot) if STARS_ENABLE: @@ -73,10 +77,6 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: payment_handlers.append(process_callback_pay_robokassa) if FREEKASSA_ENABLE: payment_handlers.append(process_callback_pay_freekassa) - if KASSAI_ENABLE: - payment_handlers.append(process_callback_pay_kassai) - if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE: - payment_handlers.append(process_callback_pay_wata) if len(payment_handlers) == 1: await callback_query.answer() @@ -88,6 +88,9 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: builder.row(InlineKeyboardButton(text=YOOKASSA, callback_data="pay_yookassa")) if YOOMONEY_ENABLE: builder.row(InlineKeyboardButton(text=YOOMONEY, callback_data="pay_yoomoney")) + if KASSAI_ENABLE: + builder.row(InlineKeyboardButton(text=KASSAI_CARDS, callback_data="pay_kassai_cards")) + builder.row(InlineKeyboardButton(text=KASSAI_SBP, callback_data="pay_kassai_sbp")) if CRYPTO_BOT_ENABLE: builder.row(InlineKeyboardButton(text=CRYPTOBOT, callback_data="pay_cryptobot")) if STARS_ENABLE: @@ -96,9 +99,6 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: builder.row(InlineKeyboardButton(text=ROBOKASSA, callback_data="pay_robokassa")) if FREEKASSA_ENABLE: builder.row(InlineKeyboardButton(text=FREEKASSA, callback_data="pay_freekassa")) - if KASSAI_ENABLE: - builder.row(InlineKeyboardButton(text=KASSAI_CARDS, callback_data="pay_kassai_cards")) - builder.row(InlineKeyboardButton(text=KASSAI_SBP, callback_data="pay_kassai_sbp")) if WATA_RU_ENABLE: builder.row(InlineKeyboardButton(text=WATA_RU, callback_data="pay_wata_ru")) if WATA_SBP_ENABLE: diff --git a/handlers/payments/kassai.py b/handlers/payments/kassai.py index 11d4408e..a07180bc 100644 --- a/handlers/payments/kassai.py +++ b/handlers/payments/kassai.py @@ -226,7 +226,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext): async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): parts = callback_query.data.split("|") method_name = parts[1] - # Теперь сумма приходит прямо в parts[2] amount_str = parts[2] method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None) @@ -276,101 +275,72 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) -> """ Создание заказа в KassaAI и получение ссылки на оплату """ - # Генерируем уникальный nonce (timestamp в секундах) nonce = int(time.time()) - - # Генерируем уникальный paymentId для отслеживания заказа unique_payment_id = f"{nonce}_{tg_id}" - - # Формируем URL с query параметрами (включая paymentId) url = f"https://api.fk.life/v1/orders/create?paymentId={unique_payment_id}" headers = { "Content-Type": "application/json", } - # Формируем email клиента (тг_id@домен) client_email = f"{tg_id}@{KASSAI_DOMAIN}" + client_ip = KASSAI_IP - # Используем IP сервера из конфигурации - client_ip = KASSAI_IP # IP сервера - - # Подготавливаем ТОЛЬКО обязательные данные для подписи и запроса - # Согласно документации: shopId, nonce, i, email, ip, amount, currency data_for_signature = { "shopId": KASSAI_SHOP_ID, "nonce": nonce, "i": method["method"], "email": client_email, "ip": client_ip, - "amount": int(amount), # Целое число + "amount": int(amount), "currency": "RUB" } - # Формируем подпись согласно документации PHP примера: - # ksort($data); hash_hmac('sha256', implode('|', $data), $api_key) sorted_keys = sorted(data_for_signature.keys()) values = [str(data_for_signature[key]) for key in sorted_keys] sign_string = "|".join(values) - # Отладочная информация - logger.info(f"KassaAI signature debug (correct algorithm):") - logger.info(f"Sorted keys: {sorted_keys}") - logger.info(f"Values: {values}") - logger.info(f"Sign string: {sign_string}") - logger.info(f"API key length: {len(KASSAI_API_KEY)}") - logger.info(f"Sending to URL: {url}") - - # HMAC SHA256 как в PHP примере signature = hmac.new( KASSAI_API_KEY.encode('utf-8'), sign_string.encode('utf-8'), hashlib.sha256 ).hexdigest() - logger.info(f"Generated signature: {signature}") - - # Данные для отправки (только обязательные поля + подпись) data = data_for_signature.copy() data["signature"] = signature - logger.info(f"Sending data: {data}") - try: async with aiohttp.ClientSession() as session: async with session.post(url, headers=headers, json=data, timeout=60) as resp: if resp.status == 200: try: resp_json = await resp.json() - logger.info(f"KassaAI API response: {resp_json}") - - # API возвращает успешный ответ со ссылкой в поле location if resp_json.get("type") == "success": payment_url = resp_json.get("location") if payment_url: - logger.info(f"KassaAI payment URL created: {payment_url}") + logger.info(f"KassaAI payment URL created for user {tg_id}") return payment_url else: - logger.error(f"KassaAI: Нет поля location в успешном ответе: {resp_json}") + logger.error(f"KassaAI: No location in response: {resp_json}") return "https://fk.life/" else: - logger.error(f"KassaAI: Неуспешный ответ: {resp_json}") + logger.error(f"KassaAI: Unsuccessful response: {resp_json}") return "https://fk.life/" except Exception as e: - logger.error(f"KassaAI: Ошибка при разборе JSON ответа: {e}") + logger.error(f"KassaAI: Error parsing JSON response: {e}") text = await resp.text() - logger.error(f"KassaAI: Содержимое ответа: {text}") + logger.error(f"KassaAI: Response content: {text}") return "https://fk.life/" else: try: error_json = await resp.json() - logger.error(f"Ошибка KassaAI API: статус={resp.status}, ответ={error_json}") + logger.error(f"KassaAI API error: status={resp.status}, response={error_json}") except Exception: text = await resp.text() - logger.error(f"Ошибка KassaAI API: статус={resp.status}, не-JSON ответ: {text}") + logger.error(f"KassaAI API error: status={resp.status}, non-JSON response: {text}") return "https://fk.life/" except Exception as e: - logger.error(f"Ошибка при создании заказа KassaAI: {e}") + logger.error(f"Error creating KassaAI order: {e}") return "https://fk.life/" @@ -380,19 +350,15 @@ def verify_kassai_signature(data: dict, signature: str) -> bool: Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID """ try: - # Формируем строку для подписи согласно документации: - # MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID sign_string = ( f"{KASSAI_SHOP_ID}:" f"{data.get('AMOUNT', '')}:" - f"{KASSAI_SECRET_KEY}:" # Используем SECRET_KEY2 для вебхуков + f"{KASSAI_SECRET_KEY}:" f"{data.get('MERCHANT_ORDER_ID', '')}" ) - # Вычисляем MD5 хеш expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest() - # Сравниваем подписи (регистронезависимо) result = signature.upper() == expected_signature.upper() if not result: diff --git a/web/kassai_payment.py b/web/kassai_payment.py index 21bc94fa..dd6cc603 100644 --- a/web/kassai_payment.py +++ b/web/kassai_payment.py @@ -7,128 +7,88 @@ from handlers.payments.kassai import verify_kassai_signature from config import KASSAI_SECRET_KEY, KASSAI_SHOP_ID from logger import logger -# Хранилище обработанных платежей для предотвращения дублирования processed_payments = set() async def kassai_payment_webhook(request: web.Request): """ Обработчик вебхука от KassaAI для подтверждения оплаты - Формат: MERCHANT_ID=123&AMOUNT=100&intid=123456&MERCHANT_ORDER_ID=test_order&SIGN=... """ try: - # Получаем данные из POST запроса (form-data) data = await request.post() data_dict = dict(data) - # Логируем входящий запрос - logger.info(f"KassaAI webhook received: {data_dict}") + logger.info(f"KassaAI webhook received from {request.remote}") - # Получаем подпись из параметров signature = data_dict.get("SIGN") - if not signature: - logger.error("KassaAI: Нет подписи SIGN в запросе!") + logger.error("KassaAI: Missing SIGN in request") return web.Response(status=400, text="Signature missing") - # Проверяем подпись согласно документации KassaAI if not verify_kassai_webhook_signature(data_dict, signature): - logger.error("KassaAI: Подпись не прошла проверку!") + logger.error("KassaAI: Invalid signature") return web.Response(status=400, text="Invalid signature") - # Логируем все важные параметры - logger.info( - f"KassaAI webhook details: " - f"MERCHANT_ID={data_dict.get('MERCHANT_ID')}, " - f"AMOUNT={data_dict.get('AMOUNT')}, " - f"intid={data_dict.get('intid')}, " - f"MERCHANT_ORDER_ID={data_dict.get('MERCHANT_ORDER_ID')}, " - f"P_EMAIL={data_dict.get('P_EMAIL')}, " - f"CUR_ID={data_dict.get('CUR_ID')}, " - f"SIGN={data_dict.get('SIGN')}" - ) - - # Получаем необходимые данные merchant_order_id = data_dict.get("MERCHANT_ORDER_ID") amount = data_dict.get("AMOUNT") p_email = data_dict.get("P_EMAIL") if not amount: - logger.error(f"KassaAI: отсутствует AMOUNT: {data_dict}") + logger.error(f"KassaAI: Missing AMOUNT") return web.Response(status=400, text="Missing required fields") - # Проверяем уникальность платежа по MERCHANT_ORDER_ID (если есть) if merchant_order_id and merchant_order_id in processed_payments: - logger.warning(f"KassaAI: платеж MERCHANT_ORDER_ID={merchant_order_id} уже был обработан. Игнорируем дубликат.") - return web.Response(status=200, text="YES") # Возвращаем успех чтобы не было повторных попыток + logger.warning(f"KassaAI: Duplicate payment {merchant_order_id}") + return web.Response(status=200, text="YES") try: - # Извлекаем tg_id из email (формат: tg_id@domain) if p_email and "@" in p_email: tg_id = int(p_email.split("@")[0]) - logger.info(f"KassaAI: извлечен tg_id={tg_id} из email={p_email}") else: - logger.error(f"KassaAI: некорректный email для извлечения tg_id: {p_email}") + logger.error(f"KassaAI: Invalid email format: {p_email}") return web.Response(status=400, text="Invalid email format") amount_float = float(amount) - logger.info(f"KassaAI: обрабатываем платеж MERCHANT_ORDER_ID={merchant_order_id}, tg_id={tg_id}, amount={amount_float}") except (ValueError, TypeError) as e: - logger.error(f"KassaAI: некорректные данные tg_id или amount: {e}") + logger.error(f"KassaAI: Invalid data format: {e}") return web.Response(status=400, text="Invalid data format") - # Обновляем баланс пользователя async with async_session_maker() as session: await update_balance(session, tg_id, amount_float) await send_payment_success_notification(tg_id, amount_float, session) await add_payment(session, tg_id, amount_float, "kassai") - # Добавляем MERCHANT_ORDER_ID в список обработанных платежей (если есть) if merchant_order_id: processed_payments.add(merchant_order_id) - logger.info(f"✅ KassaAI: баланс пополнен для пользователя {tg_id} на {amount_float}, MERCHANT_ORDER_ID={merchant_order_id}") - - # Возвращаем ответ согласно документации KassaAI + logger.info(f"✅ KassaAI: Payment processed for user {tg_id}, amount {amount_float}") return web.Response(status=200, text="YES") except Exception as e: - logger.error(f"Ошибка обработки KassaAI webhook: {e}") + logger.error(f"KassaAI webhook error: {e}") return web.Response(status=500, text="Internal server error") def verify_kassai_webhook_signature(data: dict, signature: str) -> bool: """ Проверка подписи вебхука KassaAI согласно документации FreeKassa - Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID """ try: - # Формируем строку для подписи согласно документации FreeKassa: - # MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID sign_string = ( f"{data.get('MERCHANT_ID', '')}:" f"{data.get('AMOUNT', '')}:" - f"{KASSAI_SECRET_KEY}:" # Используем SECRET_KEY2 для вебхуков + f"{KASSAI_SECRET_KEY}:" f"{data.get('MERCHANT_ORDER_ID', '')}" ) - # Вычисляем MD5 хеш expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest() - - # Сравниваем подписи (регистронезависимо) result = signature.upper() == expected_signature.upper() if not result: - logger.error(f"KassaAI webhook signature mismatch:") - logger.error(f"Expected: {expected_signature}") - logger.error(f"Received: {signature}") - logger.error(f"Sign string: {sign_string}") - logger.error(f"MERCHANT_ID: {data.get('MERCHANT_ID')}") - logger.error(f"AMOUNT: {data.get('AMOUNT')}") - logger.error(f"MERCHANT_ORDER_ID: {data.get('MERCHANT_ORDER_ID')}") + logger.error(f"KassaAI webhook signature mismatch") return result except Exception as e: - logger.error(f"Ошибка проверки подписи KassaAI webhook: {e}") + logger.error(f"KassaAI signature verification error: {e}") return False \ No newline at end of file