Правки по замечаниям
This commit is contained in:
@@ -665,8 +665,6 @@ RIOPAY_CURRENCY=RUB
|
||||
RIOPAY_MIN_AMOUNT_KOPEKS=10000
|
||||
RIOPAY_MAX_AMOUNT_KOPEKS=100000000
|
||||
RIOPAY_WEBHOOK_PATH=/riopay-webhook
|
||||
RIOPAY_WEBHOOK_HOST=0.0.0.0
|
||||
RIOPAY_WEBHOOK_PORT=8090
|
||||
# URL для редиректа после оплаты (опционально)
|
||||
RIOPAY_SUCCESS_URL=
|
||||
RIOPAY_FAIL_URL=
|
||||
|
||||
@@ -1477,7 +1477,7 @@ CONTEST_BUTTON_VISIBLE=true
|
||||
- 💳 **WATA**
|
||||
- 💳 **Freekassa** (NSPK СБП + карты)
|
||||
- 💳 **CloudPayments** (карты + СБП)
|
||||
- - 💳 **RioPay** (карты + СБП)
|
||||
- 💳 **RioPay** (карты + СБП)
|
||||
- 🔥 Автогенерация счетов и webhook-уведомления
|
||||
- 💼 История операций
|
||||
- 🔄 Автоплатёж с настройкой дня списания
|
||||
|
||||
@@ -543,8 +543,6 @@ class Settings(BaseSettings):
|
||||
RIOPAY_MIN_AMOUNT_KOPEKS: int = 10000 # 100₽
|
||||
RIOPAY_MAX_AMOUNT_KOPEKS: int = 100000000 # 1 000 000₽
|
||||
RIOPAY_WEBHOOK_PATH: str = '/riopay-webhook'
|
||||
RIOPAY_WEBHOOK_HOST: str = '0.0.0.0'
|
||||
RIOPAY_WEBHOOK_PORT: int = 8090
|
||||
RIOPAY_SUCCESS_URL: str | None = None
|
||||
RIOPAY_FAIL_URL: str | None = None
|
||||
|
||||
|
||||
@@ -4,13 +4,21 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import PaymentMethod, TransactionType
|
||||
from app.database.crud.riopay import (
|
||||
create_riopay_payment as crud_create_riopay_payment,
|
||||
get_riopay_payment_by_order_id,
|
||||
get_riopay_payment_by_riopay_order_id,
|
||||
update_riopay_payment_status,
|
||||
)
|
||||
from app.database.crud.transaction import create_transaction
|
||||
from app.database.crud.user import get_user_by_id
|
||||
from app.database.models import PaymentMethod, TransactionType, User as UserModel
|
||||
from app.services.riopay_service import riopay_service
|
||||
from app.utils.payment_logger import payment_logger as logger
|
||||
from app.utils.user_utils import format_referrer_info
|
||||
@@ -68,8 +76,7 @@ class RioPayPaymentMixin:
|
||||
return None
|
||||
|
||||
# Получаем telegram_id пользователя для order_id
|
||||
payment_module = import_module('app.services.payment_service')
|
||||
user = await payment_module.get_user_by_id(db, user_id)
|
||||
user = await get_user_by_id(db, user_id)
|
||||
tg_id = user.telegram_id if user else user_id
|
||||
|
||||
# Генерируем уникальный order_id с telegram_id для удобного поиска
|
||||
@@ -111,11 +118,8 @@ class RioPayPaymentMixin:
|
||||
'RioPay API: создан заказ', order_id=order_id, riopay_order_id=riopay_order_id, payment_url=payment_url
|
||||
)
|
||||
|
||||
# Импортируем CRUD модуль
|
||||
riopay_crud = import_module('app.database.crud.riopay')
|
||||
|
||||
# Сохраняем в БД
|
||||
local_payment = await riopay_crud.create_riopay_payment(
|
||||
local_payment = await crud_create_riopay_payment(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
order_id=order_id,
|
||||
@@ -157,31 +161,20 @@ class RioPayPaymentMixin:
|
||||
db: AsyncSession,
|
||||
*,
|
||||
payload: dict[str, Any],
|
||||
raw_body: bytes,
|
||||
signature: str | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Обрабатывает webhook от RioPay.
|
||||
|
||||
Подпись проверяется в webserver/payments.py до вызова этого метода.
|
||||
|
||||
Args:
|
||||
db: Сессия БД
|
||||
payload: JSON тело webhook
|
||||
raw_body: Сырое тело для проверки подписи
|
||||
signature: Подпись из заголовка
|
||||
|
||||
Returns:
|
||||
True если платеж успешно обработан
|
||||
"""
|
||||
try:
|
||||
# Проверка подписи (обязательна)
|
||||
if not signature:
|
||||
logger.warning('RioPay webhook: отсутствует подпись')
|
||||
return False
|
||||
|
||||
if not riopay_service.verify_webhook_signature(raw_body, signature):
|
||||
logger.warning('RioPay webhook: неверная подпись')
|
||||
return False
|
||||
|
||||
# Извлекаем данные из payload
|
||||
riopay_order_id = payload.get('id')
|
||||
external_id = payload.get('externalId')
|
||||
@@ -192,15 +185,12 @@ class RioPayPaymentMixin:
|
||||
logger.warning('RioPay webhook: отсутствуют обязательные поля', payload=payload)
|
||||
return False
|
||||
|
||||
# Импортируем CRUD модуль
|
||||
riopay_crud = import_module('app.database.crud.riopay')
|
||||
|
||||
# Ищем платеж по external_id (наш order_id) или riopay_order_id
|
||||
payment = None
|
||||
if external_id:
|
||||
payment = await riopay_crud.get_riopay_payment_by_order_id(db, external_id)
|
||||
payment = await get_riopay_payment_by_order_id(db, external_id)
|
||||
if not payment and riopay_order_id:
|
||||
payment = await riopay_crud.get_riopay_payment_by_riopay_order_id(db, riopay_order_id)
|
||||
payment = await get_riopay_payment_by_riopay_order_id(db, riopay_order_id)
|
||||
|
||||
if not payment:
|
||||
logger.warning(
|
||||
@@ -227,10 +217,9 @@ class RioPayPaymentMixin:
|
||||
'amount': amount,
|
||||
'payment_type': payload.get('paymentType'),
|
||||
'included_fee': payload.get('includedFee'),
|
||||
'raw_payload': payload,
|
||||
}
|
||||
|
||||
payment = await riopay_crud.update_riopay_payment_status(
|
||||
payment = await update_riopay_payment_status(
|
||||
db=db,
|
||||
payment=payment,
|
||||
status=internal_status,
|
||||
@@ -240,6 +229,18 @@ class RioPayPaymentMixin:
|
||||
callback_payload=callback_payload,
|
||||
)
|
||||
|
||||
# Проверка суммы: payload.amount должен совпадать с суммой в базе
|
||||
if is_paid and amount is not None:
|
||||
expected = payment.amount_kopeks / 100
|
||||
if abs(float(amount) - expected) > 0.01:
|
||||
logger.error(
|
||||
'RioPay amount mismatch',
|
||||
expected=expected,
|
||||
received=amount,
|
||||
order_id=payment.order_id,
|
||||
)
|
||||
return False
|
||||
|
||||
# Финализируем платеж если оплачен
|
||||
if is_paid:
|
||||
return await self._finalize_riopay_payment(
|
||||
@@ -261,14 +262,12 @@ class RioPayPaymentMixin:
|
||||
trigger: str,
|
||||
) -> bool:
|
||||
"""Создаёт транзакцию, начисляет баланс и отправляет уведомления."""
|
||||
payment_module = import_module('app.services.payment_service')
|
||||
|
||||
if payment.transaction_id:
|
||||
logger.info('RioPay платеж уже привязан к транзакции', order_id=payment.order_id, trigger=trigger)
|
||||
return True
|
||||
|
||||
# Получаем пользователя
|
||||
user = await payment_module.get_user_by_id(db, payment.user_id)
|
||||
user = await get_user_by_id(db, payment.user_id)
|
||||
if not user:
|
||||
logger.error(
|
||||
'Пользователь не найден для RioPay платежа',
|
||||
@@ -279,7 +278,7 @@ class RioPayPaymentMixin:
|
||||
return False
|
||||
|
||||
# Создаем транзакцию
|
||||
transaction = await payment_module.create_transaction(
|
||||
transaction = await create_transaction(
|
||||
db,
|
||||
user_id=payment.user_id,
|
||||
type=TransactionType.DEPOSIT,
|
||||
@@ -292,8 +291,7 @@ class RioPayPaymentMixin:
|
||||
)
|
||||
|
||||
# Связываем платеж с транзакцией
|
||||
riopay_crud = import_module('app.database.crud.riopay')
|
||||
await riopay_crud.update_riopay_payment_status(
|
||||
await update_riopay_payment_status(
|
||||
db=db,
|
||||
payment=payment,
|
||||
status=payment.status,
|
||||
@@ -303,13 +301,20 @@ class RioPayPaymentMixin:
|
||||
old_balance = user.balance_kopeks
|
||||
was_first_topup = not user.has_made_first_topup
|
||||
|
||||
# Начисляем баланс
|
||||
user.balance_kopeks += payment.amount_kopeks
|
||||
user.updated_at = datetime.now(UTC)
|
||||
|
||||
# Обновляем флаг первого пополнения в том же коммите
|
||||
# Атомарное начисление баланса через SQL UPDATE, чтобы избежать race condition
|
||||
# при одновременных вебхуках / check_riopay_payment_status
|
||||
update_values: dict[str, Any] = {
|
||||
UserModel.balance_kopeks: UserModel.balance_kopeks + payment.amount_kopeks,
|
||||
UserModel.updated_at: datetime.now(UTC),
|
||||
}
|
||||
if was_first_topup:
|
||||
user.has_made_first_topup = True
|
||||
update_values[UserModel.has_made_first_topup] = True
|
||||
|
||||
await db.execute(
|
||||
update(UserModel)
|
||||
.where(UserModel.id == user.id)
|
||||
.values(update_values)
|
||||
)
|
||||
|
||||
promo_group = user.get_primary_promo_group()
|
||||
subscription = getattr(user, 'subscription', None)
|
||||
@@ -401,9 +406,7 @@ class RioPayPaymentMixin:
|
||||
Проверяет статус платежа через API.
|
||||
"""
|
||||
try:
|
||||
riopay_crud = import_module('app.database.crud.riopay')
|
||||
|
||||
payment = await riopay_crud.get_riopay_payment_by_order_id(db, order_id)
|
||||
payment = await get_riopay_payment_by_order_id(db, order_id)
|
||||
if not payment:
|
||||
logger.warning('RioPay payment not found', order_id=order_id)
|
||||
return None
|
||||
@@ -433,7 +436,7 @@ class RioPayPaymentMixin:
|
||||
'riopay_order_data': order_data,
|
||||
}
|
||||
|
||||
payment = await riopay_crud.update_riopay_payment_status(
|
||||
payment = await update_riopay_payment_status(
|
||||
db=db,
|
||||
payment=payment,
|
||||
status='success',
|
||||
@@ -451,7 +454,7 @@ class RioPayPaymentMixin:
|
||||
)
|
||||
elif internal_status != payment.status:
|
||||
# Обновляем статус если изменился
|
||||
payment = await riopay_crud.update_riopay_payment_status(
|
||||
payment = await update_riopay_payment_status(
|
||||
db=db,
|
||||
payment=payment,
|
||||
status=internal_status,
|
||||
|
||||
@@ -119,7 +119,8 @@ class RioPayService:
|
||||
except Exception:
|
||||
error_msg = text
|
||||
|
||||
logger.error('RioPay create_order error', status_code=response.status, error_msg=error_msg)
|
||||
logger.error('RioPay create_order error', status_code=response.status)
|
||||
logger.debug('RioPay create_order error details', error_msg=error_msg)
|
||||
raise RioPayAPIError(response.status, error_msg)
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
|
||||
@@ -1152,8 +1152,6 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute
|
||||
success = await payment_service.process_riopay_webhook(
|
||||
db,
|
||||
payload=payload,
|
||||
raw_body=raw_body,
|
||||
signature=signature,
|
||||
)
|
||||
if success:
|
||||
return JSONResponse({'status': 'ok'}, status_code=status.HTTP_200_OK)
|
||||
|
||||
@@ -95,13 +95,14 @@ volumes:
|
||||
networks:
|
||||
bot_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
gateway: 172.20.0.1
|
||||
# ipam:
|
||||
# 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 # Используем существующую сеть панели
|
||||
driver: bridge
|
||||
# external: true # Используем существующую сеть панели
|
||||
|
||||
-1729
File diff suppressed because it is too large
Load Diff
-1747
File diff suppressed because it is too large
Load Diff
-1750
File diff suppressed because it is too large
Load Diff
-1616
File diff suppressed because it is too large
Load Diff
-1614
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ from app.services.payment_verification_service import (
|
||||
from app.services.referral_contest_service import referral_contest_service
|
||||
from app.services.remnawave_sync_service import remnawave_sync_service
|
||||
from app.services.reporting_service import reporting_service
|
||||
from app.services.riopay_service import riopay_service
|
||||
from app.services.system_settings_service import bot_configuration_service
|
||||
from app.services.traffic_monitoring_service import traffic_monitoring_scheduler
|
||||
from app.services.version_service import version_service
|
||||
@@ -934,6 +935,11 @@ async def main():
|
||||
except Exception as error:
|
||||
logger.error('Ошибка остановки веб-API', error=error)
|
||||
|
||||
try:
|
||||
await riopay_service.close()
|
||||
except Exception as e:
|
||||
logger.error('Ошибка закрытия сессии RioPay', error=e)
|
||||
|
||||
if 'bot' in locals():
|
||||
try:
|
||||
await bot.session.close()
|
||||
|
||||
Reference in New Issue
Block a user