fix payment

This commit is contained in:
Yuriy Yuriev
2026-05-22 19:15:44 +07:00
parent b276794c0a
commit bad7966fc4
4 changed files with 126 additions and 42 deletions
+58 -40
View File
@@ -10,16 +10,16 @@ import base64
import hashlib
import json
import logging
from datetime import datetime
from typing import Optional
from aiohttp import web
from config.settings import settings
from managers.storage import PaymentStorage, BalanceStorage, RubleBalanceStorage
from managers.storage import PaymentStorage, BalanceStorage, RubleBalanceStorage, PaymentHistoryStorage
logger = logging.getLogger(__name__)
# Доверенный IP Heleket из документации
HELEKET_IP = "31.133.220.8"
@@ -27,36 +27,32 @@ def _verify_sign(payload: dict, api_key: str) -> bool:
"""Верифицирует подпись webhook-запроса от Heleket.
Алгоритм: md5( base64( json(payload_without_sign) ) + api_key )
ВАЖНО: PHP json_encode() экранирует '/' как '\/', Python — нет.
Heleket вычисляет подпись на PHP, поэтому вручную экранируем слэши.
PHP json_encode() экранирует '/' как '\/', Python — нет, делаем вручную.
"""
received_sign = payload.get("sign", "")
data = {k: v for k, v in payload.items() if k != "sign"}
data_json = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
data_json = data_json.replace("/", "\\/") # имитируем PHP json_encode
data_json = data_json.replace("/", "\\/")
data_b64 = base64.b64encode(data_json.encode("utf-8")).decode("utf-8")
expected = hashlib.md5((data_b64 + api_key).encode("utf-8")).hexdigest()
return expected == received_sign
class HelketWebhookServer:
"""
aiohttp сервер для вебхуков Heleket.
Запускается параллельно с Telegram-ботом.
"""
def __init__(
self,
payment_storage: PaymentStorage,
balance_storage: BalanceStorage,
rub_storage: RubleBalanceStorage,
bot, # aiogram Bot
history_storage: PaymentHistoryStorage,
bot,
port: int = 8080,
):
self._payment_storage = payment_storage
self._balance_storage = balance_storage
self._rub_storage = rub_storage
self._history_storage = history_storage
self._bot = bot
self._port = port
self._app = web.Application()
@@ -67,7 +63,7 @@ class HelketWebhookServer:
self._app.router.add_get("/health", self._handle_health)
async def start(self) -> None:
self._runner = web.AppRunner(self._app)
self._runner = web.AppRunner(self._app, access_log=None)
await self._runner.setup()
site = web.TCPSite(self._runner, "0.0.0.0", self._port)
await site.start()
@@ -82,7 +78,6 @@ class HelketWebhookServer:
return web.Response(text="ok")
async def _handle_webhook(self, request: web.Request) -> web.Response:
# Проверяем IP (опционально — может быть за прокси/CDN)
client_ip = request.headers.get("X-Forwarded-For", request.remote)
if settings.HELEKET_WHITELIST_IP and client_ip and HELEKET_IP not in client_ip:
logger.warning(f"Webhook from unknown IP: {client_ip}")
@@ -94,9 +89,8 @@ class HelketWebhookServer:
logger.warning("Webhook: invalid JSON body")
return web.Response(status=400, text="Bad Request")
logger.debug(f"Webhook payload: {payload}")
logger.info(f"Webhook received: uuid={payload.get('uuid')} status={payload.get('status')}")
# Верифицируем подпись если есть API ключ
if self._api_key and not _verify_sign(payload, self._api_key):
logger.warning(f"Webhook: invalid signature. uuid={payload.get('uuid')}")
return web.Response(status=400, text="Invalid signature")
@@ -104,16 +98,14 @@ class HelketWebhookServer:
status = payload.get("status", "")
payment_uuid = payload.get("uuid", "")
# paid_over — клиент заплатил больше суммы счёта, тоже засчитываем
if status not in ("paid", "paid_over"):
logger.info(f"Webhook: uuid={payment_uuid} status={status} — ignored")
return web.Response(text="ok")
# Ищем платёж в хранилище
payment = await self._payment_storage.get(payment_uuid)
if not payment:
logger.warning(f"Webhook: payment {payment_uuid} not found in storage")
return web.Response(text="ok") # уже обработан или неизвестный
logger.warning(f"Webhook: payment {payment_uuid} not found (already processed?)")
return web.Response(text="ok")
user_id = payment.get("user_id")
chat_id = payment.get("chat_id") or user_id
@@ -122,43 +114,69 @@ class HelketWebhookServer:
try:
if clicks:
# Прямая покупка переходов
# Прямая покупка переходов — начисляем переходы
await self._balance_storage.add_balance(user_id, clicks)
added_clicks = clicks
msg = (
f"✅ Оплата получена!\n\n"
f"💳 Сумма: {rub}\n"
f"🖱 Начислено: {clicks} переходов\n"
f"📊 Проверьте баланс в личном кабинете"
f"🖱 Начислено: {clicks} переходов"
)
history_record = {
"type": "clicks",
"rub": rub,
"clicks": clicks,
"uuid": payment_uuid,
"at": datetime.now().isoformat(timespec="seconds"),
}
logger.info(f"Webhook: user={user_id} +{clicks} clicks")
else:
# Пополнение рублёвого баланса → конвертируем в переходы
added_clicks = int(rub // settings.CLICK_PRICE_RUB)
await self._balance_storage.add_balance(user_id, added_clicks)
# Пополнение — начисляем только рубли, переходы пользователь купит сам
await self._rub_storage.add_balance(user_id, rub)
new_rub = await self._rub_storage.get_balance(user_id)
msg = (
f"Пополнение получено!\n\n"
f"Баланс пополнен!\n\n"
f"💳 Сумма: {rub}\n"
f"🖱 Начислено: {added_clicks} переходов "
f"({rub} ÷ {settings.CLICK_PRICE_RUB} ₽/переход)\n"
f"📊 Проверьте баланс в личном кабинете"
f"🪙 Рублей на балансе: {new_rub} \n\n"
f"Перейдите в кабинет чтобы купить переходы."
)
history_record = {
"type": "topup",
"rub": rub,
"uuid": payment_uuid,
"at": datetime.now().isoformat(timespec="seconds"),
}
logger.info(f"Webhook: user={user_id} +{rub} RUB")
await self._payment_storage.delete(payment_uuid)
logger.info(
f"Webhook: payment {payment_uuid} confirmed — "
f"user={user_id} +{added_clicks} clicks"
)
await self._history_storage.add(user_id, history_record)
# Уведомляем пользователя
if chat_id:
try:
await self._bot.send_message(chat_id, msg)
except Exception as e:
logger.warning(f"Webhook: failed to notify user {user_id}: {e}")
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🏠 Личный кабинет", callback_data="my_balance"))
kb = builder.as_markup()
msg_id = payment.get("msg_id")
edited = False
if msg_id:
try:
await self._bot.edit_message_text(
msg, chat_id=chat_id, message_id=msg_id,
reply_markup=kb
)
edited = True
except Exception:
pass
if not edited:
try:
await self._bot.send_message(chat_id, msg, reply_markup=kb)
except Exception as e:
logger.warning(f"Webhook: failed to notify user {user_id}: {e}")
except Exception as e:
logger.error(f"Webhook: error processing payment {payment_uuid}: {e}", exc_info=True)
logger.error(f"Webhook: error processing {payment_uuid}: {e}", exc_info=True)
return web.Response(status=500, text="Internal error")
return web.Response(text="ok")