Merge pull request #246 from JustYay/dev
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
+4
-4
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user