diff --git a/handlers/payments/heleket/webhook.py b/handlers/payments/heleket/webhook.py index 9c7c31dd..e1c90755 100644 --- a/handlers/payments/heleket/webhook.py +++ b/handlers/payments/heleket/webhook.py @@ -1,9 +1,11 @@ import base64 import hashlib import json +from aiohttp import web from logger import logger from config import HELEKET_API_KEY -from database import async_session_maker, update_payment_status, add_balance_to_user +from database import async_session_maker, update_payment_status, update_balance, add_payment, get_payment_by_payment_id +from handlers.payments.utils import send_payment_success_notification def verify_heleket_signature(data: dict) -> bool: @@ -77,17 +79,42 @@ async def process_heleket_webhook(data: dict) -> bool: return False balance_amount = rub_amount if rub_amount else float(merchant_amount) async with async_session_maker() as session: - await update_payment_status(session, order_id, "success") - await add_balance_to_user(session, tg_id, balance_amount) - await session.commit() + payment = await get_payment_by_payment_id(session, order_id) + if payment: + if payment.get("status") == "success": + logger.info(f"Heleket: платёж {order_id} уже обработан") + return True + ok = await update_payment_status(session=session, internal_id=int(payment["id"]), new_status="success") + if not ok: + logger.error(f"Heleket: не удалось обновить статус платежа {order_id}") + return False + await update_balance(session, tg_id, balance_amount) + await send_payment_success_notification(tg_id, balance_amount, session) + await session.commit() + else: + await add_payment( + session=session, + tg_id=tg_id, + amount=balance_amount, + payment_system="HELEKET", + status="success", + currency="USD", + payment_id=order_id, + metadata=None, + ) + await update_balance(session, tg_id, balance_amount) + await send_payment_success_notification(tg_id, balance_amount, session) + await session.commit() logger.info(f"Heleket: платёж {order_id} для пользователя {tg_id} успешно обработан, баланс пополнен на {balance_amount} RUB") return True elif status in ['fail', 'wrong_amount', 'cancel', 'system_fail']: logger.warning(f"Heleket: неудачный платёж {order_id}, статус: {status}") async with async_session_maker() as session: - await update_payment_status(session, order_id, "failed") - await session.commit() + payment = await get_payment_by_payment_id(session, order_id) + if payment: + await update_payment_status(session=session, internal_id=int(payment["id"]), new_status="failed") + await session.commit() return True else: logger.info(f"Heleket: промежуточный статус {status} для платежа {order_id}") @@ -95,3 +122,23 @@ async def process_heleket_webhook(data: dict) -> bool: except Exception as e: logger.error(f"Ошибка обработки Heleket webhook: {e}") return False + + +async def heleket_webhook(request: web.Request): + """Обработчик вебхука Heleket для aiohttp""" + try: + data = await request.json() + logger.info(f"Heleket webhook received from {request.remote}") + + if not verify_heleket_signature(data): + logger.error("Heleket webhook: неверная подпись") + return web.Response(status=400, text="Invalid signature") + + success = await process_heleket_webhook(data) + if success: + return web.Response(status=200, text="OK") + else: + return web.Response(status=400, text="Processing failed") + except Exception as e: + logger.error(f"Ошибка обработки Heleket webhook: {e}") + return web.Response(status=500, text="Internal server error") diff --git a/handlers/payments/kassai/webhook.py b/handlers/payments/kassai/webhook.py index d3c44f00..529c5b0d 100644 --- a/handlers/payments/kassai/webhook.py +++ b/handlers/payments/kassai/webhook.py @@ -36,14 +36,10 @@ async def kassai_webhook(request: web.Request): amount_raw = data.get('AMOUNT') order_id = data.get('MERCHANT_ORDER_ID') - status = data.get('STATUS', '') if not amount_raw or not order_id: logger.error("KassaAI webhook: отсутствуют обязательные параметры") return web.Response(status=400) - if status.upper() != 'SUCCESS': - logger.warning(f"KassaAI webhook: неуспешный статус {status} для заказа {order_id}") - return web.Response(text="OK") amount = float(amount_raw) @@ -65,6 +61,9 @@ async def kassai_webhook(request: web.Request): if not ok: logger.error(f"KassaAI: не удалось обновить статус платежа {order_id}") return web.Response(status=500) + await update_balance(session, tg_id, amount) + await send_payment_success_notification(tg_id, amount, session) + await session.commit() else: await add_payment( session=session, @@ -76,10 +75,9 @@ async def kassai_webhook(request: web.Request): payment_id=order_id, metadata=None, ) - - await update_balance(session, tg_id, amount) - await send_payment_success_notification(tg_id, amount, session) - await session.commit() + await update_balance(session, tg_id, amount) + await send_payment_success_notification(tg_id, amount, session) + await session.commit() logger.info(f"KassaAI: платёж {order_id} успешно обработан, баланс пользователя {tg_id} пополнен на {amount} RUB") return web.Response(text="OK") except Exception as e: diff --git a/web/__init__.py b/web/__init__.py index 84886ad0..2a2cc737 100644 --- a/web/__init__.py +++ b/web/__init__.py @@ -2,8 +2,8 @@ from aiohttp.web_urldispatcher import UrlDispatcher from utils.modules_loader import load_module_webhooks -from .heleket_payment import heleket_payment_webhook -from .kassai_payment import kassai_payment_webhook +from handlers.payments.heleket.webhook import heleket_webhook +from handlers.payments.kassai.webhook import kassai_webhook from .wata_payment import wata_payment_webhook @@ -14,8 +14,8 @@ HELEKET_WEBHOOK_PATH = "/heleket/webhook" async def register_web_routes(router: UrlDispatcher) -> None: router.add_post(WATA_WEBHOOK_PATH, wata_payment_webhook) - router.add_post(KASSAI_WEBHOOK_PATH, kassai_payment_webhook) - router.add_post(HELEKET_WEBHOOK_PATH, heleket_payment_webhook) + router.add_post(KASSAI_WEBHOOK_PATH, kassai_webhook) + router.add_post(HELEKET_WEBHOOK_PATH, heleket_webhook) try: module_webhooks = load_module_webhooks() diff --git a/web/heleket_payment.py b/web/heleket_payment.py deleted file mode 100644 index d421a0f4..00000000 --- a/web/heleket_payment.py +++ /dev/null @@ -1,203 +0,0 @@ -import base64 -import hashlib -import json - -from decimal import ROUND_HALF_UP, Decimal - -import aiohttp - -from aiohttp import web -from sqlalchemy import select - -from config import HELEKET_API_KEY -from database import Payment, async_session_maker, update_balance, update_payment_status -from handlers.payments.currency_rates import to_rub -from handlers.payments.utils import send_payment_success_notification -from logger import logger - - -processed_payments = set() - - -async def heleket_payment_webhook(request: web.Request): - try: - data = await request.json() - logger.info(f"Heleket webhook received from {request.remote}") - logger.info(f"Heleket webhook data: {data}") - - signature = data.get("sign", "") - if not verify_heleket_webhook_signature(data, signature): - logger.error("Heleket: Invalid signature") - return web.Response(status=400, text="Invalid signature") - - uuid = data.get("uuid") - order_id = data.get("order_id") - payment_status = data.get("status") - amount_raw = data.get("amount") - currency = str(data.get("currency") or "").upper() or None - payment_amount_raw = data.get("payment_amount") - to_currency = str(data.get("to_currency") or "").upper() or None - additional_data = data.get("additional_data", "") - - logger.info( - f"Heleket payment: uuid={uuid}, order_id={order_id}, status={payment_status}, " - f"amount={amount_raw}, payment_amount={payment_amount_raw}, currency={currency}, to_currency={to_currency}" - ) - - if payment_status != "paid": - logger.info(f"Heleket: Payment not completed, status={payment_status}") - return web.Response(status=200, text="OK") - - if not uuid: - logger.error("Heleket: Missing uuid") - return web.Response(status=400, text="Missing required fields") - - if uuid in processed_payments: - logger.warning(f"Heleket: Duplicate payment uuid={uuid}") - return web.Response(status=200, text="OK") - if order_id and order_id in processed_payments: - logger.warning(f"Heleket: Duplicate payment order_id={order_id}") - return web.Response(status=200, text="OK") - - tg_id = None - rub_amount: Decimal | None = None - - try: - if additional_data and "tg_id:" in additional_data: - parts = [p.strip() for p in additional_data.split(",")] - for part in parts: - if part.startswith("tg_id:"): - tg_id = int(part.split("tg_id:")[1]) - elif part.startswith("rub_amount:"): - val = part.split("rub_amount:")[1] - rub_amount = Decimal(str(val)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) - - if tg_id is None and order_id and "_" in order_id: - tg_id = int(order_id.split("_")[1]) - except (ValueError, IndexError) as e: - logger.error(f"Heleket: Error extracting tg_id or rub_amount: {e}") - return web.Response(status=400, text="Invalid user ID or amount format") - - paid_ccy = None - original_amount_dec: Decimal | None = None - - if currency and amount_raw is not None: - try: - original_amount_dec = Decimal(str(amount_raw)) - paid_ccy = currency - except Exception: - original_amount_dec = None - - if original_amount_dec is None and to_currency and payment_amount_raw is not None: - try: - original_amount_dec = Decimal(str(payment_amount_raw)) - paid_ccy = to_currency - except Exception: - original_amount_dec = None - - if rub_amount is None: - amt_str = str(amount_raw or "").strip() - if amt_str and currency: - try: - amt_dec = Decimal(amt_str) - async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as s: - rub_amount = await to_rub(amt_dec, currency, session=s) - except Exception as e: - logger.error(f"Heleket: RUB conversion failed ({currency}→RUB) for amount={amt_str}: {e}") - rub_amount = None - - if rub_amount is None and payment_amount_raw and to_currency: - try: - amt_dec = Decimal(str(payment_amount_raw)) - async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as s: - rub_amount = await to_rub(amt_dec, to_currency, session=s) - except Exception as e: - logger.error( - f"Heleket: RUB conversion failed ({to_currency}→RUB) for payment_amount={payment_amount_raw}: {e}" - ) - rub_amount = None - - if rub_amount is None: - logger.error("Heleket: Could not determine rub_amount") - return web.Response(status=400, text="Cannot determine amount") - - if tg_id is None: - logger.error("Heleket: Cannot extract tg_id from data") - return web.Response(status=400, text="Cannot extract user ID") - - meta_patch = { - "provider": "HELEKET", - "heleket_raw": data, - "provider_uuid": uuid, - "provider_order_id": order_id, - } - if paid_ccy and original_amount_dec is not None: - meta_patch["paid_invoice_amount"] = float(original_amount_dec) - meta_patch["paid_invoice_currency"] = paid_ccy - meta_patch["fx"] = {"base": paid_ccy, "rub_equivalent": str(rub_amount)} - - async with async_session_maker() as session: - internal_payment = None - if order_id: - res = await session.execute(select(Payment).where(Payment.payment_id == str(order_id)).limit(1)) - internal_payment = res.scalar_one_or_none() - - if internal_payment: - internal_payment.status = "success" - if order_id: - internal_payment.payment_id = str(order_id) - if paid_ccy: - internal_payment.currency = paid_ccy - if original_amount_dec is not None: - internal_payment.original_amount = float(original_amount_dec) - current_md = getattr(internal_payment, "metadata_", None) - if isinstance(current_md, dict): - current_md.update(meta_patch) - internal_payment.metadata_ = current_md - else: - internal_payment.metadata_ = meta_patch - await session.commit() - else: - ok = await update_payment_status( - session=session, - internal_id=0, - new_status="success", - payment_id=str(order_id) if order_id else None, - metadata_patch=meta_patch, - ) - if not ok: - logger.error("Heleket: Failed to update payment status for unknown internal payment") - - await update_balance(session, tg_id, float(rub_amount)) - await send_payment_success_notification(tg_id, float(rub_amount), session) - - processed_payments.add(uuid) - if order_id: - processed_payments.add(order_id) - - logger.info(f"Heleket: Payment processed for user {tg_id}, amount {rub_amount} RUB, uuid={uuid}") - return web.Response(status=200, text="OK") - - except Exception as e: - logger.error(f"Heleket webhook error: {e}") - return web.Response(status=500, text="Internal server error") - - -def verify_heleket_webhook_signature(data: dict, signature: str) -> bool: - try: - data_without_sign = {k: v for k, v in data.items() if k != "sign"} - json_data = json.dumps(data_without_sign, separators=(",", ":"), ensure_ascii=False) - base64_data = base64.b64encode(json_data.encode("utf-8")).decode("utf-8") - sign_string = base64_data + HELEKET_API_KEY - expected_signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest() - - result = signature.upper() == expected_signature.upper() - if not result: - logger.error("Heleket webhook signature mismatch") - logger.error(f"Expected: {expected_signature}, Got: {signature}") - logger.error(f"Base64 data: {base64_data}") - logger.error(f"Sign string: {sign_string}") - return result - except Exception as e: - logger.error(f"Heleket signature verification error: {e}") - return False diff --git a/web/kassai_payment.py b/web/kassai_payment.py deleted file mode 100644 index 5f4d5056..00000000 --- a/web/kassai_payment.py +++ /dev/null @@ -1,105 +0,0 @@ -import hashlib - -from aiohttp import web - -from config import KASSAI_SECRET_KEY -from database import add_payment, async_session_maker, update_balance -from handlers.payments.utils import send_payment_success_notification -from logger import logger - - -processed_payments = set() - - -async def kassai_payment_webhook(request: web.Request): - """ - Обработчик вебхука от KassaAI для подтверждения оплаты - """ - try: - data = await request.post() - data_dict = dict(data) - - logger.info(f"KassaAI webhook received from {request.remote}") - - signature = data_dict.get("SIGN") - if not signature: - logger.error("KassaAI: Missing SIGN in request") - return web.Response(status=400, text="Signature missing") - - if not verify_kassai_webhook_signature(data_dict, signature): - logger.error("KassaAI: Invalid signature") - return web.Response(status=400, text="Invalid signature") - - merchant_order_id = data_dict.get("MERCHANT_ORDER_ID") - amount = data_dict.get("AMOUNT") - p_email = data_dict.get("P_EMAIL") - intid = data_dict.get("intid") - - logger.info(f"KassaAI payment: intid={intid}, MERCHANT_ORDER_ID={merchant_order_id}, amount={amount}") - - if not amount or not intid: - logger.error("KassaAI: Missing AMOUNT or intid") - return web.Response(status=400, text="Missing required fields") - - if intid in processed_payments: - logger.warning(f"KassaAI: Duplicate payment intid={intid}") - return web.Response(status=200, text="YES") - - if merchant_order_id and merchant_order_id in processed_payments: - logger.warning(f"KassaAI: Duplicate payment MERCHANT_ORDER_ID={merchant_order_id}") - return web.Response(status=200, text="YES") - - try: - if p_email and "@" in p_email: - tg_id = int(p_email.split("@")[0]) - else: - logger.error(f"KassaAI: Invalid email format: {p_email}") - return web.Response(status=400, text="Invalid email format") - - amount_float = float(amount) - except (ValueError, TypeError) as 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 add_payment(session, tg_id, amount_float, "kassai") - await send_payment_success_notification(tg_id, amount_float, session) - - processed_payments.add(intid) - if merchant_order_id: - processed_payments.add(merchant_order_id) - - logger.info( - f"✅ KassaAI: Payment processed for user {tg_id}, amount {amount_float}, intid={intid}, MERCHANT_ORDER_ID={merchant_order_id}" - ) - return web.Response(status=200, text="YES") - - except Exception as 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 - """ - try: - sign_string = ( - f"{data.get('MERCHANT_ID', '')}:" - f"{data.get('AMOUNT', '')}:" - f"{KASSAI_SECRET_KEY}:" - f"{data.get('MERCHANT_ORDER_ID', '')}" - ) - - expected_signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest() - result = signature.upper() == expected_signature.upper() - - if not result: - logger.error("KassaAI webhook signature mismatch") - - return result - - except Exception as e: - logger.error(f"KassaAI signature verification error: {e}") - return False