mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-21 20:01:47 +00:00
Добавлена возможность ограничивать пользователям:
- Пополнение баланса (restriction_topup)
- Покупку/продление подписки (restriction_subscription)
Изменения:
- models.py: добавлены поля restriction_topup, restriction_subscription,
restriction_reason и property has_restrictions
- universal_migration.py: миграция для новых полей
- admin/users.py: меню управления ограничениями в карточке пользователя
- keyboards/admin.py: клавиатура ограничений с toggle-кнопками
- states.py: состояние editing_user_restriction_reason
Проверки ограничений добавлены на двух уровнях:
- start_*_payment: при выборе метода оплаты
- process_*_payment_amount: при создании платежа
Затронутые провайдеры: stars, yookassa, mulenpay, wata, pal24,
cryptobot, heleket, platega, tribute, cloudpayments
При ограничении пользователь видит причину и кнопку "Обжаловать",
ведущую на контакт поддержки из настроек.
506 lines
16 KiB
Python
506 lines
16 KiB
Python
"""Handler for CloudPayments balance top-up."""
|
||
|
||
import logging
|
||
|
||
from aiogram import types
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.database.models import User
|
||
from app.keyboards.inline import get_back_keyboard
|
||
from app.localization.texts import get_texts
|
||
from app.services.payment_service import PaymentService
|
||
from app.states import BalanceStates
|
||
from app.utils.decorators import error_handler
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def _create_cloudpayments_payment_and_respond(
|
||
message_or_callback,
|
||
db_user: User,
|
||
db: AsyncSession,
|
||
amount_kopeks: int,
|
||
edit_message: bool = False,
|
||
):
|
||
"""
|
||
Common logic for creating CloudPayments payment and sending response.
|
||
|
||
Args:
|
||
message_or_callback: Either a Message or CallbackQuery object
|
||
db_user: User object
|
||
db: Database session
|
||
amount_kopeks: Amount in kopeks
|
||
edit_message: Whether to edit existing message or send new one
|
||
"""
|
||
texts = get_texts(db_user.language)
|
||
amount_rub = amount_kopeks / 100
|
||
|
||
# Create payment
|
||
payment_service = PaymentService()
|
||
|
||
description = settings.PAYMENT_BALANCE_TEMPLATE.format(
|
||
service_name=settings.PAYMENT_SERVICE_NAME,
|
||
description=settings.CLOUDPAYMENTS_DESCRIPTION,
|
||
)
|
||
|
||
result = await payment_service.create_cloudpayments_payment(
|
||
db=db,
|
||
user_id=db_user.id,
|
||
amount_kopeks=amount_kopeks,
|
||
description=description,
|
||
telegram_id=db_user.telegram_id,
|
||
language=db_user.language,
|
||
)
|
||
|
||
if not result:
|
||
error_text = texts.t(
|
||
"PAYMENT_CREATE_ERROR",
|
||
"Не удалось создать платёж. Попробуйте позже.",
|
||
)
|
||
if edit_message:
|
||
await message_or_callback.edit_text(
|
||
error_text,
|
||
reply_markup=get_back_keyboard(db_user.language),
|
||
parse_mode="HTML",
|
||
)
|
||
else:
|
||
await message_or_callback.answer(
|
||
error_text,
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
payment_url = result.get("payment_url")
|
||
|
||
# Create keyboard with payment button
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t(
|
||
"PAY_BUTTON",
|
||
"💳 Оплатить {amount}₽",
|
||
).format(amount=f"{amount_rub:.0f}"),
|
||
url=payment_url,
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t("BACK_BUTTON", "◀️ Назад"),
|
||
callback_data="menu_balance",
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
response_text = texts.t(
|
||
"CLOUDPAYMENTS_PAYMENT_CREATED",
|
||
"💳 <b>Оплата банковской картой</b>\n\n"
|
||
"Сумма: <b>{amount}₽</b>\n\n"
|
||
"Нажмите кнопку ниже для оплаты.\n"
|
||
"После успешной оплаты баланс будет пополнен автоматически.",
|
||
).format(amount=f"{amount_rub:.2f}")
|
||
|
||
if edit_message:
|
||
await message_or_callback.edit_text(
|
||
response_text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
else:
|
||
await message_or_callback.answer(
|
||
response_text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
logger.info(
|
||
"CloudPayments payment created: user=%s, amount=%s₽",
|
||
db_user.telegram_id,
|
||
amount_rub,
|
||
)
|
||
|
||
|
||
@error_handler
|
||
async def process_cloudpayments_payment_amount(
|
||
message: types.Message,
|
||
db_user: User,
|
||
db: AsyncSession,
|
||
amount_kopeks: int,
|
||
state: FSMContext,
|
||
):
|
||
"""
|
||
Process payment amount directly (called from quick_amount handlers).
|
||
|
||
Similar to process_heleket_payment_amount and other payment handlers.
|
||
"""
|
||
texts = get_texts(db_user.language)
|
||
|
||
# Проверка ограничения на пополнение
|
||
if getattr(db_user, 'restriction_topup', False):
|
||
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}\n\n"
|
||
"Если вы считаете это ошибкой, вы можете обжаловать решение.",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.clear()
|
||
return
|
||
|
||
if not settings.is_cloudpayments_enabled():
|
||
await message.answer(
|
||
texts.t("CLOUDPAYMENTS_NOT_AVAILABLE", "CloudPayments временно недоступен"),
|
||
)
|
||
return
|
||
|
||
# Validate amount
|
||
if amount_kopeks < settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS:
|
||
min_rub = settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS / 100
|
||
await message.answer(
|
||
texts.t(
|
||
"AMOUNT_TOO_LOW",
|
||
"Минимальная сумма пополнения: {min_amount:.0f}₽",
|
||
).format(min_amount=min_rub),
|
||
)
|
||
return
|
||
|
||
if amount_kopeks > settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS:
|
||
max_rub = settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS / 100
|
||
await message.answer(
|
||
texts.t(
|
||
"AMOUNT_TOO_HIGH",
|
||
"Максимальная сумма пополнения: {max_amount:,.0f}₽",
|
||
).format(max_amount=max_rub),
|
||
)
|
||
return
|
||
|
||
# Clear state
|
||
await state.clear()
|
||
|
||
await _create_cloudpayments_payment_and_respond(
|
||
message, db_user, db, amount_kopeks, edit_message=False
|
||
)
|
||
|
||
|
||
@error_handler
|
||
async def start_cloudpayments_payment(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
):
|
||
"""
|
||
Start CloudPayments payment flow.
|
||
|
||
Shows amount input prompt or quick amount buttons.
|
||
"""
|
||
texts = get_texts(db_user.language)
|
||
|
||
# Проверка ограничения на пополнение
|
||
if getattr(db_user, 'restriction_topup', False):
|
||
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}\n\n"
|
||
"Если вы считаете это ошибкой, вы можете обжаловать решение.",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
)
|
||
await callback.answer()
|
||
return
|
||
|
||
if not settings.is_cloudpayments_enabled():
|
||
await callback.answer(
|
||
texts.t("CLOUDPAYMENTS_NOT_AVAILABLE", "CloudPayments временно недоступен"),
|
||
show_alert=True,
|
||
)
|
||
return
|
||
|
||
min_amount_rub = settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS / 100
|
||
max_amount_rub = settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS / 100
|
||
|
||
message_text = texts.t(
|
||
"CLOUDPAYMENTS_ENTER_AMOUNT",
|
||
"💳 <b>Оплата банковской картой (CloudPayments)</b>\n\n"
|
||
"Введите сумму для пополнения от {min_amount:.0f} до {max_amount:,.0f} рублей:",
|
||
).format(min_amount=min_amount_rub, max_amount=max_amount_rub)
|
||
|
||
keyboard = get_back_keyboard(db_user.language)
|
||
|
||
await callback.message.edit_text(
|
||
message_text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
await state.set_state(BalanceStates.waiting_for_amount)
|
||
await state.update_data(payment_method="cloudpayments")
|
||
await state.update_data(
|
||
cloudpayments_prompt_message_id=callback.message.message_id,
|
||
cloudpayments_prompt_chat_id=callback.message.chat.id,
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@error_handler
|
||
async def process_cloudpayments_amount(
|
||
message: types.Message,
|
||
db_user: User,
|
||
db: AsyncSession,
|
||
state: FSMContext,
|
||
):
|
||
"""
|
||
Process entered amount and create CloudPayments payment.
|
||
|
||
Generates payment link and sends it to user.
|
||
"""
|
||
texts = get_texts(db_user.language)
|
||
|
||
# Get state data
|
||
state_data = await state.get_data()
|
||
payment_method = state_data.get("payment_method")
|
||
|
||
if payment_method != "cloudpayments":
|
||
return # Not our payment method
|
||
|
||
# Parse amount
|
||
try:
|
||
amount_text = message.text.strip().replace(",", ".").replace(" ", "")
|
||
amount_rub = float(amount_text)
|
||
amount_kopeks = int(amount_rub * 100)
|
||
except (ValueError, TypeError):
|
||
await message.answer(
|
||
texts.t("INVALID_AMOUNT", "Введите корректную сумму числом"),
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
# Validate amount
|
||
if amount_kopeks < settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS:
|
||
min_rub = settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS / 100
|
||
await message.answer(
|
||
texts.t(
|
||
"AMOUNT_TOO_LOW",
|
||
"Минимальная сумма пополнения: {min_amount:.0f}₽",
|
||
).format(min_amount=min_rub),
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
if amount_kopeks > settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS:
|
||
max_rub = settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS / 100
|
||
await message.answer(
|
||
texts.t(
|
||
"AMOUNT_TOO_HIGH",
|
||
"Максимальная сумма пополнения: {max_amount:,.0f}₽",
|
||
).format(max_amount=max_rub),
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
# Clear state
|
||
await state.clear()
|
||
|
||
# Create payment
|
||
payment_service = PaymentService()
|
||
|
||
description = settings.PAYMENT_BALANCE_TEMPLATE.format(
|
||
service_name=settings.PAYMENT_SERVICE_NAME,
|
||
description=settings.CLOUDPAYMENTS_DESCRIPTION,
|
||
)
|
||
|
||
result = await payment_service.create_cloudpayments_payment(
|
||
db=db,
|
||
user_id=db_user.id,
|
||
amount_kopeks=amount_kopeks,
|
||
description=description,
|
||
telegram_id=db_user.telegram_id,
|
||
language=db_user.language,
|
||
)
|
||
|
||
if not result:
|
||
await message.answer(
|
||
texts.t(
|
||
"PAYMENT_CREATE_ERROR",
|
||
"Не удалось создать платёж. Попробуйте позже.",
|
||
),
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
payment_url = result.get("payment_url")
|
||
|
||
# Create keyboard with payment button
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t(
|
||
"PAY_BUTTON",
|
||
"💳 Оплатить {amount}₽",
|
||
).format(amount=f"{amount_rub:.0f}"),
|
||
url=payment_url,
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t("BACK_BUTTON", "◀️ Назад"),
|
||
callback_data="menu_balance",
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
await message.answer(
|
||
texts.t(
|
||
"CLOUDPAYMENTS_PAYMENT_CREATED",
|
||
"💳 <b>Оплата банковской картой</b>\n\n"
|
||
"Сумма: <b>{amount}₽</b>\n\n"
|
||
"Нажмите кнопку ниже для оплаты.\n"
|
||
"После успешной оплаты баланс будет пополнен автоматически.",
|
||
).format(amount=f"{amount_rub:.2f}"),
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
logger.info(
|
||
"CloudPayments payment created: user=%s, amount=%s₽",
|
||
db_user.telegram_id,
|
||
amount_rub,
|
||
)
|
||
|
||
|
||
@error_handler
|
||
async def handle_cloudpayments_quick_amount(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession,
|
||
state: FSMContext,
|
||
):
|
||
"""
|
||
Handle quick amount selection for CloudPayments.
|
||
|
||
Called when user clicks a predefined amount button.
|
||
"""
|
||
texts = get_texts(db_user.language)
|
||
|
||
if not settings.is_cloudpayments_enabled():
|
||
await callback.answer(
|
||
texts.t("CLOUDPAYMENTS_NOT_AVAILABLE", "CloudPayments временно недоступен"),
|
||
show_alert=True,
|
||
)
|
||
return
|
||
|
||
# Extract amount from callback data: topup_amount|cloudpayments|{amount_kopeks}
|
||
try:
|
||
parts = callback.data.split("|")
|
||
if len(parts) >= 3:
|
||
amount_kopeks = int(parts[2])
|
||
else:
|
||
await callback.answer("Invalid callback data", show_alert=True)
|
||
return
|
||
except (ValueError, IndexError):
|
||
await callback.answer("Invalid amount", show_alert=True)
|
||
return
|
||
|
||
amount_rub = amount_kopeks / 100
|
||
|
||
# Validate amount
|
||
if amount_kopeks < settings.CLOUDPAYMENTS_MIN_AMOUNT_KOPEKS:
|
||
await callback.answer(
|
||
texts.t("AMOUNT_TOO_LOW_SHORT", "Сумма слишком мала"),
|
||
show_alert=True,
|
||
)
|
||
return
|
||
|
||
if amount_kopeks > settings.CLOUDPAYMENTS_MAX_AMOUNT_KOPEKS:
|
||
await callback.answer(
|
||
texts.t("AMOUNT_TOO_HIGH_SHORT", "Сумма слишком велика"),
|
||
show_alert=True,
|
||
)
|
||
return
|
||
|
||
await callback.answer()
|
||
|
||
# Create payment
|
||
payment_service = PaymentService()
|
||
|
||
description = settings.PAYMENT_BALANCE_TEMPLATE.format(
|
||
service_name=settings.PAYMENT_SERVICE_NAME,
|
||
description=settings.CLOUDPAYMENTS_DESCRIPTION,
|
||
)
|
||
|
||
result = await payment_service.create_cloudpayments_payment(
|
||
db=db,
|
||
user_id=db_user.id,
|
||
amount_kopeks=amount_kopeks,
|
||
description=description,
|
||
telegram_id=db_user.telegram_id,
|
||
language=db_user.language,
|
||
)
|
||
|
||
if not result:
|
||
await callback.message.edit_text(
|
||
texts.t(
|
||
"PAYMENT_CREATE_ERROR",
|
||
"Не удалось создать платёж. Попробуйте позже.",
|
||
),
|
||
reply_markup=get_back_keyboard(db_user.language),
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
|
||
payment_url = result.get("payment_url")
|
||
|
||
# Create keyboard with payment button
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t(
|
||
"PAY_BUTTON",
|
||
"💳 Оплатить {amount}₽",
|
||
).format(amount=f"{amount_rub:.0f}"),
|
||
url=payment_url,
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t("BACK_BUTTON", "◀️ Назад"),
|
||
callback_data="menu_balance",
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
texts.t(
|
||
"CLOUDPAYMENTS_PAYMENT_CREATED",
|
||
"💳 <b>Оплата банковской картой</b>\n\n"
|
||
"Сумма: <b>{amount}₽</b>\n\n"
|
||
"Нажмите кнопку ниже для оплаты.\n"
|
||
"После успешной оплаты баланс будет пополнен автоматически.",
|
||
).format(amount=f"{amount_rub:.2f}"),
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
logger.info(
|
||
"CloudPayments payment created (quick): user=%s, amount=%s₽",
|
||
db_user.telegram_id,
|
||
amount_rub,
|
||
)
|