Files
Click/services/payment_service.py
T
Yuriy Yuriev 61cf029744 fix payment
2026-05-22 15:44:46 +07:00

114 lines
4.1 KiB
Python

"""Heleket payment integration."""
import base64
import hashlib
import json
import logging
from typing import Optional
import aiohttp
from config.settings import settings
logger = logging.getLogger(__name__)
def _make_sign(body_bytes: bytes, api_key: str) -> str:
"""Генерирует подпись запроса: md5(base64(body) + api_key)."""
b64 = base64.b64encode(body_bytes).decode("utf-8")
return hashlib.md5((b64 + api_key).encode("utf-8")).hexdigest()
class HelketPayment:
"""Heleket payment gateway. Принимает сумму в рублях напрямую."""
BASE_URL = "https://api.heleket.com/v1"
def __init__(self):
self.api_key = settings.HELEKET_API_KEY
self.shop_id = settings.HELEKET_SHOP_ID
self._mock = not (self.api_key and self.shop_id)
def _post_headers(self, body_bytes: bytes) -> dict:
"""Заголовки для POST-запроса с подписью."""
return {
"merchant": self.api_key,
"sign": _make_sign(body_bytes, self.api_key),
"Content-Type": "application/json",
}
def _get_headers(self) -> dict:
"""Заголовки для GET-запроса (тело пустое)."""
return {
"merchant": self.api_key,
"sign": _make_sign(b"", self.api_key),
}
async def create_invoice(self, amount_rub: int, order_id: str) -> Optional[dict]:
"""
Создаёт счёт на сумму в рублях.
Возвращает dict: payment_id, address, url. None при ошибке.
"""
if self._mock:
return {
"payment_id": f"mock_{order_id}",
"address": None,
"url": None,
"mock": True,
}
payload = {
"amount": str(amount_rub),
"currency": "RUB",
"order_id": order_id,
"url_callback": settings.HELEKET_WEBHOOK_URL,
}
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.BASE_URL}/payment",
data=body,
headers=self._post_headers(body),
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
data = await resp.json()
if data.get("state") == 0:
r = data.get("result", {})
return {
"payment_id": r.get("uuid"),
"address": r.get("address"),
"url": r.get("url"),
"mock": False,
}
logger.error(f"Heleket create error: {data}")
return None
except Exception as e:
logger.error(f"Heleket request error: {e}")
return None
async def check_status(self, payment_id: str) -> str:
"""Возвращает: 'paid' | 'pending' | 'expired' | 'error'"""
if payment_id.startswith("mock_"):
return "pending"
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.BASE_URL}/payment/info/{payment_id}",
headers=self._get_headers(),
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
data = await resp.json()
if data.get("state") == 0:
status = data.get("result", {}).get("status", "")
if status in ("paid", "paid_over"):
return "paid"
if status in ("cancel", "expired", "fail", "wrong_amount"):
return "expired"
return "pending"
return "error"
except Exception as e:
logger.error(f"Heleket check error: {e}")
return "error"