""" Тест webhook-сервера Heleket. Использование: # Отправить тестовый webhook локально (симуляция от Heleket) python test_webhook.py --local # Попросить Heleket повторно отправить webhook по uuid платежа python test_webhook.py --resend --uuid # Попросить Heleket повторно отправить по order_id python test_webhook.py --resend --order-id """ import asyncio import sys import argparse import base64 import hashlib import json from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) import aiohttp from config.settings import settings def _make_sign_request(body_bytes: bytes, api_key: str) -> str: """Подпись исходящего запроса к Heleket API.""" b64 = base64.b64encode(body_bytes).decode("utf-8") return hashlib.md5((b64 + api_key).encode("utf-8")).hexdigest() def _make_webhook_sign(payload: dict, api_key: str) -> str: """Подпись входящего webhook (как Heleket её считает).""" 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 escapes / b64 = base64.b64encode(data_json.encode("utf-8")).decode("utf-8") return hashlib.md5((b64 + api_key).encode("utf-8")).hexdigest() async def test_local(port: int, api_key: str): """Отправляет тестовый webhook на локальный сервер с правильной подписью.""" url = f"http://127.0.0.1:{port}/heleket/webhook" payload = { "type": "payment", "uuid": "test-uuid-0000-0000-0000-000000000001", "order_id": "test_order_123", "amount": "100.00", "payment_amount": "100.00", "payment_amount_usd": "1.10", "merchant_amount": "98.00", "commission": "2.00", "is_final": True, "status": "paid", "currency": "RUB", "payer_currency": "USDT", "network": "tron", "from": "TTestWalletAddress123", "txid": "test_txid_abc123", } payload["sign"] = _make_webhook_sign(payload, api_key) print(f"\n→ POST {url}") print(f" payload: {json.dumps(payload, ensure_ascii=False, indent=2)}") try: async with aiohttp.ClientSession() as session: async with session.post( url, json=payload, timeout=aiohttp.ClientTimeout(total=10), ) as resp: body = await resp.text() print(f"\n← HTTP {resp.status}: {body}") if resp.status == 200: print("✅ Webhook принят успешно") elif resp.status == 400: print("❌ Ошибка подписи или неверный формат") elif resp.status == 403: print("❌ Заблокирован по IP (HELEKET_WHITELIST_IP=true)") print(" Для локального теста отключи: HELEKET_WHITELIST_IP=false") except aiohttp.ClientConnectorError: print(f"❌ Не могу подключиться к {url}") print(" Убедись что бот запущен и webhook-сервер работает") except Exception as e: print(f"❌ Ошибка: {e}") async def resend_webhook(uuid: str = None, order_id: str = None): """Просит Heleket повторно отправить webhook на наш сервер.""" merchant_uuid = settings.HELEKET_MERCHANT_UUID api_key = settings.HELEKET_API_KEY if not merchant_uuid or not api_key: print("❌ HELEKET_MERCHANT_UUID или HELEKET_API_KEY не заданы в .env") return if not uuid and not order_id: print("❌ Укажи --uuid или --order-id") return payload = {} if uuid: payload["uuid"] = uuid if order_id: payload["order_id"] = order_id body = json.dumps(payload, separators=(",", ":")).encode("utf-8") sign = _make_sign_request(body, api_key) headers = { "merchant": merchant_uuid, "sign": sign, "Content-Type": "application/json", } print(f"\n→ POST https://api.heleket.com/v1/payment/resend") print(f" payload: {payload}") try: async with aiohttp.ClientSession() as session: async with session.post( "https://api.heleket.com/v1/payment/resend", data=body, headers=headers, timeout=aiohttp.ClientTimeout(total=15), ) as resp: data = await resp.json() print(f"\n← HTTP {resp.status}: {json.dumps(data, ensure_ascii=False, indent=2)}") if data.get("state") == 0: print("✅ Heleket повторно отправит webhook на твой сервер") print(f" URL: {settings.HELEKET_WEBHOOK_URL}") else: msg = data.get("message", "unknown error") print(f"❌ Ошибка: {msg}") if "Notification not found" in str(msg): print(" Платёж создан без url_callback — webhook не настроен") elif "Too much resend" in str(msg): print(" Превышен лимит 10 повторных отправок") except Exception as e: print(f"❌ Ошибка запроса: {e}") def main(): parser = argparse.ArgumentParser(description="Тест webhook Heleket") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--local", action="store_true", help="Отправить тестовый webhook на локальный сервер") group.add_argument("--resend", action="store_true", help="Попросить Heleket повторно отправить webhook") parser.add_argument("--uuid", help="UUID платежа (для --resend)") parser.add_argument("--order-id", help="Order ID платежа (для --resend)") parser.add_argument("--port", type=int, default=settings.HELEKET_WEBHOOK_PORT, help=f"Порт webhook-сервера (по умолчанию: {settings.HELEKET_WEBHOOK_PORT})") args = parser.parse_args() api_key = settings.HELEKET_API_KEY if not api_key: print("❌ HELEKET_API_KEY не задан в .env") sys.exit(1) if args.local: print("🧪 Тест локального webhook-сервера") if settings.HELEKET_WHITELIST_IP: print("⚠️ HELEKET_WHITELIST_IP=true — локальный тест будет заблокирован по IP") print(" Временно поставь HELEKET_WHITELIST_IP=false для теста") asyncio.run(test_local(args.port, api_key)) else: print("📤 Запрос повторной отправки webhook через Heleket API") asyncio.run(resend_webhook(args.uuid, args.order_id)) if __name__ == "__main__": main()