reviewers fix
This commit is contained in:
+10
-10
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user