From b5234f32653d59074b85c2327351ca74dcbd983c Mon Sep 17 00:00:00 2001 From: evansvl Date: Sun, 11 Jan 2026 05:45:09 +0300 Subject: [PATCH] Enhance Freekassa payment handling and improve Docker Compose configuration --- app/external/yookassa_webhook.py | 8 ++- app/handlers/admin/payments.py | 12 +++++ app/services/payment/freekassa.py | 81 ++++++++++++++++++++++++++----- docker-compose.local.yml | 50 +++++++++++-------- docker-compose.yml | 44 ++++++++++------- 5 files changed, 143 insertions(+), 52 deletions(-) diff --git a/app/external/yookassa_webhook.py b/app/external/yookassa_webhook.py index 6ea1811d..246c948a 100644 --- a/app/external/yookassa_webhook.py +++ b/app/external/yookassa_webhook.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import logging import json @@ -9,13 +11,15 @@ from ipaddress import ( ip_address, ip_network, ) -from typing import Iterable, Optional, Dict, Any, List, Union, Tuple +from typing import Iterable, Optional, Dict, Any, List, Union, Tuple, TYPE_CHECKING from aiohttp import web from app.config import settings -from app.services.payment_service import PaymentService from app.database.database import get_db +if TYPE_CHECKING: + from app.services.payment_service import PaymentService + logger = logging.getLogger(__name__) diff --git a/app/handlers/admin/payments.py b/app/handlers/admin/payments.py index 1d4ef7c3..3205e01c 100644 --- a/app/handlers/admin/payments.py +++ b/app/handlers/admin/payments.py @@ -135,6 +135,16 @@ def _status_info( return "✅", texts.t("ADMIN_PAYMENT_STATUS_PAID", "✅ Paid") return "⏳", texts.t("ADMIN_PAYMENT_STATUS_PENDING", "⏳ Pending") + if record.method == PaymentMethod.FREEKASSA: + mapping = { + "pending": ("⏳", texts.t("ADMIN_PAYMENT_STATUS_PENDING", "⏳ Pending")), + "success": ("✅", texts.t("ADMIN_PAYMENT_STATUS_PAID", "✅ Paid")), + "paid": ("✅", texts.t("ADMIN_PAYMENT_STATUS_PAID", "✅ Paid")), + "canceled": ("❌", texts.t("ADMIN_PAYMENT_STATUS_CANCELED", "❌ Cancelled")), + "error": ("❌", texts.t("ADMIN_PAYMENT_STATUS_FAILED", "❌ Failed")), + } + return mapping.get(status, ("❓", texts.t("ADMIN_PAYMENT_STATUS_UNKNOWN", "❓ Unknown"))) + return "❓", texts.t("ADMIN_PAYMENT_STATUS_UNKNOWN", "❓ Unknown") @@ -158,6 +168,8 @@ def _is_checkable(record: PendingPayment) -> bool: return status in {"pending", "waiting_for_capture"} if record.method == PaymentMethod.CRYPTOBOT: return status in {"active"} + if record.method == PaymentMethod.FREEKASSA: + return status in {"pending", ""} return False diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py index 163b2734..b6e13fb2 100644 --- a/app/services/payment/freekassa.py +++ b/app/services/payment/freekassa.py @@ -505,17 +505,7 @@ class FreekassaPaymentMixin: local_payment_id: int, ) -> Optional[Dict[str, Any]]: """ - Проверяет статус платежа Freekassa по локальному ID. - - Freekassa не предоставляет API для проверки статуса платежа, - поэтому возвращаем текущее состояние из БД. - - Args: - db: Сессия БД - local_payment_id: Внутренний ID платежа - - Returns: - Dict с информацией о платеже или None если не найден + Проверяет статус платежа Freekassa по локальному ID через API. """ freekassa_crud = import_module("app.database.crud.freekassa") @@ -524,8 +514,73 @@ class FreekassaPaymentMixin: logger.warning("Freekassa payment not found: id=%s", local_payment_id) return None - # Freekassa не имеет API для проверки статуса, - # информация приходит только через webhook + if payment.is_paid: + return { + "payment": payment, + "status": "success", + "is_paid": True, + } + + if not settings.FREEKASSA_API_KEY: + return { + "payment": payment, + "status": payment.status or "pending", + "is_paid": payment.is_paid, + } + + try: + # Запрашиваем статус заказа в Freekassa + response = await freekassa_service.get_order_status(payment.order_id) + + # Freekassa возвращает список заказов + orders = response.get("orders", []) + target_order = None + + # Ищем наш заказ в списке + for order in orders: + # В ответе API поле называется merchant_order_id, а не paymentId + # Поддерживаем оба варианта на всякий случай + order_key = str(order.get("merchant_order_id") or order.get("paymentId")) + if order_key == str(payment.order_id): + target_order = order + break + + if target_order: + # Статус 1 = Оплачен + fk_status = int(target_order.get("status", 0)) + + if fk_status == 1: + logger.info("Freekassa payment %s confirmed via API", payment.order_id) + + callback_payload = { + "check_source": "api", + "fk_order_data": target_order, + } + + # ID заказа на стороне FK (fk_order_id или id) + fk_intid = str(target_order.get("fk_order_id") or target_order.get("id")) + + # Обновляем статус + payment = await freekassa_crud.update_freekassa_payment_status( + db=db, + payment=payment, + status="success", + is_paid=True, + freekassa_order_id=fk_intid, + payment_system_id=int(target_order.get("curID")) if target_order.get("curID") else None, + callback_payload=callback_payload, + ) + + # Финализируем + await self._finalize_freekassa_payment( + db, + payment, + intid=fk_intid, + trigger="api_check", + ) + except Exception as e: + logger.error("Error checking Freekassa payment status: %s", e) + return { "payment": payment, "status": payment.status or "pending", diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 26893b7c..61bb6c5f 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -7,14 +7,18 @@ services: POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot} POSTGRES_USER: ${POSTGRES_USER:-remnawave_user} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123} - POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" + POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=C' volumes: - postgres_data:/var/lib/postgresql/data networks: - bot_network - - remnawave-network # Подключаем к сети панели + - remnawave-network # Подключаем к сети панели healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"] + test: + [ + 'CMD-SHELL', + 'pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}', + ] interval: 30s timeout: 5s retries: 5 @@ -29,9 +33,9 @@ services: - redis_data:/data networks: - bot_network - - remnawave-network # Подключаем к сети панели + - remnawave-network # Подключаем к сети панели healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 30s timeout: 10s retries: 3 @@ -48,16 +52,16 @@ services: env_file: - .env environment: - DOCKER_ENV: "true" - DATABASE_MODE: "auto" - POSTGRES_HOST: "postgres" - POSTGRES_PORT: "5432" - POSTGRES_DB: "${POSTGRES_DB:-remnawave_bot}" - POSTGRES_USER: "${POSTGRES_USER:-remnawave_user}" - POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-secure_password_123}" - REDIS_URL: "redis://redis:6379/0" - TZ: "Europe/Moscow" - LOCALES_PATH: "${LOCALES_PATH:-/app/locales}" + DOCKER_ENV: 'true' + DATABASE_MODE: 'auto' + POSTGRES_HOST: 'postgres' + POSTGRES_PORT: '5432' + POSTGRES_DB: '${POSTGRES_DB:-remnawave_bot}' + POSTGRES_USER: '${POSTGRES_USER:-remnawave_user}' + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-secure_password_123}' + REDIS_URL: 'redis://redis:6379/0' + TZ: 'Europe/Moscow' + LOCALES_PATH: '${LOCALES_PATH:-/app/locales}' volumes: - ./logs:/app/logs:rw - ./data:/app/data:rw @@ -66,12 +70,16 @@ services: - /etc/localtime:/etc/localtime:ro - ./vpn_logo.png:/app/vpn_logo.png:ro ports: - - "${WEB_API_PORT:-8080}:8080" + - '${WEB_API_PORT:-8080}:8080' networks: - bot_network - - remnawave-network # Подключаем к сети панели + - remnawave-network # Подключаем к сети панели healthcheck: - test: ["CMD-SHELL", "python -c \"import requests, os; requests.get('http://localhost:8080/health', headers={'X-API-Key': os.environ.get('WEB_API_DEFAULT_TOKEN')}, timeout=5) or exit(1)\""] + test: + [ + 'CMD-SHELL', + 'python -c "import requests, os; requests.get(''http://localhost:8080/health'', headers={''X-API-Key'': os.environ.get(''WEB_API_DEFAULT_TOKEN'')}, timeout=5) or exit(1)"', + ] interval: 60s timeout: 10s retries: 3 @@ -90,7 +98,9 @@ networks: config: - subnet: 172.20.0.0/16 gateway: 172.20.0.1 - + driver_opts: + com.docker.network.driver.mtu: 1350 + remnawave-network: name: remnawave-network - external: true # Используем существующую сеть панели + external: true # Используем существующую сеть панели diff --git a/docker-compose.yml b/docker-compose.yml index c62611f2..1d015da6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,13 +7,17 @@ services: POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot} POSTGRES_USER: ${POSTGRES_USER:-remnawave_user} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123} - POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" + POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=C' volumes: - postgres_data:/var/lib/postgresql/data networks: - bot_network healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}"] + test: + [ + 'CMD-SHELL', + 'pg_isready -U ${POSTGRES_USER:-remnawave_user} -d ${POSTGRES_DB:-remnawave_bot}', + ] interval: 30s timeout: 5s retries: 5 @@ -29,7 +33,7 @@ services: networks: - bot_network healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 30s timeout: 10s retries: 3 @@ -46,18 +50,18 @@ services: env_file: - .env environment: - DOCKER_ENV: "true" - DATABASE_MODE: "auto" - POSTGRES_HOST: "postgres" - POSTGRES_PORT: "5432" - POSTGRES_DB: "${POSTGRES_DB:-remnawave_bot}" - POSTGRES_USER: "${POSTGRES_USER:-remnawave_user}" - POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-secure_password_123}" - - REDIS_URL: "redis://redis:6379/0" - - TZ: "Europe/Moscow" - LOCALES_PATH: "${LOCALES_PATH:-/app/locales}" + DOCKER_ENV: 'true' + DATABASE_MODE: 'auto' + POSTGRES_HOST: 'postgres' + POSTGRES_PORT: '5432' + POSTGRES_DB: '${POSTGRES_DB:-remnawave_bot}' + POSTGRES_USER: '${POSTGRES_USER:-remnawave_user}' + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-secure_password_123}' + + REDIS_URL: 'redis://redis:6379/0' + + TZ: 'Europe/Moscow' + LOCALES_PATH: '${LOCALES_PATH:-/app/locales}' volumes: # Логи - ./logs:/app/logs:rw @@ -72,11 +76,15 @@ services: # Логотип для сообщений - ./vpn_logo.png:/app/vpn_logo.png:ro ports: - - "${WEB_API_PORT:-8080}:8080" + - '${WEB_API_PORT:-8080}:8080' networks: - bot_network healthcheck: - test: ["CMD-SHELL", "python -c \"import requests, os; requests.get('http://localhost:8080/health', headers={'X-API-Key': os.environ.get('WEB_API_DEFAULT_TOKEN')}, timeout=5) or exit(1)\""] + test: + [ + 'CMD-SHELL', + 'python -c "import requests, os; requests.get(''http://localhost:8080/health'', headers={''X-API-Key'': os.environ.get(''WEB_API_DEFAULT_TOKEN'')}, timeout=5) or exit(1)"', + ] interval: 60s timeout: 10s retries: 3 @@ -95,3 +103,5 @@ networks: config: - subnet: 172.20.0.0/16 gateway: 172.20.0.1 + driver_opts: + com.docker.network.driver.mtu: 1350