afea054c8f
- Lava Business via gate.lava.ru (HMAC-SHA256 signed JSON requests) - Sub-methods: card and SBP via includeService filter - Webhook signature verified from raw bytes with secret_key_2 - Sticky terminal-status guard (success after amount_mismatch escalates to ERROR) - Order ID with full uuid4 hex (128-bit entropy) - Cross-row contamination guard: order_id assertion on invoice_id fallback - Warning when hook URL cannot be derived from webhook/web_api/cabinet bases - Explicit failure when Lava response lacks payment_url (no orphan rows) - Adds LAVA settings category, /lava-webhook endpoint, cabinet topup branch - Mirrors existing Antilopay/Jupiter/Donut mixin pattern
876 lines
35 KiB
Python
876 lines
35 KiB
Python
import html
|
|
|
|
import structlog
|
|
from aiogram import Dispatcher, F, types
|
|
from aiogram.exceptions import TelegramBadRequest
|
|
from aiogram.fsm.context import FSMContext
|
|
from aiogram.types import InaccessibleMessage
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.database.crud.transaction import get_user_transactions
|
|
from app.database.models import TransactionType, User
|
|
from app.handlers.subscription.autopay import handle_confirm_unlink, handle_saved_cards_list, handle_unlink_card
|
|
from app.keyboards.inline import (
|
|
get_back_keyboard,
|
|
get_balance_keyboard,
|
|
get_pagination_keyboard,
|
|
get_payment_methods_keyboard,
|
|
)
|
|
from app.localization.texts import get_texts
|
|
from app.states import BalanceStates
|
|
from app.utils.decorators import error_handler
|
|
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
TRANSACTIONS_PER_PAGE = 10
|
|
|
|
CREDIT_TRANSACTION_TYPES: frozenset[str] = frozenset(
|
|
{
|
|
TransactionType.DEPOSIT.value,
|
|
TransactionType.REFERRAL_REWARD.value,
|
|
TransactionType.REFUND.value,
|
|
TransactionType.POLL_REWARD.value,
|
|
}
|
|
)
|
|
|
|
|
|
async def route_payment_by_method(
|
|
message: types.Message, db_user: User, amount_kopeks: int, state: FSMContext, payment_method: str
|
|
) -> bool:
|
|
"""
|
|
Роутер платежей по методу оплаты.
|
|
|
|
Args:
|
|
message: Сообщение для ответа
|
|
db_user: Пользователь БД
|
|
amount_kopeks: Сумма в копейках
|
|
state: FSM состояние
|
|
payment_method: Метод оплаты (yookassa, stars, cryptobot и т.д.)
|
|
|
|
Returns:
|
|
True если платеж обработан, False если метод неизвестен
|
|
"""
|
|
if payment_method == 'stars':
|
|
from .stars import process_stars_payment_amount
|
|
|
|
await process_stars_payment_amount(message, db_user, amount_kopeks, state)
|
|
return True
|
|
|
|
# Все остальные методы требуют сессию БД
|
|
from app.database.database import AsyncSessionLocal
|
|
|
|
if payment_method == 'yookassa':
|
|
from .yookassa import process_yookassa_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_yookassa_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'yookassa_sbp':
|
|
from .yookassa import process_yookassa_sbp_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_yookassa_sbp_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'mulenpay':
|
|
from .mulenpay import process_mulenpay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_mulenpay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'platega':
|
|
from .platega import process_platega_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_platega_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'wata':
|
|
from .wata import process_wata_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_wata_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'pal24':
|
|
from .pal24 import process_pal24_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_pal24_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'cryptobot':
|
|
from .cryptobot import process_cryptobot_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_cryptobot_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'heleket':
|
|
from .heleket import process_heleket_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_heleket_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'cloudpayments':
|
|
from .cloudpayments import process_cloudpayments_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_cloudpayments_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('freekassa', 'freekassa_sbp', 'freekassa_card'):
|
|
from .freekassa import process_freekassa_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_freekassa_payment_amount(
|
|
message, db_user, db, amount_kopeks, state, payment_method=payment_method
|
|
)
|
|
return True
|
|
|
|
if payment_method in ('kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card', 'kassa_ai_sberpay'):
|
|
from .kassa_ai import process_kassa_ai_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_kassa_ai_payment_amount(
|
|
message, db_user, db, amount_kopeks, state, payment_method=payment_method
|
|
)
|
|
return True
|
|
|
|
if payment_method == 'severpay':
|
|
from .severpay import process_severpay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_severpay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'paypear':
|
|
from .paypear import process_paypear_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_paypear_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'rollypay':
|
|
from .rollypay import process_rollypay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_rollypay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'overpay':
|
|
from .overpay import process_overpay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_overpay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('aurapay', 'aurapay_sbp', 'aurapay_card'):
|
|
from .aurapay import process_aurapay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_aurapay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('etoplatezhi', 'etoplatezhi_sbp', 'etoplatezhi_card'):
|
|
from .etoplatezhi import process_etoplatezhi_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_etoplatezhi_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('antilopay', 'antilopay_sbp', 'antilopay_card', 'antilopay_sberpay'):
|
|
from .antilopay import process_antilopay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_antilopay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('jupiter', 'jupiter_sbp'):
|
|
from .jupiter import process_jupiter_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_jupiter_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('donut', 'donut_card', 'donut_sbp', 'donut_sbp_qr'):
|
|
from .donut import process_donut_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_donut_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method in ('lava', 'lava_card', 'lava_sbp'):
|
|
from .lava import process_lava_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_lava_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
if payment_method == 'riopay':
|
|
from .riopay import process_riopay_payment_amount
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
await process_riopay_payment_amount(message, db_user, db, amount_kopeks, state)
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
@error_handler
|
|
async def show_balance_menu(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
|
# Проверяем, доступно ли сообщение
|
|
if isinstance(callback.message, InaccessibleMessage):
|
|
await callback.answer()
|
|
return
|
|
|
|
texts = get_texts(db_user.language)
|
|
|
|
balance_text = texts.BALANCE_INFO.format(balance=texts.format_price(db_user.balance_kopeks))
|
|
|
|
reply_markup = get_balance_keyboard(db_user.language)
|
|
|
|
try:
|
|
if callback.message and callback.message.text:
|
|
await callback.message.edit_text(balance_text, reply_markup=reply_markup)
|
|
elif callback.message and callback.message.caption:
|
|
await callback.message.edit_caption(balance_text, reply_markup=reply_markup)
|
|
else:
|
|
await callback.message.answer(balance_text, reply_markup=reply_markup)
|
|
except TelegramBadRequest as error:
|
|
logger.warning('Failed to edit balance message, sending a new one instead', error=error)
|
|
await callback.message.answer(balance_text, reply_markup=reply_markup)
|
|
await callback.answer()
|
|
|
|
|
|
@error_handler
|
|
async def show_balance_history(callback: types.CallbackQuery, db_user: User, db: AsyncSession, page: int = 1):
|
|
texts = get_texts(db_user.language)
|
|
|
|
offset = (page - 1) * TRANSACTIONS_PER_PAGE
|
|
|
|
raw_transactions = await get_user_transactions(db, db_user.id, limit=TRANSACTIONS_PER_PAGE * 3, offset=offset)
|
|
|
|
seen_transactions = set()
|
|
unique_transactions = []
|
|
|
|
for transaction in raw_transactions:
|
|
rounded_time = transaction.created_at.replace(second=0, microsecond=0)
|
|
transaction_key = (transaction.amount_kopeks, transaction.description, rounded_time)
|
|
|
|
if transaction_key not in seen_transactions:
|
|
seen_transactions.add(transaction_key)
|
|
unique_transactions.append(transaction)
|
|
|
|
if len(unique_transactions) >= TRANSACTIONS_PER_PAGE:
|
|
break
|
|
|
|
all_transactions = await get_user_transactions(db, db_user.id, limit=1000)
|
|
seen_all = set()
|
|
total_unique = 0
|
|
|
|
for transaction in all_transactions:
|
|
rounded_time = transaction.created_at.replace(second=0, microsecond=0)
|
|
transaction_key = (transaction.amount_kopeks, transaction.description, rounded_time)
|
|
if transaction_key not in seen_all:
|
|
seen_all.add(transaction_key)
|
|
total_unique += 1
|
|
|
|
if not unique_transactions:
|
|
await callback.message.edit_text('📊 История операций пуста', reply_markup=get_back_keyboard(db_user.language))
|
|
await callback.answer()
|
|
return
|
|
|
|
text = '📊 <b>История операций</b>\n\n'
|
|
|
|
for transaction in unique_transactions:
|
|
is_credit = transaction.type in CREDIT_TRANSACTION_TYPES
|
|
emoji = '💰' if is_credit else '💸'
|
|
amount_text = (
|
|
f'+{texts.format_price(transaction.amount_kopeks)}'
|
|
if is_credit
|
|
else f'-{texts.format_price(abs(transaction.amount_kopeks))}'
|
|
)
|
|
|
|
text += f'{emoji} {amount_text}\n'
|
|
text += f'📝 {html.escape(transaction.description or "")}\n'
|
|
text += f'📅 {transaction.created_at.strftime("%d.%m.%Y %H:%M")}\n\n'
|
|
|
|
keyboard = []
|
|
total_pages = (total_unique + TRANSACTIONS_PER_PAGE - 1) // TRANSACTIONS_PER_PAGE
|
|
|
|
if total_pages > 1:
|
|
pagination_row = get_pagination_keyboard(page, total_pages, 'balance_history', db_user.language)
|
|
keyboard.extend(pagination_row)
|
|
|
|
keyboard.append([types.InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
|
|
|
|
await callback.message.edit_text(
|
|
text, reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard), parse_mode='HTML'
|
|
)
|
|
await callback.answer()
|
|
|
|
|
|
@error_handler
|
|
async def handle_balance_history_pagination(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
|
page = int(callback.data.split('_')[-1])
|
|
await show_balance_history(callback, db_user, db, page)
|
|
|
|
|
|
@error_handler
|
|
async def show_payment_methods(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
|
|
from app.config import settings
|
|
from app.utils.payment_utils import get_payment_methods_text
|
|
|
|
texts = get_texts(db_user.language)
|
|
|
|
# Проверка ограничения на пополнение
|
|
if getattr(db_user, 'restriction_topup', False):
|
|
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
|
|
support_url = settings.get_support_contact_url()
|
|
keyboard = []
|
|
if support_url:
|
|
keyboard.append([types.InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
|
|
keyboard.append([types.InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
|
|
|
|
await callback.message.edit_text(
|
|
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}\n\n'
|
|
'Если вы считаете это ошибкой, вы можете обжаловать решение.',
|
|
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
|
)
|
|
await callback.answer()
|
|
return
|
|
|
|
payment_text = get_payment_methods_text(db_user.language)
|
|
|
|
# Проверяем сохранённую корзину для автоподстановки суммы пополнения
|
|
amount_kopeks = 0
|
|
try:
|
|
from app.services.user_cart_service import user_cart_service
|
|
|
|
cart_data = await user_cart_service.get_user_cart(db_user.id)
|
|
if cart_data and cart_data.get('saved_cart'):
|
|
missing = cart_data.get('missing_amount', 0)
|
|
if missing > 0:
|
|
amount_kopeks = missing
|
|
except Exception:
|
|
pass
|
|
|
|
full_text = payment_text
|
|
|
|
keyboard = get_payment_methods_keyboard(amount_kopeks, db_user.language)
|
|
|
|
# Если сообщение недоступно, отправляем новое
|
|
if isinstance(callback.message, InaccessibleMessage):
|
|
await callback.message.answer(full_text, reply_markup=keyboard, parse_mode='HTML')
|
|
await callback.answer()
|
|
return
|
|
|
|
try:
|
|
await callback.message.edit_text(full_text, reply_markup=keyboard, parse_mode='HTML')
|
|
except TelegramBadRequest:
|
|
try:
|
|
await callback.message.edit_caption(full_text, reply_markup=keyboard, parse_mode='HTML')
|
|
except TelegramBadRequest:
|
|
try:
|
|
await callback.message.delete()
|
|
except TelegramBadRequest:
|
|
pass
|
|
await callback.message.answer(full_text, reply_markup=keyboard, parse_mode='HTML')
|
|
|
|
await callback.answer()
|
|
|
|
|
|
@error_handler
|
|
async def handle_payment_methods_unavailable(callback: types.CallbackQuery, db_user: User):
|
|
texts = get_texts(db_user.language)
|
|
|
|
await callback.answer(
|
|
texts.t(
|
|
'PAYMENT_METHODS_UNAVAILABLE_ALERT',
|
|
'⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.',
|
|
),
|
|
show_alert=True,
|
|
)
|
|
|
|
|
|
@error_handler
|
|
async def handle_successful_topup_with_cart(user_id: int, amount_kopeks: int, bot, db: AsyncSession):
|
|
from aiogram.fsm.storage.base import StorageKey
|
|
|
|
from app.bot import dp
|
|
from app.database.crud.user import get_user_by_id
|
|
|
|
user = await get_user_by_id(db, user_id)
|
|
if not user:
|
|
return
|
|
|
|
# Email-only users don't have telegram_id - skip Telegram notification
|
|
if not user.telegram_id:
|
|
logger.info('Skipping cart notification for email-only user', user_id=user_id)
|
|
return
|
|
|
|
storage = dp.storage
|
|
key = StorageKey(bot_id=bot.id, chat_id=user.telegram_id, user_id=user.telegram_id)
|
|
|
|
try:
|
|
state_data = await storage.get_data(key)
|
|
current_state = await storage.get_state(key)
|
|
|
|
if current_state == 'SubscriptionStates:cart_saved_for_topup' and state_data.get('saved_cart'):
|
|
texts = get_texts(user.language)
|
|
total_price = state_data.get('total_price', 0)
|
|
|
|
keyboard = types.InlineKeyboardMarkup(
|
|
inline_keyboard=[
|
|
[
|
|
types.InlineKeyboardButton(
|
|
text='🛒 Вернуться к оформлению подписки', callback_data='return_to_saved_cart'
|
|
)
|
|
],
|
|
[types.InlineKeyboardButton(text='💰 Мой баланс', callback_data='menu_balance')],
|
|
[types.InlineKeyboardButton(text='🏠 Главное меню', callback_data='back_to_menu')],
|
|
]
|
|
)
|
|
|
|
if 0 < total_price <= user.balance_kopeks:
|
|
balance_hint = 'Средств на балансе достаточно для оформления.'
|
|
else:
|
|
missing = max(total_price - user.balance_kopeks, 0)
|
|
balance_hint = f'Не хватает: {texts.format_price(missing)}'
|
|
|
|
success_text = (
|
|
f'✅ Баланс пополнен на {texts.format_price(amount_kopeks)}!\n\n'
|
|
f'💰 Текущий баланс: {texts.format_price(user.balance_kopeks)}\n\n'
|
|
f'🛒 У вас есть сохранённая корзина на {texts.format_price(total_price)}\n'
|
|
f'{balance_hint}\n\n'
|
|
f'Хотите продолжить оформление?'
|
|
)
|
|
|
|
await bot.send_message(
|
|
chat_id=user.telegram_id, text=success_text, reply_markup=keyboard, parse_mode='HTML'
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error('Ошибка обработки успешного пополнения с корзиной', error=e)
|
|
|
|
|
|
@error_handler
|
|
async def request_support_topup(callback: types.CallbackQuery, db_user: User):
|
|
texts = get_texts(db_user.language)
|
|
|
|
if not settings.is_support_topup_enabled():
|
|
await callback.answer(
|
|
texts.t(
|
|
'SUPPORT_TOPUP_DISABLED',
|
|
'Пополнение через поддержку отключено. Попробуйте другой способ оплаты.',
|
|
),
|
|
show_alert=True,
|
|
)
|
|
return
|
|
|
|
user_id_display = db_user.telegram_id or db_user.email or f'#{db_user.id}'
|
|
support_text = f"""
|
|
🛠️ <b>Пополнение через поддержку</b>
|
|
|
|
Для пополнения баланса обратитесь в техподдержку:
|
|
{settings.get_support_contact_display_html()}
|
|
|
|
Укажите:
|
|
• ID: {user_id_display}
|
|
• Сумму пополнения
|
|
• Способ оплаты
|
|
|
|
⏰ Время обработки: 1-24 часа
|
|
|
|
<b>Доступные способы:</b>
|
|
• Криптовалюта
|
|
• Переводы между банками
|
|
• Другие платежные системы
|
|
"""
|
|
|
|
keyboard = types.InlineKeyboardMarkup(
|
|
inline_keyboard=[
|
|
[
|
|
types.InlineKeyboardButton(
|
|
text='💬 Написать в поддержку', url=settings.get_support_contact_url() or 'https://t.me/'
|
|
)
|
|
],
|
|
[types.InlineKeyboardButton(text=texts.BACK, callback_data='balance_topup')],
|
|
]
|
|
)
|
|
|
|
await callback.message.edit_text(support_text, reply_markup=keyboard, parse_mode='HTML')
|
|
await callback.answer()
|
|
|
|
|
|
@error_handler
|
|
async def process_topup_amount(message: types.Message, db_user: User, state: FSMContext):
|
|
texts = get_texts(db_user.language)
|
|
|
|
try:
|
|
if not message.text:
|
|
if message.successful_payment:
|
|
logger.info(
|
|
'Получено сообщение об успешном платеже без текста, обработчик суммы пополнения завершает работу'
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
await message.answer(texts.INVALID_AMOUNT, reply_markup=get_back_keyboard(db_user.language))
|
|
return
|
|
|
|
amount_text = message.text.strip()
|
|
if not amount_text:
|
|
await message.answer(texts.INVALID_AMOUNT, reply_markup=get_back_keyboard(db_user.language))
|
|
return
|
|
|
|
amount_rubles = float(amount_text.replace(',', '.'))
|
|
|
|
if amount_rubles < 1:
|
|
await message.answer(
|
|
'Минимальная сумма пополнения: 1 ₽',
|
|
reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'),
|
|
)
|
|
return
|
|
|
|
if amount_rubles > 50000:
|
|
await message.answer(
|
|
'Максимальная сумма пополнения: 50,000 ₽',
|
|
reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'),
|
|
)
|
|
return
|
|
|
|
amount_kopeks = int(amount_rubles * 100)
|
|
data = await state.get_data()
|
|
payment_method = data.get('payment_method', 'stars')
|
|
|
|
if payment_method in ['yookassa', 'yookassa_sbp']:
|
|
if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS:
|
|
min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100
|
|
await message.answer(
|
|
f'❌ Минимальная сумма для оплаты через YooKassa: {min_rubles:.0f} ₽',
|
|
reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'),
|
|
)
|
|
return
|
|
|
|
if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS:
|
|
max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100
|
|
await message.answer(
|
|
f'❌ Максимальная сумма для оплаты через YooKassa: {max_rubles:,.0f} ₽'.replace(',', ' '),
|
|
reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'),
|
|
)
|
|
return
|
|
|
|
if not await route_payment_by_method(message, db_user, amount_kopeks, state, payment_method):
|
|
await message.answer('Неизвестный способ оплаты')
|
|
|
|
except ValueError:
|
|
await message.answer(texts.INVALID_AMOUNT, reply_markup=get_back_keyboard(db_user.language))
|
|
|
|
|
|
@error_handler
|
|
async def handle_sbp_payment(callback: types.CallbackQuery, db: AsyncSession):
|
|
try:
|
|
local_payment_id = int(callback.data.split('_')[-1])
|
|
|
|
from app.database.crud.yookassa import get_yookassa_payment_by_local_id
|
|
|
|
payment = await get_yookassa_payment_by_local_id(db, local_payment_id)
|
|
|
|
if not payment:
|
|
await callback.answer('❌ Платеж не найден', show_alert=True)
|
|
return
|
|
|
|
import json
|
|
|
|
metadata = json.loads(payment.metadata_json) if payment.metadata_json else {}
|
|
confirmation_token = metadata.get('confirmation_token')
|
|
|
|
if not confirmation_token:
|
|
await callback.answer('❌ Токен подтверждения не найден', show_alert=True)
|
|
return
|
|
|
|
await callback.message.answer(
|
|
f'Для оплаты через СБП откройте приложение вашего банка и подтвердите платеж.\\n\\n'
|
|
f'Если у вас не открылось банковское приложение автоматически, вы можете:\\n'
|
|
f'1. Скопировать этот токен: <code>{confirmation_token}</code>\\n'
|
|
f'2. Открыть приложение вашего банка\\n'
|
|
f'3. Найти функцию оплаты по токену\\n'
|
|
f'4. Вставить токен и подтвердить платеж',
|
|
parse_mode='HTML',
|
|
)
|
|
|
|
await callback.answer('Информация об оплате отправлена', show_alert=True)
|
|
|
|
except Exception as e:
|
|
logger.error('Ошибка обработки embedded платежа СБП', error=e)
|
|
await callback.answer('❌ Ошибка обработки платежа', show_alert=True)
|
|
|
|
|
|
@error_handler
|
|
async def handle_topup_amount_callback(
|
|
callback: types.CallbackQuery,
|
|
db_user: User,
|
|
state: FSMContext,
|
|
):
|
|
try:
|
|
_, method, amount_str = callback.data.split('|', 2)
|
|
amount_kopeks = int(amount_str)
|
|
except ValueError:
|
|
await callback.answer('❌ Некорректный запрос', show_alert=True)
|
|
return
|
|
|
|
if amount_kopeks <= 0:
|
|
await callback.answer('❌ Некорректная сумма', show_alert=True)
|
|
return
|
|
|
|
try:
|
|
# Особые случаи, требующие специальной логики
|
|
if method.startswith('platega_m'):
|
|
from app.database.database import AsyncSessionLocal
|
|
|
|
from .platega import process_platega_payment_amount
|
|
|
|
platega_method_code = int(method[len('platega_m') :])
|
|
await state.update_data(payment_method='platega', platega_method=platega_method_code)
|
|
await state.set_state(BalanceStates.waiting_for_amount)
|
|
async with AsyncSessionLocal() as db:
|
|
await process_platega_payment_amount(callback.message, db_user, db, amount_kopeks, state)
|
|
elif method == 'platega':
|
|
from app.database.database import AsyncSessionLocal
|
|
|
|
from .platega import process_platega_payment_amount, start_platega_payment
|
|
|
|
data = await state.get_data()
|
|
method_code = int(data.get('platega_method', 0)) if data else 0
|
|
|
|
if method_code > 0:
|
|
await state.set_state(BalanceStates.waiting_for_amount)
|
|
async with AsyncSessionLocal() as db:
|
|
await process_platega_payment_amount(callback.message, db_user, db, amount_kopeks, state)
|
|
else:
|
|
await state.update_data(platega_pending_amount=amount_kopeks)
|
|
await start_platega_payment(callback, db_user, state)
|
|
elif method == 'tribute':
|
|
from .tribute import start_tribute_payment
|
|
|
|
await start_tribute_payment(callback, db_user)
|
|
return
|
|
# Стандартные методы через роутер
|
|
else:
|
|
await state.update_data(payment_method=method)
|
|
await state.set_state(BalanceStates.waiting_for_amount)
|
|
if not await route_payment_by_method(callback.message, db_user, amount_kopeks, state, method):
|
|
await callback.answer('❌ Неизвестный способ оплаты', show_alert=True)
|
|
return
|
|
|
|
await callback.answer()
|
|
|
|
except Exception as error:
|
|
logger.error('Ошибка быстрого пополнения', error=error)
|
|
await callback.answer('❌ Ошибка обработки запроса', show_alert=True)
|
|
|
|
|
|
def register_balance_handlers(dp: Dispatcher):
|
|
dp.callback_query.register(show_balance_menu, F.data == 'menu_balance')
|
|
|
|
dp.callback_query.register(show_balance_history, F.data == 'balance_history')
|
|
|
|
dp.callback_query.register(handle_balance_history_pagination, F.data.startswith('balance_history_page_'))
|
|
|
|
dp.callback_query.register(show_payment_methods, F.data == 'balance_topup')
|
|
|
|
from .stars import start_stars_payment
|
|
|
|
dp.callback_query.register(start_stars_payment, F.data == 'topup_stars')
|
|
|
|
from .yookassa import start_yookassa_payment
|
|
|
|
dp.callback_query.register(start_yookassa_payment, F.data == 'topup_yookassa')
|
|
|
|
from .yookassa import start_yookassa_sbp_payment
|
|
|
|
dp.callback_query.register(start_yookassa_sbp_payment, F.data == 'topup_yookassa_sbp')
|
|
|
|
from .mulenpay import start_mulenpay_payment
|
|
|
|
dp.callback_query.register(start_mulenpay_payment, F.data == 'topup_mulenpay')
|
|
|
|
from .wata import start_wata_payment
|
|
|
|
dp.callback_query.register(start_wata_payment, F.data == 'topup_wata')
|
|
|
|
from .pal24 import start_pal24_payment
|
|
|
|
dp.callback_query.register(start_pal24_payment, F.data == 'topup_pal24')
|
|
from .pal24 import handle_pal24_method_selection
|
|
|
|
dp.callback_query.register(
|
|
handle_pal24_method_selection,
|
|
F.data.startswith('pal24_method_'),
|
|
)
|
|
|
|
from .platega import handle_platega_method_selection, start_platega_direct_method, start_platega_payment
|
|
|
|
dp.callback_query.register(start_platega_payment, F.data == 'topup_platega')
|
|
dp.callback_query.register(
|
|
handle_platega_method_selection,
|
|
F.data.startswith('platega_method_'),
|
|
)
|
|
dp.callback_query.register(
|
|
start_platega_direct_method,
|
|
F.data.regexp(r'^topup_platega_m\d+$'),
|
|
)
|
|
|
|
from .yookassa import check_yookassa_payment_status
|
|
|
|
dp.callback_query.register(check_yookassa_payment_status, F.data.startswith('check_yookassa_'))
|
|
|
|
from .tribute import start_tribute_payment
|
|
|
|
dp.callback_query.register(start_tribute_payment, F.data == 'topup_tribute')
|
|
|
|
dp.callback_query.register(request_support_topup, F.data == 'topup_support')
|
|
|
|
from .yookassa import check_yookassa_payment_status
|
|
|
|
dp.callback_query.register(check_yookassa_payment_status, F.data.startswith('check_yookassa_'))
|
|
|
|
dp.message.register(process_topup_amount, BalanceStates.waiting_for_amount)
|
|
|
|
from .cryptobot import start_cryptobot_payment
|
|
|
|
dp.callback_query.register(start_cryptobot_payment, F.data == 'topup_cryptobot')
|
|
|
|
from .cryptobot import check_cryptobot_payment_status
|
|
|
|
dp.callback_query.register(check_cryptobot_payment_status, F.data.startswith('check_cryptobot_'))
|
|
|
|
from .heleket import check_heleket_payment_status, start_heleket_payment
|
|
|
|
dp.callback_query.register(start_heleket_payment, F.data == 'topup_heleket')
|
|
dp.callback_query.register(check_heleket_payment_status, F.data.startswith('check_heleket_'))
|
|
|
|
from .cloudpayments import start_cloudpayments_payment
|
|
|
|
dp.callback_query.register(start_cloudpayments_payment, F.data == 'topup_cloudpayments')
|
|
|
|
from .freekassa import (
|
|
start_freekassa_card_topup,
|
|
start_freekassa_sbp_topup,
|
|
start_freekassa_topup,
|
|
)
|
|
|
|
dp.callback_query.register(start_freekassa_topup, F.data == 'topup_freekassa')
|
|
dp.callback_query.register(start_freekassa_sbp_topup, F.data == 'topup_freekassa_sbp')
|
|
dp.callback_query.register(start_freekassa_card_topup, F.data == 'topup_freekassa_card')
|
|
|
|
from .kassa_ai import (
|
|
start_kassa_ai_card_topup,
|
|
start_kassa_ai_sberpay_topup,
|
|
start_kassa_ai_sbp_topup,
|
|
start_kassa_ai_topup,
|
|
)
|
|
|
|
dp.callback_query.register(start_kassa_ai_topup, F.data == 'topup_kassa_ai')
|
|
dp.callback_query.register(start_kassa_ai_sbp_topup, F.data == 'topup_kassa_ai_sbp')
|
|
dp.callback_query.register(start_kassa_ai_card_topup, F.data == 'topup_kassa_ai_card')
|
|
dp.callback_query.register(start_kassa_ai_sberpay_topup, F.data == 'topup_kassa_ai_sberpay')
|
|
|
|
from .riopay import start_riopay_topup
|
|
|
|
dp.callback_query.register(start_riopay_topup, F.data == 'topup_riopay')
|
|
|
|
from .severpay import start_severpay_topup
|
|
|
|
dp.callback_query.register(start_severpay_topup, F.data == 'topup_severpay')
|
|
|
|
from .paypear import start_paypear_topup
|
|
|
|
dp.callback_query.register(start_paypear_topup, F.data == 'topup_paypear')
|
|
|
|
from .rollypay import start_rollypay_topup
|
|
|
|
dp.callback_query.register(start_rollypay_topup, F.data == 'topup_rollypay')
|
|
|
|
from .overpay import start_overpay_topup
|
|
|
|
dp.callback_query.register(start_overpay_topup, F.data == 'topup_overpay')
|
|
|
|
from .aurapay import start_aurapay_card_topup, start_aurapay_sbp_topup, start_aurapay_topup
|
|
|
|
dp.callback_query.register(start_aurapay_topup, F.data == 'topup_aurapay')
|
|
dp.callback_query.register(start_aurapay_sbp_topup, F.data == 'topup_aurapay_sbp')
|
|
dp.callback_query.register(start_aurapay_card_topup, F.data == 'topup_aurapay_card')
|
|
|
|
from .etoplatezhi import start_etoplatezhi_card_topup, start_etoplatezhi_sbp_topup, start_etoplatezhi_topup
|
|
|
|
dp.callback_query.register(start_etoplatezhi_topup, F.data == 'topup_etoplatezhi')
|
|
dp.callback_query.register(start_etoplatezhi_sbp_topup, F.data == 'topup_etoplatezhi_sbp')
|
|
dp.callback_query.register(start_etoplatezhi_card_topup, F.data == 'topup_etoplatezhi_card')
|
|
|
|
from .antilopay import (
|
|
start_antilopay_card_topup,
|
|
start_antilopay_sberpay_topup,
|
|
start_antilopay_sbp_topup,
|
|
start_antilopay_topup,
|
|
)
|
|
|
|
dp.callback_query.register(start_antilopay_topup, F.data == 'topup_antilopay')
|
|
dp.callback_query.register(start_antilopay_sbp_topup, F.data == 'topup_antilopay_sbp')
|
|
dp.callback_query.register(start_antilopay_card_topup, F.data == 'topup_antilopay_card')
|
|
dp.callback_query.register(start_antilopay_sberpay_topup, F.data == 'topup_antilopay_sberpay')
|
|
|
|
from .jupiter import start_jupiter_sbp_topup, start_jupiter_topup
|
|
|
|
dp.callback_query.register(start_jupiter_topup, F.data == 'topup_jupiter')
|
|
dp.callback_query.register(start_jupiter_sbp_topup, F.data == 'topup_jupiter_sbp')
|
|
|
|
from .donut import (
|
|
start_donut_card_topup,
|
|
start_donut_sbp_qr_topup,
|
|
start_donut_sbp_topup,
|
|
start_donut_topup,
|
|
)
|
|
|
|
dp.callback_query.register(start_donut_topup, F.data == 'topup_donut')
|
|
dp.callback_query.register(start_donut_card_topup, F.data == 'topup_donut_card')
|
|
dp.callback_query.register(start_donut_sbp_topup, F.data == 'topup_donut_sbp')
|
|
dp.callback_query.register(start_donut_sbp_qr_topup, F.data == 'topup_donut_sbp_qr')
|
|
|
|
from .lava import start_lava_card_topup, start_lava_sbp_topup, start_lava_topup
|
|
|
|
dp.callback_query.register(start_lava_topup, F.data == 'topup_lava')
|
|
dp.callback_query.register(start_lava_card_topup, F.data == 'topup_lava_card')
|
|
dp.callback_query.register(start_lava_sbp_topup, F.data == 'topup_lava_sbp')
|
|
|
|
from .mulenpay import check_mulenpay_payment_status
|
|
|
|
dp.callback_query.register(check_mulenpay_payment_status, F.data.startswith('check_mulenpay_'))
|
|
|
|
from .wata import check_wata_payment_status
|
|
|
|
dp.callback_query.register(check_wata_payment_status, F.data.startswith('check_wata_'))
|
|
|
|
from .pal24 import check_pal24_payment_status
|
|
|
|
dp.callback_query.register(check_pal24_payment_status, F.data.startswith('check_pal24_'))
|
|
|
|
from .platega import check_platega_payment_status
|
|
|
|
dp.callback_query.register(check_platega_payment_status, F.data.startswith('check_platega_'))
|
|
|
|
dp.callback_query.register(handle_payment_methods_unavailable, F.data == 'payment_methods_unavailable')
|
|
|
|
dp.callback_query.register(handle_topup_amount_callback, F.data.startswith('topup_amount|'))
|
|
|
|
dp.callback_query.register(handle_saved_cards_list, F.data == 'saved_cards_list')
|
|
dp.callback_query.register(handle_unlink_card, F.data.startswith('unlink_card_'))
|
|
dp.callback_query.register(handle_confirm_unlink, F.data.startswith('confirm_unlink_'))
|