reviewers fix

This commit is contained in:
firewookie
2026-03-08 09:22:44 +05:00
parent a6849242ff
commit 848c9f71a2
7 changed files with 112 additions and 78 deletions
+10 -10
View File
@@ -1,6 +1,5 @@
"""CRUD операции для платежей RioPay."""
import json
from datetime import UTC, datetime
import structlog
@@ -25,7 +24,7 @@ async def create_riopay_payment(
payment_method: str | None = None,
riopay_order_id: str | None = None,
expires_at: datetime | None = None,
metadata_json: str | None = None,
metadata_json: dict | None = None,
) -> RioPayPayment:
"""Создает запись о платеже RioPay."""
payment = RioPayPayment(
@@ -38,14 +37,14 @@ async def create_riopay_payment(
payment_method=payment_method,
riopay_order_id=riopay_order_id,
expires_at=expires_at,
metadata_json=json.loads(metadata_json) if metadata_json else None,
metadata_json=metadata_json,
status='pending',
is_paid=False,
)
db.add(payment)
await db.commit()
await db.refresh(payment)
logger.info('Создан платеж RioPay: order_id=, user_id', order_id=order_id, user_id=user_id)
logger.info('Создан платеж RioPay', order_id=order_id, user_id=user_id)
return payment
@@ -72,7 +71,7 @@ async def update_riopay_payment_status(
payment: RioPayPayment,
*,
status: str,
is_paid: bool = False,
is_paid: bool | None = None,
riopay_order_id: str | None = None,
payment_method: str | None = None,
callback_payload: dict | None = None,
@@ -80,11 +79,12 @@ async def update_riopay_payment_status(
) -> RioPayPayment:
"""Обновляет статус платежа."""
payment.status = status
payment.is_paid = is_paid
payment.updated_at = datetime.now(UTC)
if is_paid:
payment.paid_at = datetime.now(UTC)
if is_paid is not None:
payment.is_paid = is_paid
if is_paid:
payment.paid_at = datetime.now(UTC)
if riopay_order_id:
payment.riopay_order_id = riopay_order_id
if payment_method is not None:
@@ -97,10 +97,10 @@ async def update_riopay_payment_status(
await db.commit()
await db.refresh(payment)
logger.info(
'Обновлен статус платежа RioPay: order_id=, status=, is_paid',
'Обновлен статус платежа RioPay',
order_id=payment.order_id,
status=status,
is_paid=is_paid,
is_paid=payment.is_paid,
)
return payment
+1 -1
View File
@@ -721,7 +721,7 @@ class RioPayPayment(Base):
__tablename__ = 'riopay_payments'
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True)
# Идентификаторы
order_id = Column(String(64), unique=True, nullable=False, index=True) # Наш internal ID
+24 -29
View File
@@ -18,6 +18,19 @@ from app.utils.decorators import error_handler
logger = structlog.get_logger(__name__)
def _check_topup_restriction(db_user: User, texts) -> InlineKeyboardMarkup | None:
"""Проверяет ограничение на пополнение. Возвращает клавиатуру если ограничен, иначе None."""
if not getattr(db_user, 'restriction_topup', False):
return None
keyboard = []
support_url = settings.get_support_contact_url()
if support_url:
keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
async def _create_riopay_payment_and_respond(
message_or_callback,
db_user: User,
@@ -111,7 +124,7 @@ async def _create_riopay_payment_and_respond(
parse_mode='HTML',
)
logger.info('RioPay payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub)
logger.info('RioPay payment created', telegram_id=db_user.telegram_id, amount_rub=amount_rub)
@error_handler
@@ -127,19 +140,13 @@ async def process_riopay_payment_amount(
"""
texts = get_texts(db_user.language)
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
await message.answer(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
reply_markup=restriction_kb,
)
await state.clear()
return
@@ -191,19 +198,13 @@ async def start_riopay_topup(
"""
texts = get_texts(db_user.language)
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
await callback.message.edit_text(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
reply_markup=restriction_kb,
)
return
@@ -261,7 +262,7 @@ async def process_riopay_custom_amount(
try:
amount_text = message.text.replace(',', '.').replace(' ', '').strip()
amount_rubles = float(amount_text)
amount_kopeks = int(amount_rubles * 100)
amount_kopeks = round(amount_rubles * 100)
except (ValueError, TypeError):
await message.answer(
texts.t(
@@ -313,19 +314,13 @@ async def process_riopay_quick_amount(
await callback.answer('Invalid amount', show_alert=True)
return
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
await callback.message.edit_text(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
reply_markup=restriction_kb,
)
return
+8 -9
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import json
import uuid
from datetime import UTC, datetime, timedelta
from importlib import import_module
@@ -125,7 +124,7 @@ class RioPayPaymentMixin:
riopay_order_id=riopay_order_id,
payment_method=result.get('paymentType'),
expires_at=expires_at,
metadata_json=json.dumps(metadata, ensure_ascii=False),
metadata_json=metadata,
)
logger.info(
@@ -264,7 +263,7 @@ class RioPayPaymentMixin:
if payment.transaction_id:
logger.info(
'RioPay платеж уже привязан к транзакции (trigger=)', order_id=payment.order_id, trigger=trigger
'RioPay платеж уже привязан к транзакции', order_id=payment.order_id, trigger=trigger
)
return True
@@ -272,7 +271,7 @@ class RioPayPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
'Пользователь не найден для RioPay платежа (trigger=)',
'Пользователь не найден для RioPay платежа',
user_id=payment.user_id,
order_id=payment.order_id,
trigger=trigger,
@@ -308,6 +307,10 @@ class RioPayPaymentMixin:
user.balance_kopeks += payment.amount_kopeks
user.updated_at = datetime.now(UTC)
# Обновляем флаг первого пополнения в том же коммите
if was_first_topup:
user.has_made_first_topup = True
promo_group = user.get_primary_promo_group()
subscription = getattr(user, 'subscription', None)
referrer_info = format_referrer_info(user)
@@ -323,10 +326,6 @@ class RioPayPaymentMixin:
except Exception as error:
logger.error('Ошибка обработки реферального пополнения RioPay', error=error)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
await db.refresh(payment)
@@ -385,7 +384,7 @@ class RioPayPaymentMixin:
)
logger.info(
'Обработан RioPay платеж для пользователя (trigger=)',
'Обработан RioPay платеж',
order_id=payment.order_id,
user_id=payment.user_id,
trigger=trigger,
+41 -25
View File
@@ -15,11 +15,21 @@ logger = structlog.get_logger(__name__)
API_BASE_URL = 'https://api.riopay.online'
class RioPayAPIError(Exception):
"""Ошибка API RioPay."""
def __init__(self, status_code: int, message: str):
self.status_code = status_code
self.message = message
super().__init__(f'RioPay API error ({status_code}): {message}')
class RioPayService:
"""Сервис для работы с API RioPay."""
def __init__(self):
self._api_token: str | None = None
self._session: aiohttp.ClientSession | None = None
@property
def api_token(self) -> str:
@@ -39,6 +49,20 @@ class RioPayService:
'Content-Type': 'application/json',
}
async def _get_session(self) -> aiohttp.ClientSession:
"""Возвращает переиспользуемую HTTP-сессию."""
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
)
return self._session
async def close(self) -> None:
"""Закрывает HTTP-сессию."""
if self._session and not self._session.closed:
await self._session.close()
self._session = None
async def create_order(
self,
*,
@@ -76,23 +100,19 @@ class RioPayService:
)
try:
async with (
aiohttp.ClientSession() as session,
session.post(
f'{API_BASE_URL}/v1/orders',
json=payload,
headers=self._get_headers(),
timeout=aiohttp.ClientTimeout(total=30),
) as response,
):
text = await response.text()
logger.info('RioPay API response', status_code=response.status, text=text)
session = await self._get_session()
async with session.post(
f'{API_BASE_URL}/v1/orders',
json=payload,
headers=self._get_headers(),
) as response:
if response.status == 201:
data = await response.json(content_type=None)
logger.info('RioPay API order created', status_code=response.status, order_id=data.get('id'))
return data
# Ошибка
text = await response.text()
try:
error_data = await response.json(content_type=None)
error_msg = error_data.get('message') or error_data.get('error') or text
@@ -100,7 +120,7 @@ class RioPayService:
error_msg = text
logger.error('RioPay create_order error', status_code=response.status, error_msg=error_msg)
raise Exception(f'RioPay API error ({response.status}): {error_msg}')
raise RioPayAPIError(response.status, error_msg)
except aiohttp.ClientError as e:
logger.exception('RioPay API connection error', error=e)
@@ -114,21 +134,17 @@ class RioPayService:
logger.info('RioPay get_order', order_id=order_id)
try:
async with (
aiohttp.ClientSession() as session,
session.get(
f'{API_BASE_URL}/v1/orders/{order_id}',
headers=self._get_headers(),
timeout=aiohttp.ClientTimeout(total=30),
) as response,
):
text = await response.text()
logger.info('RioPay get_order response', status_code=response.status, text=text)
session = await self._get_session()
async with session.get(
f'{API_BASE_URL}/v1/orders/{order_id}',
headers=self._get_headers(),
) as response:
if response.status == 200:
return await response.json(content_type=None)
raise Exception(f'RioPay get_order error ({response.status}): {text}')
text = await response.text()
logger.error('RioPay get_order error', status_code=response.status)
raise RioPayAPIError(response.status, text)
except aiohttp.ClientError as e:
logger.exception('RioPay API connection error', error=e)
+5 -1
View File
@@ -1166,7 +1166,11 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute
if success:
return JSONResponse({'status': 'ok'}, status_code=status.HTTP_200_OK)
logger.error('RioPay webhook processing failed', payload=payload)
logger.error(
'RioPay webhook processing failed',
order_id=payload.get('id'),
status=payload.get('status'),
)
return Response('Error', status_code=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.exception('RioPay webhook processing error', e=e)
@@ -11,15 +11,35 @@ from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = '0016'
revision: str = '0015'
down_revision: Union[str, None] = '0014'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
pass
op.create_table(
'riopay_payments',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), nullable=False, index=True),
sa.Column('order_id', sa.String(64), unique=True, nullable=False, index=True),
sa.Column('riopay_order_id', sa.String(64), unique=True, nullable=True, index=True),
sa.Column('amount_kopeks', sa.Integer(), nullable=False),
sa.Column('currency', sa.String(10), nullable=False, server_default='RUB'),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('status', sa.String(32), nullable=False, server_default='pending'),
sa.Column('is_paid', sa.Boolean(), server_default=sa.text('0')),
sa.Column('payment_url', sa.Text(), nullable=True),
sa.Column('payment_method', sa.String(32), nullable=True),
sa.Column('metadata_json', sa.JSON(), nullable=True),
sa.Column('callback_payload', sa.JSON(), nullable=True),
sa.Column('paid_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()),
sa.Column('transaction_id', sa.Integer(), sa.ForeignKey('transactions.id'), nullable=True),
)
def downgrade() -> None:
pass
op.drop_table('riopay_payments')