mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Add Freekassa webhook handling and configuration options
This commit is contained in:
@@ -5,6 +5,8 @@ import hmac
|
||||
import time
|
||||
import logging
|
||||
import asyncio
|
||||
import json
|
||||
import urllib.request
|
||||
from typing import Optional, Dict, Any, Set
|
||||
|
||||
import aiohttp
|
||||
@@ -182,6 +184,7 @@ class FreekassaService:
|
||||
phone: Optional[str] = None,
|
||||
payment_system_id: Optional[int] = None,
|
||||
lang: str = "ru",
|
||||
ip: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Формирует URL для перенаправления на оплату (форма выбора).
|
||||
@@ -189,6 +192,60 @@ class FreekassaService:
|
||||
"""
|
||||
# Приводим amount к int, если это целое число
|
||||
final_amount = int(amount) if float(amount).is_integer() else amount
|
||||
|
||||
# Используем payment_system_id из настроек, если не передан явно
|
||||
ps_id = payment_system_id or settings.FREEKASSA_PAYMENT_SYSTEM_ID
|
||||
|
||||
# Специальная обработка для метода оплаты 44 (NSPK), чтобы работало как в старой версии
|
||||
if ps_id == 44:
|
||||
try:
|
||||
# Определяем IP (важно для API запроса) - здесь синхронно, поэтому лучше иметь передачу IP
|
||||
# Если IP не передан, используем fallback
|
||||
target_ip = ip or "185.92.183.173"
|
||||
target_email = email or "test@example.com"
|
||||
|
||||
params = {
|
||||
"shopId": self.shop_id,
|
||||
"nonce": int(time.time_ns()),
|
||||
"paymentId": str(order_id),
|
||||
"i": 44,
|
||||
"email": target_email,
|
||||
"ip": target_ip,
|
||||
"amount": final_amount,
|
||||
"currency": "RUB"
|
||||
}
|
||||
|
||||
# Генерация подписи
|
||||
params["signature"] = self._generate_api_signature(params)
|
||||
|
||||
logger.info(f"Freekassa synchronous build_payment_url for 44: {params}")
|
||||
|
||||
data_json = json.dumps(params).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
f"{API_BASE_URL}/orders/create",
|
||||
data=data_json,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
resp_body = response.read().decode('utf-8')
|
||||
data = json.loads(resp_body)
|
||||
|
||||
if data.get("type") == "error":
|
||||
logger.error(f"Freekassa build_payment_url error: {data}")
|
||||
# Fallback to standard flow if error? Or raise?
|
||||
# User wants it to work. Raise to see error is safer.
|
||||
# raise Exception(f"Freekassa API Error: {data.get('message')}")
|
||||
# Но чтобы не ломать полностью, можно попробовать вернуть обычную ссылку,
|
||||
# если API не сработал? Нет, вернем ошибку или ссылку из data.
|
||||
|
||||
if data.get("location"):
|
||||
return data.get("location")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create order 44 via sync API: {e}")
|
||||
# Если не получилось, попробуем сгенерировать обычную ссылку как fallback
|
||||
pass
|
||||
|
||||
signature = self.generate_form_signature(final_amount, currency, order_id)
|
||||
|
||||
params = {
|
||||
@@ -205,8 +262,6 @@ class FreekassaService:
|
||||
if phone:
|
||||
params["phone"] = phone
|
||||
|
||||
# Используем payment_system_id из настроек, если не передан явно
|
||||
ps_id = payment_system_id or settings.FREEKASSA_PAYMENT_SYSTEM_ID
|
||||
if ps_id:
|
||||
params["i"] = ps_id
|
||||
|
||||
@@ -238,15 +293,17 @@ class FreekassaService:
|
||||
# Используем payment_system_id из настроек, если не передан явно
|
||||
ps_id = payment_system_id or settings.FREEKASSA_PAYMENT_SYSTEM_ID or 1
|
||||
|
||||
# Определяем публичный IP сервера (127.0.0.1 отклоняется API)
|
||||
server_ip = ip or await get_public_ip()
|
||||
target_email = email or "test@example.com"
|
||||
|
||||
# Определяем публичный IP сервера
|
||||
server_ip = ip or await get_public_ip()
|
||||
|
||||
params = {
|
||||
"shopId": self.shop_id,
|
||||
"nonce": int(time.time_ns()), # Наносекунды для уникальности
|
||||
"paymentId": str(order_id),
|
||||
"i": ps_id,
|
||||
"email": email or "user@example.com",
|
||||
"email": target_email,
|
||||
"ip": server_ip,
|
||||
"amount": final_amount,
|
||||
"currency": currency,
|
||||
|
||||
@@ -798,6 +798,100 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute
|
||||
|
||||
routes_registered = True
|
||||
|
||||
if settings.is_freekassa_enabled():
|
||||
@router.options(settings.FREEKASSA_WEBHOOK_PATH)
|
||||
async def freekassa_options() -> Response:
|
||||
return Response(
|
||||
status_code=status.HTTP_200_OK,
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get(settings.FREEKASSA_WEBHOOK_PATH)
|
||||
async def freekassa_health() -> JSONResponse:
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "freekassa_webhook",
|
||||
"enabled": settings.is_freekassa_enabled(),
|
||||
}
|
||||
)
|
||||
|
||||
@router.post(settings.FREEKASSA_WEBHOOK_PATH)
|
||||
async def freekassa_webhook(request: Request) -> Response:
|
||||
# Получаем IP клиента с учетом прокси
|
||||
x_forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if x_forwarded_for:
|
||||
client_ip = x_forwarded_for.split(",")[0].strip()
|
||||
else:
|
||||
real_ip = request.headers.get("X-Real-IP")
|
||||
if real_ip:
|
||||
client_ip = real_ip.strip()
|
||||
else:
|
||||
client_ip = request.client.host if request.client else "127.0.0.1"
|
||||
|
||||
# Получаем данные формы
|
||||
try:
|
||||
form_data = await request.form()
|
||||
except Exception:
|
||||
logger.error("Freekassa webhook: не удалось прочитать данные формы")
|
||||
return Response("Error reading form data", status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Извлекаем параметры
|
||||
merchant_id = form_data.get("MERCHANT_ID")
|
||||
amount = form_data.get("AMOUNT")
|
||||
order_id = form_data.get("MERCHANT_ORDER_ID")
|
||||
sign = form_data.get("SIGN")
|
||||
intid = form_data.get("intid")
|
||||
cur_id = form_data.get("CUR_ID")
|
||||
|
||||
if not all([merchant_id, amount, order_id, sign, intid]):
|
||||
logger.warning("Freekassa webhook: отсутствуют обязательные параметры")
|
||||
return Response("Missing parameters", status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Преобразуем типы
|
||||
try:
|
||||
merchant_id_int = int(merchant_id)
|
||||
amount_float = float(amount)
|
||||
cur_id_int = int(cur_id) if cur_id else None
|
||||
except ValueError:
|
||||
logger.warning("Freekassa webhook: неверный формат параметров")
|
||||
return Response("Invalid parameters format", status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Обрабатываем callback
|
||||
db_generator = get_db()
|
||||
try:
|
||||
db = await db_generator.__anext__()
|
||||
except StopAsyncIteration:
|
||||
return Response("DB Error", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
try:
|
||||
success = await payment_service.process_freekassa_webhook(
|
||||
db,
|
||||
merchant_id=merchant_id_int,
|
||||
amount=amount_float,
|
||||
order_id=order_id,
|
||||
sign=sign,
|
||||
intid=intid,
|
||||
cur_id=cur_id_int,
|
||||
client_ip=client_ip,
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
await db_generator.__anext__()
|
||||
except StopAsyncIteration:
|
||||
pass
|
||||
|
||||
if success:
|
||||
return Response("YES", status_code=status.HTTP_200_OK)
|
||||
|
||||
return Response("Error", status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
routes_registered = True
|
||||
|
||||
if routes_registered:
|
||||
@router.get("/health/payment-webhooks")
|
||||
async def payment_webhooks_health() -> JSONResponse:
|
||||
@@ -813,6 +907,7 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute
|
||||
"pal24_enabled": settings.is_pal24_enabled(),
|
||||
"platega_enabled": settings.is_platega_enabled(),
|
||||
"cloudpayments_enabled": settings.is_cloudpayments_enabled(),
|
||||
"freekassa_enabled": settings.is_freekassa_enabled(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ def create_unified_app(
|
||||
"pal24": settings.is_pal24_enabled(),
|
||||
"wata": settings.is_wata_enabled(),
|
||||
"heleket": settings.is_heleket_enabled(),
|
||||
"freekassa": settings.is_freekassa_enabled(),
|
||||
}
|
||||
|
||||
if enable_telegram_webhook:
|
||||
|
||||
2
main.py
2
main.py
@@ -648,6 +648,8 @@ async def main():
|
||||
webhook_lines.append(f"WATA: {_fmt(settings.WATA_WEBHOOK_PATH)}")
|
||||
if settings.is_heleket_enabled():
|
||||
webhook_lines.append(f"Heleket: {_fmt(settings.HELEKET_WEBHOOK_PATH)}")
|
||||
if settings.is_freekassa_enabled():
|
||||
webhook_lines.append(f"Freekassa: {_fmt(settings.FREEKASSA_WEBHOOK_PATH)}")
|
||||
|
||||
timeline.log_section(
|
||||
"Активные webhook endpoints",
|
||||
|
||||
Reference in New Issue
Block a user