204 lines
8.5 KiB
Python
204 lines
8.5 KiB
Python
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
|