mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 11:21:17 +00:00
175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
"""HTTP client for Heleket payment API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
from typing import Any, Dict, Optional
|
|
|
|
import aiohttp
|
|
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HeleketService:
|
|
"""Minimal wrapper around Heleket API endpoints."""
|
|
|
|
def __init__(self) -> None:
|
|
self.base_url = settings.HELEKET_BASE_URL.rstrip("/")
|
|
self.merchant_id = settings.HELEKET_MERCHANT_ID
|
|
self.api_key = settings.HELEKET_API_KEY
|
|
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
return bool(self.merchant_id and self.api_key)
|
|
|
|
def _prepare_body(
|
|
self,
|
|
payload: Dict[str, Any],
|
|
*,
|
|
ignore_none: bool,
|
|
sort_keys: bool,
|
|
) -> str:
|
|
if ignore_none:
|
|
cleaned = {key: value for key, value in payload.items() if value is not None}
|
|
else:
|
|
cleaned = dict(payload)
|
|
|
|
serialized = json.dumps(
|
|
cleaned,
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
sort_keys=sort_keys,
|
|
)
|
|
|
|
if "/" in serialized:
|
|
serialized = serialized.replace("/", "\\/")
|
|
|
|
return serialized
|
|
|
|
def _generate_signature(self, body: str) -> str:
|
|
api_key = self.api_key or ""
|
|
encoded = base64.b64encode(body.encode("utf-8")).decode("utf-8")
|
|
raw = f"{encoded}{api_key}"
|
|
return hashlib.md5(raw.encode("utf-8")).hexdigest()
|
|
|
|
async def _request(
|
|
self,
|
|
endpoint: str,
|
|
payload: Dict[str, Any],
|
|
*,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
) -> Optional[Dict[str, Any]]:
|
|
if not self.is_configured:
|
|
logger.error("Heleket сервис не настроен: merchant или api_key отсутствуют")
|
|
return None
|
|
|
|
body = self._prepare_body(payload, ignore_none=True, sort_keys=True)
|
|
signature = self._generate_signature(body)
|
|
|
|
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
headers = {
|
|
"merchant": self.merchant_id or "",
|
|
"sign": signature,
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
try:
|
|
timeout = aiohttp.ClientTimeout(total=30)
|
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
async with session.post(
|
|
url,
|
|
data=body.encode("utf-8"),
|
|
headers=headers,
|
|
params=params,
|
|
) as response:
|
|
text = await response.text()
|
|
if response.content_type != "application/json":
|
|
logger.error("Ответ Heleket не JSON (%s): %s", response.content_type, text)
|
|
return None
|
|
|
|
try:
|
|
data = json.loads(text)
|
|
except json.JSONDecodeError:
|
|
logger.error("Ошибка парсинга Heleket JSON: %s", text)
|
|
return None
|
|
|
|
if response.status >= 400:
|
|
logger.error("Heleket API %s вернул статус %s: %s", endpoint, response.status, data)
|
|
return None
|
|
|
|
if isinstance(data, dict) and data.get("state") == 0:
|
|
return data
|
|
|
|
logger.error("Heleket API вернул ошибку: %s", data)
|
|
return None
|
|
except Exception as error: # pragma: no cover - defensive
|
|
logger.error("Ошибка запроса к Heleket API: %s", error)
|
|
return None
|
|
|
|
async def create_payment(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
return await self._request("payment", payload)
|
|
|
|
async def get_payment_info(
|
|
self,
|
|
*,
|
|
uuid: Optional[str] = None,
|
|
order_id: Optional[str] = None,
|
|
) -> Optional[Dict[str, Any]]:
|
|
if not uuid and not order_id:
|
|
raise ValueError("Нужно указать uuid или order_id для Heleket payment/info")
|
|
|
|
payload: Dict[str, Any] = {}
|
|
if uuid:
|
|
payload["uuid"] = uuid
|
|
if order_id:
|
|
payload["order_id"] = order_id
|
|
|
|
return await self._request("payment/info", payload)
|
|
|
|
async def list_payments(
|
|
self,
|
|
*,
|
|
date_from: Optional[str] = None,
|
|
date_to: Optional[str] = None,
|
|
cursor: Optional[str] = None,
|
|
) -> Optional[Dict[str, Any]]:
|
|
payload: Dict[str, Any] = {}
|
|
if date_from:
|
|
payload["date_from"] = date_from
|
|
if date_to:
|
|
payload["date_to"] = date_to
|
|
|
|
params = {"cursor": cursor} if cursor else None
|
|
return await self._request("payment/list", payload, params=params)
|
|
|
|
def verify_webhook_signature(self, payload: Dict[str, Any]) -> bool:
|
|
if not self.is_configured:
|
|
logger.warning("Heleket сервис не настроен, подпись пропускается")
|
|
return True
|
|
|
|
if not isinstance(payload, dict):
|
|
logger.error("Heleket webhook payload не dict: %s", payload)
|
|
return False
|
|
|
|
signature = payload.get("sign")
|
|
if not signature:
|
|
logger.error("Heleket webhook без подписи")
|
|
return False
|
|
|
|
data = dict(payload)
|
|
data.pop("sign", None)
|
|
body = self._prepare_body(data, ignore_none=False, sort_keys=False)
|
|
expected = self._generate_signature(body)
|
|
|
|
is_valid = hmac.compare_digest(expected, str(signature))
|
|
|
|
if not is_valid:
|
|
logger.error("Неверная подпись Heleket webhook: ожидается %s, получено %s", expected, signature)
|
|
return is_valid
|