From aec01ce0d4a36da5ddd07b56ca4bd5de04735a0b Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 16:57:08 +0300 Subject: [PATCH 1/8] fix: reset device limit to new tariff base on tariff switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, extra purchased devices were carried over when switching tariffs, causing incorrect pricing — users upgrading to a more expensive plan kept the old per-device rate until next renewal. Now tariff switch resets device_limit to the new tariff's base limit. Extra purchased devices are not carried over. --- app/cabinet/routes/subscription.py | 2 +- app/database/crud/subscription.py | 17 ++++++----------- app/handlers/subscription/tariff_purchase.py | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/cabinet/routes/subscription.py b/app/cabinet/routes/subscription.py index 0dfbdaff..da060680 100644 --- a/app/cabinet/routes/subscription.py +++ b/app/cabinet/routes/subscription.py @@ -4107,7 +4107,7 @@ async def switch_tariff( # Update subscription old_tariff_name = current_tariff.name if current_tariff else 'Unknown' - # Preserve extra purchased devices above the old tariff's base limit + # Reset device limit to new tariff base (extra purchased devices are not carried over) from app.database.crud.subscription import calc_device_limit_on_tariff_switch # Re-load subscription to avoid MissingGreenlet from expired lazy relationship diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 251f1578..74eca5db 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -40,23 +40,18 @@ def calc_device_limit_on_tariff_switch( new_tariff_device_limit: int | None, max_device_limit: int | None = None, ) -> int: - """Calculate device_limit preserving extra purchased devices when switching tariffs. + """Calculate device_limit when switching tariffs. - Extra devices = current_device_limit - old_tariff_device_limit (clamped to 0). - Result = new_tariff_device_limit + extra_devices, capped at max_device_limit. + Resets to new tariff base device limit — previously purchased + extra devices are NOT carried over. Capped at max_device_limit. """ - old_base = old_tariff_device_limit if old_tariff_device_limit is not None else 0 - current = current_device_limit if current_device_limit is not None else old_base - extra = max(0, current - old_base) - new_base = new_tariff_device_limit if new_tariff_device_limit is not None else 1 - total = new_base + extra effective_max = max_device_limit or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None) - if effective_max and total > effective_max: - total = effective_max + if effective_max and new_base > effective_max: + new_base = effective_max - return total + return new_base def is_active_paid_subscription(subscription: Subscription | None) -> bool: diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py index c4cbc82a..f3b19ac2 100644 --- a/app/handlers/subscription/tariff_purchase.py +++ b/app/handlers/subscription/tariff_purchase.py @@ -1469,7 +1469,7 @@ async def confirm_daily_tariff_purchase( try: if existing_subscription: # Обновляем существующую подписку на суточный тариф - # Сохраняем докупленные устройства при смене тарифа + # Сбрасываем лимит устройств на базу нового тарифа (докупленные не переносятся) from app.database.crud.subscription import calc_device_limit_on_tariff_switch old_tariff = ( @@ -2697,7 +2697,7 @@ async def confirm_daily_tariff_switch( squads = [s.squad_uuid for s in all_servers if s.squad_uuid] # Обновляем подписку на суточный тариф - # Сохраняем докупленные устройства при смене тарифа + # Сбрасываем лимит устройств на базу нового тарифа (докупленные не переносятся) from app.database.crud.subscription import calc_device_limit_on_tariff_switch old_tariff = await get_tariff_by_id(db, subscription.tariff_id) if subscription.tariff_id else None @@ -3288,7 +3288,7 @@ async def confirm_instant_switch( is_new_daily = getattr(new_tariff, 'is_daily', False) # Обновляем подписку с новыми параметрами тарифа - # Сохраняем докупленные устройства при смене тарифа + # Сбрасываем лимит устройств на базу нового тарифа (докупленные не переносятся) from app.database.crud.subscription import calc_device_limit_on_tariff_switch old_tariff = await get_tariff_by_id(db, subscription.tariff_id) if subscription.tariff_id else None From 1a87d438fe127a0b62bd6ee59887212869a3cb17 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 17:00:56 +0300 Subject: [PATCH 2/8] fix: correct RioPay API header case and remove undocumented fields Header was 'x-api-token' but RioPay API expects 'X-Api-Token' (case-sensitive check on their side), causing 403 Invalid API token. Also removed undocumented 'currency' and 'failUrl' fields from create_order payload per official RioPay API docs. --- app/services/payment/riopay.py | 3 --- app/services/riopay_service.py | 9 ++------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/services/payment/riopay.py b/app/services/payment/riopay.py index b63b68a3..1d530c29 100644 --- a/app/services/payment/riopay.py +++ b/app/services/payment/riopay.py @@ -87,7 +87,6 @@ class RioPayPaymentMixin: # Генерируем уникальный order_id с telegram_id для удобного поиска order_id = f'rp{tg_id}_{uuid.uuid4().hex[:6]}' amount_rubles = amount_kopeks / 100 - currency = settings.RIOPAY_CURRENCY # Срок действия платежа (1 час по умолчанию) expires_at = datetime.now(UTC) + timedelta(hours=1) @@ -105,11 +104,9 @@ class RioPayPaymentMixin: # Используем API для создания заказа result = await riopay_service.create_order( amount=amount_rubles, - currency=currency, external_id=order_id, purpose=description, success_url=success_url or settings.RIOPAY_SUCCESS_URL, - fail_url=fail_url or settings.RIOPAY_FAIL_URL, ) payment_url = result.get('paymentLink') diff --git a/app/services/riopay_service.py b/app/services/riopay_service.py index 70221123..20dbc419 100644 --- a/app/services/riopay_service.py +++ b/app/services/riopay_service.py @@ -45,7 +45,7 @@ class RioPayService: def _get_headers(self) -> dict[str, str]: """Формирует заголовки для API запросов.""" return { - 'x-api-token': self.api_token, + 'X-Api-Token': self.api_token, 'Content-Type': 'application/json', } @@ -67,11 +67,10 @@ class RioPayService: self, *, amount: float, - currency: str = 'RUB', external_id: str, purpose: str = 'Пополнение баланса', success_url: str | None = None, - fail_url: str | None = None, + ) -> dict[str, Any]: """ Создает заказ через API RioPay. @@ -82,21 +81,17 @@ class RioPayService: """ payload: dict[str, Any] = { 'amount': str(amount), - 'currency': currency, 'externalId': external_id, 'purpose': purpose, } if success_url: payload['successUrl'] = success_url - if fail_url: - payload['failUrl'] = fail_url logger.info( 'RioPay API create_order', external_id=external_id, amount=amount, - currency=currency, ) try: From d7ad9d70330b6ef5599f7a9409cdd16657d61b85 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 17:44:52 +0300 Subject: [PATCH 3/8] fix: correct CryptoBot webhook signature verification and auto-fill topup amount from cart - Use API token as fallback for webhook signature verification per CryptoBot docs - Try raw body, re-serialized compact JSON, and ASCII-escaped JSON for signature matching - Auto-fill payment amount from saved cart in show_payment_methods instead of hardcoded 0 --- app/external/cryptobot.py | 37 ++++++++++++++++++++++++++---------- app/handlers/balance/main.py | 15 ++++++++++++++- app/webserver/payments.py | 2 +- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/external/cryptobot.py b/app/external/cryptobot.py index 75cf9165..e2f2c81e 100644 --- a/app/external/cryptobot.py +++ b/app/external/cryptobot.py @@ -1,5 +1,6 @@ import hashlib import hmac +import json from typing import Any import aiohttp @@ -122,22 +123,38 @@ class CryptoBotService: return await self._make_request('GET', 'getExchangeRates') def verify_webhook_signature(self, body: str, signature: str) -> bool: - if not self.webhook_secret: - logger.warning('CryptoBot webhook secret не настроен') + # По документации CryptoBot, ключ — SHA256 от API токена + token = self.webhook_secret or self.api_token + if not token: + logger.warning('CryptoBot webhook secret и API token не настроены') return True try: - secret_hash = hashlib.sha256(self.webhook_secret.encode()).digest() - expected_signature = hmac.new(secret_hash, body.encode(), hashlib.sha256).hexdigest() + secret_hash = hashlib.sha256(token.encode()).digest() - is_valid = hmac.compare_digest(signature, expected_signature) + # 1. Raw body — CryptoBot скорее всего шлёт compact JSON + expected = hmac.new(secret_hash, body.encode('utf-8'), hashlib.sha256).hexdigest() + if hmac.compare_digest(signature, expected): + logger.info('CryptoBot webhook подпись валидна') + return True - if is_valid: - logger.info('✅ CryptoBot webhook подпись валидна') - else: - logger.error('❌ Неверная подпись CryptoBot webhook') + # 2. Fallback: JSON.stringify(body) как в доке — re-serialize compact + parsed = json.loads(body) + check_string = json.dumps(parsed, separators=(',', ':'), ensure_ascii=False) + expected = hmac.new(secret_hash, check_string.encode('utf-8'), hashlib.sha256).hexdigest() + if hmac.compare_digest(signature, expected): + logger.info('CryptoBot webhook подпись валидна (re-serialized)') + return True - return is_valid + # 3. Fallback: ensure_ascii=True (если CryptoBot эскейпит unicode) + check_string_ascii = json.dumps(parsed, separators=(',', ':'), ensure_ascii=True) + expected = hmac.new(secret_hash, check_string_ascii.encode('utf-8'), hashlib.sha256).hexdigest() + if hmac.compare_digest(signature, expected): + logger.info('CryptoBot webhook подпись валидна (ascii-escaped)') + return True + + logger.error('Неверная подпись CryptoBot webhook') + return False except Exception as e: logger.error('Ошибка проверки подписи CryptoBot webhook', error=e) diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 415de81d..03a5034d 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -379,9 +379,22 @@ async def show_payment_methods(callback: types.CallbackQuery, db_user: User, db: 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(0, db_user.language) + keyboard = get_payment_methods_keyboard(amount_kopeks, db_user.language) # Если сообщение недоступно, отправляем новое if isinstance(callback.message, InaccessibleMessage): diff --git a/app/webserver/payments.py b/app/webserver/payments.py index 32847ba0..940bc5c5 100644 --- a/app/webserver/payments.py +++ b/app/webserver/payments.py @@ -312,7 +312,7 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute ) signature = request.headers.get('Crypto-Pay-API-Signature') - secret = settings.CRYPTOBOT_WEBHOOK_SECRET + secret = settings.CRYPTOBOT_WEBHOOK_SECRET or settings.CRYPTOBOT_API_TOKEN if secret: if not signature: return JSONResponse( From d0eab3f7aacf0249ca244f168c96044347c918e3 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 17:47:51 +0300 Subject: [PATCH 4/8] fix: disable quick amount buttons in balance topup Buttons showed incorrect prices. Hardcoded is_quick_amount_buttons_enabled to False. --- app/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config.py b/app/config.py index 491f013b..751db510 100644 --- a/app/config.py +++ b/app/config.py @@ -1255,8 +1255,8 @@ class Settings(BaseSettings): return bool(value) def is_quick_amount_buttons_enabled(self) -> bool: - """Показывать ли кнопки быстрого выбора суммы пополнения.""" - return self.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not self.DISABLE_TOPUP_BUTTONS + """Кнопки быстрого выбора суммы пополнения отключены.""" + return False def get_available_languages(self) -> list[str]: defaults = ['ru', 'en', 'ua', 'zh', 'fa'] From 136f29c1eb63778b2b329ed5bf72ab06a4531d0b Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 17:55:16 +0300 Subject: [PATCH 5/8] refactor: remove quick amount buttons feature entirely Removed across 16 files: config, all payment handlers, handler registrations, env example. --- .env.example | 5 - app/config.py | 6 - app/handlers/balance/cloudpayments.py | 124 +------------------ app/handlers/balance/cryptobot.py | 41 ++----- app/handlers/balance/freekassa.py | 124 +------------------ app/handlers/balance/heleket.py | 7 -- app/handlers/balance/kassa_ai.py | 81 +------------ app/handlers/balance/main.py | 151 +----------------------- app/handlers/balance/mulenpay.py | 7 -- app/handlers/balance/pal24.py | 7 -- app/handlers/balance/platega.py | 7 -- app/handlers/balance/riopay.py | 72 +---------- app/handlers/balance/severpay.py | 72 +---------- app/handlers/balance/stars.py | 16 +-- app/handlers/balance/wata.py | 7 -- app/handlers/balance/yookassa.py | 52 ++------ app/services/system_settings_service.py | 1 - 17 files changed, 28 insertions(+), 752 deletions(-) diff --git a/.env.example b/.env.example index cfcab541..36bdac6b 100644 --- a/.env.example +++ b/.env.example @@ -491,16 +491,11 @@ YOOKASSA_WEBHOOK_PORT=8082 YOOKASSA_MIN_AMOUNT_KOPEKS=5000 YOOKASSA_MAX_AMOUNT_KOPEKS=1000000 -# Быстрый выбор суммы пополнения через YooKassa -YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED=true - # Рекуррентные платежи YooKassa (автосохранение карты для автоплатежей) YOOKASSA_RECURRENT_ENABLED=false # true = карта сохраняется обязательно, false = пользователь решает (чекбокс на стороне YooKassa) YOOKASSA_RECURRENT_REQUIRED=true -# Отключить отображение кнопок выбора суммы пополнения (оставить только ввод вручную) -DISABLE_TOPUP_BUTTONS=false # Отключить пополнение баланса через поддержку SUPPORT_TOPUP_ENABLED=true diff --git a/app/config.py b/app/config.py index 751db510..214a6b81 100644 --- a/app/config.py +++ b/app/config.py @@ -353,10 +353,8 @@ class Settings(BaseSettings): YOOKASSA_TRUSTED_PROXY_NETWORKS: str = '' YOOKASSA_MIN_AMOUNT_KOPEKS: int = 5000 YOOKASSA_MAX_AMOUNT_KOPEKS: int = 1000000 - YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED: bool = False YOOKASSA_RECURRENT_ENABLED: bool = False YOOKASSA_RECURRENT_REQUIRED: bool = False - DISABLE_TOPUP_BUTTONS: bool = False SUPPORT_TOPUP_ENABLED: bool = True PAYMENT_VERIFICATION_AUTO_CHECK_ENABLED: bool = False PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES: int = 10 @@ -1254,10 +1252,6 @@ class Settings(BaseSettings): return bool(value) - def is_quick_amount_buttons_enabled(self) -> bool: - """Кнопки быстрого выбора суммы пополнения отключены.""" - return False - def get_available_languages(self) -> list[str]: defaults = ['ru', 'en', 'ua', 'zh', 'fa'] diff --git a/app/handlers/balance/cloudpayments.py b/app/handlers/balance/cloudpayments.py index aa34b137..a04da927 100644 --- a/app/handlers/balance/cloudpayments.py +++ b/app/handlers/balance/cloudpayments.py @@ -129,9 +129,9 @@ async def process_cloudpayments_payment_amount( state: FSMContext, ): """ - Process payment amount directly (called from quick_amount handlers). + Process payment amount directly. - Similar to process_heleket_payment_amount and other payment handlers. + Similar to other payment amount handlers. """ texts = get_texts(db_user.language) @@ -197,7 +197,7 @@ async def start_cloudpayments_payment( """ Start CloudPayments payment flow. - Shows amount input prompt or quick amount buttons. + Shows amount input prompt. """ texts = get_texts(db_user.language) @@ -377,121 +377,3 @@ async def process_cloudpayments_amount( logger.info('CloudPayments payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=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', - '💳 Оплата банковской картой\n\n' - 'Сумма: {amount}₽\n\n' - 'Нажмите кнопку ниже для оплаты.\n' - 'После успешной оплаты баланс будет пополнен автоматически.', - ).format(amount=f'{amount_rub:.2f}'), - reply_markup=keyboard, - parse_mode='HTML', - ) - - logger.info( - 'CloudPayments payment created (quick): user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub - ) diff --git a/app/handlers/balance/cryptobot.py b/app/handlers/balance/cryptobot.py index 738751fd..916eaeb1 100644 --- a/app/handlers/balance/cryptobot.py +++ b/app/handlers/balance/cryptobot.py @@ -53,41 +53,18 @@ async def start_cryptobot_payment(callback: types.CallbackQuery, db_user: User, available_assets = settings.get_cryptobot_assets() assets_text = ', '.join(available_assets) - # Формируем текст сообщения в зависимости от настройки - if settings.is_quick_amount_buttons_enabled(): - message_text = ( - f'🪙 Пополнение криптовалютой\n\n' - f'Выберите сумму пополнения или введите вручную сумму ' - f'от 100 до 100,000 ₽:\n\n' - f'💰 Доступные активы: {assets_text}\n' - f'⚡ Мгновенное зачисление на баланс\n' - f'🔒 Безопасная оплата через CryptoBot\n\n' - f'{rate_text}\n' - f'Сумма будет автоматически конвертирована в USD для оплаты.' - ) - else: - message_text = ( - f'🪙 Пополнение криптовалютой\n\n' - f'Введите сумму для пополнения от 100 до 100,000 ₽:\n\n' - f'💰 Доступные активы: {assets_text}\n' - f'⚡ Мгновенное зачисление на баланс\n' - f'🔒 Безопасная оплата через CryptoBot\n\n' - f'{rate_text}\n' - f'Сумма будет автоматически конвертирована в USD для оплаты.' - ) + message_text = ( + f'🪙 Пополнение криптовалютой\n\n' + f'Введите сумму для пополнения от 100 до 100,000 ₽:\n\n' + f'💰 Доступные активы: {assets_text}\n' + f'⚡ Мгновенное зачисление на баланс\n' + f'🔒 Безопасная оплата через CryptoBot\n\n' + f'{rate_text}\n' + f'Сумма будет автоматически конвертирована в USD для оплаты.' + ) - # Создаем клавиатуру keyboard = get_back_keyboard(db_user.language) - # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - # Вставляем кнопки быстрого выбора перед кнопкой "Назад" - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text(message_text, reply_markup=keyboard, parse_mode='HTML') await state.set_state(BalanceStates.waiting_for_amount) diff --git a/app/handlers/balance/freekassa.py b/app/handlers/balance/freekassa.py index 838dd389..2699e5a4 100644 --- a/app/handlers/balance/freekassa.py +++ b/app/handlers/balance/freekassa.py @@ -154,7 +154,7 @@ async def process_freekassa_payment_amount( payment_method: str | None = None, ): """ - Process payment amount directly (called from quick_amount handlers). + Process payment amount directly. payment_method: 'freekassa', 'freekassa_sbp', 'freekassa_card' """ texts = get_texts(db_user.language) @@ -372,125 +372,3 @@ async def process_freekassa_custom_amount( ) -async def _process_freekassa_quick_amount_impl( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, - payment_method: str, -): - """ - Process quick amount selection for Freekassa payment. - Called when user clicks a predefined amount button. - payment_method: 'freekassa', 'freekassa_sbp', 'freekassa_card' - """ - texts = get_texts(db_user.language) - - if not settings.is_freekassa_enabled(): - await callback.answer( - texts.t('FREEKASSA_NOT_AVAILABLE', 'Freekassa временно недоступен'), - show_alert=True, - ) - return - - if payment_method == 'freekassa_sbp' and not settings.is_freekassa_sbp_enabled(): - await callback.answer( - texts.t('FREEKASSA_NOT_AVAILABLE', 'Freekassa временно недоступен'), - show_alert=True, - ) - return - - if payment_method == 'freekassa_card' and not settings.is_freekassa_card_enabled(): - await callback.answer( - texts.t('FREEKASSA_NOT_AVAILABLE', 'Freekassa временно недоступен'), - show_alert=True, - ) - return - - # Extract amount from callback data: topup_amount|{method}|{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 - - # Проверка ограничения на пополнение - 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'🚫 Пополнение ограничено\n\n{reason}', - parse_mode='HTML', - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), - ) - return - - # Validate amount - min_amount = settings.FREEKASSA_MIN_AMOUNT_KOPEKS - max_amount = settings.FREEKASSA_MAX_AMOUNT_KOPEKS - - if amount_kopeks < min_amount: - await callback.answer( - texts.t('AMOUNT_TOO_LOW_SHORT', 'Сумма слишком мала'), - show_alert=True, - ) - return - - if amount_kopeks > max_amount: - await callback.answer( - texts.t('AMOUNT_TOO_HIGH_SHORT', 'Сумма слишком велика'), - show_alert=True, - ) - return - - await callback.answer() - await state.clear() - - await _create_freekassa_payment_and_respond( - message_or_callback=callback.message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - edit_message=True, - payment_method=payment_method, - ) - - -@error_handler -async def process_freekassa_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - await _process_freekassa_quick_amount_impl(callback, db_user, db, state, 'freekassa') - - -@error_handler -async def process_freekassa_sbp_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - await _process_freekassa_quick_amount_impl(callback, db_user, db, state, 'freekassa_sbp') - - -@error_handler -async def process_freekassa_card_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - await _process_freekassa_quick_amount_impl(callback, db_user, db, state, 'freekassa_card') diff --git a/app/handlers/balance/heleket.py b/app/handlers/balance/heleket.py index 205c3ac8..30082e23 100644 --- a/app/handlers/balance/heleket.py +++ b/app/handlers/balance/heleket.py @@ -72,13 +72,6 @@ async def start_heleket_payment( keyboard = get_back_keyboard(db_user.language) - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_buttons: - keyboard.inline_keyboard = quick_buttons + keyboard.inline_keyboard - await callback.message.edit_text( '\n'.join(filter(None, message_lines)), reply_markup=keyboard, diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index cce40352..b813a8be 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -168,7 +168,7 @@ async def process_kassa_ai_payment_amount( state: FSMContext, payment_method: str = 'kassa_ai', ): - """Process payment amount directly (called from custom_amount and quick_amount handlers).""" + """Process payment amount directly (called from custom_amount handlers).""" texts = get_texts(db_user.language) # Проверка ограничения на пополнение @@ -275,54 +275,6 @@ async def _start_kassa_ai_sub_topup( ) -async def _process_kassa_ai_sub_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, - payment_method: str, -): - """Generic quick amount handler for any KassaAI sub-method.""" - cfg = _KASSA_AI_METHOD_CONFIG[payment_method] - texts = get_texts(db_user.language) - - if not cfg['is_enabled'](): - await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', cfg['unavailable_text']), show_alert=True) - return - - try: - parts = callback.data.split('|') - amount_kopeks = int(parts[2]) if len(parts) >= 3 else None - if amount_kopeks is None: - raise ValueError - except (ValueError, IndexError): - await callback.answer('Invalid amount', show_alert=True) - return - - if await _check_topup_restriction(callback, db_user): - return - - min_amount = settings.KASSA_AI_MIN_AMOUNT_KOPEKS - max_amount = settings.KASSA_AI_MAX_AMOUNT_KOPEKS - if amount_kopeks < min_amount: - await callback.answer(texts.t('AMOUNT_TOO_LOW_SHORT', 'Сумма слишком мала'), show_alert=True) - return - if amount_kopeks > max_amount: - await callback.answer(texts.t('AMOUNT_TOO_HIGH_SHORT', 'Сумма слишком велика'), show_alert=True) - return - - await callback.answer() - await state.clear() - await _create_kassa_ai_payment_and_respond( - message_or_callback=callback.message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - edit_message=True, - payment_method=payment_method, - ) - - # --- Public handler functions (registered in main.py) --- @@ -376,17 +328,6 @@ async def process_kassa_ai_custom_amount( ) -@error_handler -async def process_kassa_ai_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """Process quick amount selection for KassaAI payment.""" - await _process_kassa_ai_sub_quick_amount(callback, db_user, db, state, 'kassa_ai') - - @error_handler async def start_kassa_ai_sbp_topup( callback: types.CallbackQuery, @@ -398,17 +339,6 @@ async def start_kassa_ai_sbp_topup( await _start_kassa_ai_sub_topup(callback, db_user, db, state, 'kassa_ai_sbp') -@error_handler -async def process_kassa_ai_sbp_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """Process quick amount for KassaAI SBP.""" - await _process_kassa_ai_sub_quick_amount(callback, db_user, db, state, 'kassa_ai_sbp') - - @error_handler async def start_kassa_ai_card_topup( callback: types.CallbackQuery, @@ -420,12 +350,3 @@ async def start_kassa_ai_card_topup( await _start_kassa_ai_sub_topup(callback, db_user, db, state, 'kassa_ai_card') -@error_handler -async def process_kassa_ai_card_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """Process quick amount for KassaAI Card.""" - await _process_kassa_ai_sub_quick_amount(callback, db_user, db, state, 'kassa_ai_card') diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 03a5034d..0e880558 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -157,102 +157,6 @@ async def route_payment_by_method( return False -async def get_quick_amount_buttons(language: str, user: User) -> list: - """ - Generate quick amount buttons with user-specific pricing and discounts. - - Uses PricingEngine as the single source of truth for all price calculations, - including base period price, devices, servers, traffic, and per-category discounts. - - Args: - language: User's language for formatting - user: User object to calculate personalized discounts - - Returns: - List of button rows for inline keyboard - """ - if not settings.is_quick_amount_buttons_enabled(): - return [] - - from app.database.crud.subscription import get_subscription_by_user_id - from app.database.database import AsyncSessionLocal - from app.services.pricing_engine import pricing_engine - - texts = get_texts(language) - - buttons = [] - - async with AsyncSessionLocal() as db: - subscription = await get_subscription_by_user_id(db, user.id) - - tariff = None - tariff_periods = None - if settings.is_tariffs_mode() and subscription and subscription.tariff_id: - tariff = subscription.tariff - if tariff and tariff.period_prices: - tariff_periods = sorted(int(k) for k in tariff.period_prices.keys()) - - if tariff_periods: - periods = tariff_periods[:6] - else: - periods = settings.get_available_subscription_periods()[:6] - - for period in periods: - try: - if tariff and tariff_periods and period in tariff_periods: - result = await pricing_engine.calculate_tariff_purchase_price( - tariff, - period, - device_limit=subscription.device_limit if subscription else None, - user=user, - ) - elif subscription: - result = await pricing_engine.calculate_renewal_price(db, subscription, period, user=user) - else: - result = await pricing_engine.calculate_classic_new_subscription_price( - db, - period, - [], - 0, - settings.DEFAULT_DEVICE_LIMIT, - user=user, - ) - - total_price = result.final_total - original_total = result.original_total - - if total_price <= 0: - continue - - callback_data = f'quick_amount_{total_price}' - period_label = f'{period} дней' - - has_discount = original_total > total_price and original_total > 0 - if has_discount: - discount_pct = round((original_total - total_price) * 100 / original_total) - if discount_pct > 0: - button_text = ( - f'{texts.format_price(original_total)} ➜ ' - f'{texts.format_price(total_price)} ' - f'(-{discount_pct}%) • {period_label}' - ) - else: - button_text = f'{texts.format_price(total_price)} • {period_label}' - else: - button_text = f'{texts.format_price(total_price)} • {period_label}' - - buttons.append(types.InlineKeyboardButton(text=button_text, callback_data=callback_data)) - except Exception: - logger.warning('Failed to calculate price for period', period=period) - continue - - keyboard_rows = [] - for i in range(0, len(buttons), 2): - keyboard_rows.append(buttons[i : i + 2]) - - return keyboard_rows - - @error_handler async def show_balance_menu(callback: types.CallbackQuery, db_user: User, db: AsyncSession): # Проверяем, доступно ли сообщение @@ -640,37 +544,6 @@ async def handle_sbp_payment(callback: types.CallbackQuery, db: AsyncSession): await callback.answer('❌ Ошибка обработки платежа', show_alert=True) -@error_handler -async def handle_quick_amount_selection(callback: types.CallbackQuery, db_user: User, state: FSMContext): - """ - Обработчик выбора суммы через кнопки быстрого выбора - """ - # Проверяем, что пользователь в правильном состоянии FSM - current_state = await state.get_state() - if current_state != BalanceStates.waiting_for_amount: - await callback.answer('❌ Сначала выберите способ оплаты', show_alert=True) - return - - # Извлекаем сумму из callback_data - try: - amount_kopeks = int(callback.data.split('_')[-1]) - - # Получаем метод оплаты из состояния - data = await state.get_data() - payment_method = data.get('payment_method', 'yookassa') - - # Роутим платеж на соответствующий обработчик - if not await route_payment_by_method(callback.message, db_user, amount_kopeks, state, payment_method): - await callback.answer('❌ Неизвестный способ оплаты', show_alert=True) - return - - except ValueError: - await callback.answer('❌ Ошибка обработки суммы', show_alert=True) - except Exception as e: - logger.error('Ошибка обработки быстрого выбора суммы', error=e) - await callback.answer('❌ Ошибка обработки запроса', show_alert=True) - - @error_handler async def handle_topup_amount_callback( callback: types.CallbackQuery, @@ -797,52 +670,37 @@ def register_balance_handlers(dp: Dispatcher): 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 handle_cloudpayments_quick_amount, start_cloudpayments_payment + from .cloudpayments import start_cloudpayments_payment dp.callback_query.register(start_cloudpayments_payment, F.data == 'topup_cloudpayments') - dp.callback_query.register(handle_cloudpayments_quick_amount, F.data.startswith('topup_amount|cloudpayments|')) from .freekassa import ( - process_freekassa_card_quick_amount, - process_freekassa_quick_amount, - process_freekassa_sbp_quick_amount, 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(process_freekassa_quick_amount, F.data.startswith('topup_amount|freekassa|')) dp.callback_query.register(start_freekassa_sbp_topup, F.data == 'topup_freekassa_sbp') - dp.callback_query.register(process_freekassa_sbp_quick_amount, F.data.startswith('topup_amount|freekassa_sbp|')) dp.callback_query.register(start_freekassa_card_topup, F.data == 'topup_freekassa_card') - dp.callback_query.register(process_freekassa_card_quick_amount, F.data.startswith('topup_amount|freekassa_card|')) from .kassa_ai import ( - process_kassa_ai_card_quick_amount, - process_kassa_ai_quick_amount, - process_kassa_ai_sbp_quick_amount, start_kassa_ai_card_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(process_kassa_ai_quick_amount, F.data.startswith('topup_amount|kassa_ai|')) dp.callback_query.register(start_kassa_ai_sbp_topup, F.data == 'topup_kassa_ai_sbp') - dp.callback_query.register(process_kassa_ai_sbp_quick_amount, F.data.startswith('topup_amount|kassa_ai_sbp|')) dp.callback_query.register(start_kassa_ai_card_topup, F.data == 'topup_kassa_ai_card') - dp.callback_query.register(process_kassa_ai_card_quick_amount, F.data.startswith('topup_amount|kassa_ai_card|')) - from .riopay import process_riopay_quick_amount, start_riopay_topup + from .riopay import start_riopay_topup dp.callback_query.register(start_riopay_topup, F.data == 'topup_riopay') - dp.callback_query.register(process_riopay_quick_amount, F.data.startswith('topup_amount|riopay|')) - from .severpay import process_severpay_quick_amount, start_severpay_topup + from .severpay import start_severpay_topup dp.callback_query.register(start_severpay_topup, F.data == 'topup_severpay') - dp.callback_query.register(process_severpay_quick_amount, F.data.startswith('topup_amount|severpay|')) from .mulenpay import check_mulenpay_payment_status @@ -862,9 +720,6 @@ def register_balance_handlers(dp: Dispatcher): dp.callback_query.register(handle_payment_methods_unavailable, F.data == 'payment_methods_unavailable') - # Регистрируем обработчик для кнопок быстрого выбора суммы - dp.callback_query.register(handle_quick_amount_selection, F.data.startswith('quick_amount_')) - 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') diff --git a/app/handlers/balance/mulenpay.py b/app/handlers/balance/mulenpay.py index 04f308dd..8098b116 100644 --- a/app/handlers/balance/mulenpay.py +++ b/app/handlers/balance/mulenpay.py @@ -65,13 +65,6 @@ async def start_mulenpay_payment( keyboard = get_back_keyboard(db_user.language) - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text( message_text, reply_markup=keyboard, diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py index f0cafe83..5114afad 100644 --- a/app/handlers/balance/pal24.py +++ b/app/handlers/balance/pal24.py @@ -303,13 +303,6 @@ async def start_pal24_payment( keyboard = get_back_keyboard(db_user.language) - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text( message_text, reply_markup=keyboard, diff --git a/app/handlers/balance/platega.py b/app/handlers/balance/platega.py index 708e125e..50a13e11 100644 --- a/app/handlers/balance/platega.py +++ b/app/handlers/balance/platega.py @@ -71,13 +71,6 @@ async def _prompt_amount( keyboard = get_back_keyboard(db_user.language) - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await message.edit_text( prompt_template.format( method_name=method_name, diff --git a/app/handlers/balance/riopay.py b/app/handlers/balance/riopay.py index 6b198b69..ab482aab 100644 --- a/app/handlers/balance/riopay.py +++ b/app/handlers/balance/riopay.py @@ -136,7 +136,7 @@ async def process_riopay_payment_amount( state: FSMContext, ): """ - Process payment amount directly (called from quick_amount handlers). + Process payment amount directly. """ texts = get_texts(db_user.language) @@ -284,73 +284,3 @@ async def process_riopay_custom_amount( ) -@error_handler -async def process_riopay_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """ - Process quick amount selection for RioPay payment. - Called when user clicks a predefined amount button. - """ - texts = get_texts(db_user.language) - - if not settings.is_riopay_enabled(): - await callback.answer( - texts.t('RIOPAY_NOT_AVAILABLE', 'RioPay временно недоступен'), - show_alert=True, - ) - return - - # Extract amount from callback data: topup_amount|riopay|{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 - - restriction_kb = _check_topup_restriction(db_user, texts) - if restriction_kb: - reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' - await callback.message.edit_text( - f'🚫 Пополнение ограничено\n\n{reason}', - parse_mode='HTML', - reply_markup=restriction_kb, - ) - return - - # Validate amount - min_amount = settings.RIOPAY_MIN_AMOUNT_KOPEKS - max_amount = settings.RIOPAY_MAX_AMOUNT_KOPEKS - - if amount_kopeks < min_amount: - await callback.answer( - texts.t('AMOUNT_TOO_LOW_SHORT', 'Сумма слишком мала'), - show_alert=True, - ) - return - - if amount_kopeks > max_amount: - await callback.answer( - texts.t('AMOUNT_TOO_HIGH_SHORT', 'Сумма слишком велика'), - show_alert=True, - ) - return - - await callback.answer() - await state.clear() - - await _create_riopay_payment_and_respond( - message_or_callback=callback.message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - edit_message=True, - ) diff --git a/app/handlers/balance/severpay.py b/app/handlers/balance/severpay.py index 7282571c..338b77b3 100644 --- a/app/handlers/balance/severpay.py +++ b/app/handlers/balance/severpay.py @@ -136,7 +136,7 @@ async def process_severpay_payment_amount( state: FSMContext, ): """ - Process payment amount directly (called from quick_amount handlers). + Process payment amount directly. """ texts = get_texts(db_user.language) @@ -245,73 +245,3 @@ async def start_severpay_topup( ) -@error_handler -async def process_severpay_quick_amount( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, - state: FSMContext, -): - """ - Process quick amount selection for SeverPay payment. - Called when user clicks a predefined amount button. - """ - texts = get_texts(db_user.language) - - if not settings.is_severpay_enabled(): - await callback.answer( - texts.t('SEVERPAY_NOT_AVAILABLE', 'SeverPay временно недоступен'), - show_alert=True, - ) - return - - # Extract amount from callback data: topup_amount|severpay|{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 - - restriction_kb = _check_topup_restriction(db_user, texts) - if restriction_kb: - reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' - await callback.message.edit_text( - f'🚫 Пополнение ограничено\n\n{reason}', - parse_mode='HTML', - reply_markup=restriction_kb, - ) - return - - # Validate amount - min_amount = settings.SEVERPAY_MIN_AMOUNT_KOPEKS - max_amount = settings.SEVERPAY_MAX_AMOUNT_KOPEKS - - if amount_kopeks < min_amount: - await callback.answer( - texts.t('AMOUNT_TOO_LOW_SHORT', 'Сумма слишком мала'), - show_alert=True, - ) - return - - if amount_kopeks > max_amount: - await callback.answer( - texts.t('AMOUNT_TOO_HIGH_SHORT', 'Сумма слишком велика'), - show_alert=True, - ) - return - - await callback.answer() - await state.clear() - - await _create_severpay_payment_and_respond( - message_or_callback=callback.message, - db_user=db_user, - db=db, - amount_kopeks=amount_kopeks, - edit_message=True, - ) diff --git a/app/handlers/balance/stars.py b/app/handlers/balance/stars.py index 13ef433c..bc1eb0ff 100644 --- a/app/handlers/balance/stars.py +++ b/app/handlers/balance/stars.py @@ -40,24 +40,10 @@ async def start_stars_payment(callback: types.CallbackQuery, db_user: User, stat await callback.answer() return - # Формируем текст сообщения в зависимости от настройки - if settings.is_quick_amount_buttons_enabled(): - message_text = '⭐ Пополнение через Telegram Stars\n\nВыберите сумму пополнения или введите вручную:' - else: - message_text = texts.TOP_UP_AMOUNT + message_text = texts.TOP_UP_AMOUNT - # Создаем клавиатуру keyboard = get_back_keyboard(db_user.language) - # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - # Вставляем кнопки быстрого выбора перед кнопкой "Назад" - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text(message_text, reply_markup=keyboard) await state.update_data( diff --git a/app/handlers/balance/wata.py b/app/handlers/balance/wata.py index 362bba5c..c086abea 100644 --- a/app/handlers/balance/wata.py +++ b/app/handlers/balance/wata.py @@ -61,13 +61,6 @@ async def start_wata_payment( keyboard = get_back_keyboard(db_user.language) - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text( message_text, reply_markup=keyboard, diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py index 08767133..a6f9e025 100644 --- a/app/handlers/balance/yookassa.py +++ b/app/handlers/balance/yookassa.py @@ -46,31 +46,13 @@ async def start_yookassa_payment(callback: types.CallbackQuery, db_user: User, s min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 - # Формируем текст сообщения в зависимости от настройки - if settings.is_quick_amount_buttons_enabled(): - message_text = ( - f'💳 Оплата банковской картой\n\n' - f'Выберите сумму пополнения или введите вручную сумму ' - f'от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:' - ) - else: - message_text = ( - f'💳 Оплата банковской картой\n\n' - f'Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:' - ) + message_text = ( + f'💳 Оплата банковской картой\n\n' + f'Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:' + ) - # Создаем клавиатуру keyboard = get_back_keyboard(db_user.language) - # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - # Вставляем кнопки быстрого выбора перед кнопкой "Назад" - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text(message_text, reply_markup=keyboard, parse_mode='HTML') await state.set_state(BalanceStates.waiting_for_amount) @@ -110,31 +92,13 @@ async def start_yookassa_sbp_payment(callback: types.CallbackQuery, db_user: Use min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 - # Формируем текст сообщения в зависимости от настройки - if settings.is_quick_amount_buttons_enabled(): - message_text = ( - f'🏦 Оплата через СБП\n\n' - f'Выберите сумму пополнения или введите вручную сумму ' - f'от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:' - ) - else: - message_text = ( - f'🏦 Оплата через СБП\n\n' - f'Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:' - ) + message_text = ( + f'🏦 Оплата через СБП\n\n' + f'Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:' + ) - # Создаем клавиатуру keyboard = get_back_keyboard(db_user.language) - # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки - if settings.is_quick_amount_buttons_enabled(): - from .main import get_quick_amount_buttons - - quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user) - if quick_amount_buttons: - # Вставляем кнопки быстрого выбора перед кнопкой "Назад" - keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - await callback.message.edit_text(message_text, reply_markup=keyboard, parse_mode='HTML') await state.set_state(BalanceStates.waiting_for_amount) diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 063652f5..53b5adb0 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -275,7 +275,6 @@ class BotConfigurationService: 'SIMPLE_SUBSCRIPTION_DEVICE_LIMIT': 'SIMPLE_SUBSCRIPTION', 'SIMPLE_SUBSCRIPTION_TRAFFIC_GB': 'SIMPLE_SUBSCRIPTION', 'SIMPLE_SUBSCRIPTION_SQUAD_UUID': 'SIMPLE_SUBSCRIPTION', - 'DISABLE_TOPUP_BUTTONS': 'PAYMENT', 'SUPPORT_TOPUP_ENABLED': 'PAYMENT', 'ENABLE_NOTIFICATIONS': 'NOTIFICATIONS', 'NOTIFICATION_RETRY_ATTEMPTS': 'NOTIFICATIONS', From 0ceff44c30cf11466c4cbe51b520558c51c4af4a Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 18:05:53 +0300 Subject: [PATCH 6/8] fix: sync crypto link from happ.cryptoLink in webhook handlers Webhook only checked subscriptionCryptoLink field, missing happ.cryptoLink fallback that sync already used. Also clear stale crypto link when URL changes but no new crypto link is provided. --- app/services/remnawave_webhook_service.py | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 595a6eb1..449d2ace 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -654,13 +654,16 @@ class RemnaWaveWebhookService: changed = True # Sync subscription crypto link (for HAPP_CRYPT4_LINK) - subscription_crypto_link = data.get('subscriptionCryptoLink') - if ( - subscription_crypto_link - and self._is_valid_link(subscription_crypto_link) - and subscription.subscription_crypto_link != subscription_crypto_link - ): - subscription.subscription_crypto_link = subscription_crypto_link + subscription_crypto_link = data.get('subscriptionCryptoLink') or (data.get('happ') or {}).get( + 'cryptoLink', '' + ) + if subscription_crypto_link and self._is_valid_link(subscription_crypto_link): + if subscription.subscription_crypto_link != subscription_crypto_link: + subscription.subscription_crypto_link = subscription_crypto_link + changed = True + elif subscription_url and subscription.subscription_crypto_link: + # URL обновился, а крипто-ссылка не пришла — сбрасываем старую + subscription.subscription_crypto_link = None changed = True # Always stamp to protect from sync overwrite, even if no fields changed @@ -743,18 +746,20 @@ class RemnaWaveWebhookService: ) -> None: if subscription: new_url = data.get('subscriptionUrl') - new_crypto_link = data.get('subscriptionCryptoLink') + new_crypto_link = data.get('subscriptionCryptoLink') or (data.get('happ') or {}).get( + 'cryptoLink', '' + ) changed = False if new_url and self._is_valid_url(new_url) and subscription.subscription_url != new_url: subscription.subscription_url = new_url changed = True - if ( - new_crypto_link - and self._is_valid_link(new_crypto_link) - and subscription.subscription_crypto_link != new_crypto_link - ): - subscription.subscription_crypto_link = new_crypto_link + if new_crypto_link and self._is_valid_link(new_crypto_link): + if subscription.subscription_crypto_link != new_crypto_link: + subscription.subscription_crypto_link = new_crypto_link + changed = True + elif new_url and subscription.subscription_crypto_link: + subscription.subscription_crypto_link = None changed = True # Always stamp to protect from sync overwrite From 5081debee7625954bd7b82f66e09dd58e08e8822 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 18:15:20 +0300 Subject: [PATCH 7/8] fix: add null check for subscription in execute_change_devices --- app/handlers/subscription/devices.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py index 8e6a2cdb..6489574f 100644 --- a/app/handlers/subscription/devices.py +++ b/app/handlers/subscription/devices.py @@ -497,6 +497,12 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d texts = get_texts(db_user.language) subscription = db_user.subscription + if not subscription: + await callback.answer( + texts.t('NO_ACTIVE_SUBSCRIPTION', '⚠️ У вас нет активной подписки'), + show_alert=True, + ) + return current_devices = subscription.device_limit # Проверяем тариф подписки From a6dcf26c20c79511982d9c6ea77d3eba18fabcd9 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 18 Mar 2026 18:20:50 +0300 Subject: [PATCH 8/8] chore: ruff format and cleanup --- app/handlers/balance/cloudpayments.py | 2 -- app/handlers/balance/freekassa.py | 2 -- app/handlers/balance/kassa_ai.py | 2 -- app/handlers/balance/riopay.py | 2 -- app/handlers/balance/severpay.py | 2 -- app/services/remnawave_webhook_service.py | 8 ++------ app/services/riopay_service.py | 1 - uv.lock | 2 +- 8 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/handlers/balance/cloudpayments.py b/app/handlers/balance/cloudpayments.py index a04da927..7aa6420a 100644 --- a/app/handlers/balance/cloudpayments.py +++ b/app/handlers/balance/cloudpayments.py @@ -375,5 +375,3 @@ async def process_cloudpayments_amount( ) logger.info('CloudPayments payment created: user amount=₽', telegram_id=db_user.telegram_id, amount_rub=amount_rub) - - diff --git a/app/handlers/balance/freekassa.py b/app/handlers/balance/freekassa.py index 2699e5a4..5aa28b92 100644 --- a/app/handlers/balance/freekassa.py +++ b/app/handlers/balance/freekassa.py @@ -370,5 +370,3 @@ async def process_freekassa_custom_amount( state=state, payment_method=data.get('payment_method'), ) - - diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index b813a8be..b1555ca5 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -348,5 +348,3 @@ async def start_kassa_ai_card_topup( ): """Start KassaAI Card top-up process.""" await _start_kassa_ai_sub_topup(callback, db_user, db, state, 'kassa_ai_card') - - diff --git a/app/handlers/balance/riopay.py b/app/handlers/balance/riopay.py index ab482aab..0c79403c 100644 --- a/app/handlers/balance/riopay.py +++ b/app/handlers/balance/riopay.py @@ -282,5 +282,3 @@ async def process_riopay_custom_amount( amount_kopeks=amount_kopeks, state=state, ) - - diff --git a/app/handlers/balance/severpay.py b/app/handlers/balance/severpay.py index 338b77b3..6d994d09 100644 --- a/app/handlers/balance/severpay.py +++ b/app/handlers/balance/severpay.py @@ -243,5 +243,3 @@ async def start_severpay_topup( parse_mode='HTML', reply_markup=keyboard, ) - - diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 449d2ace..dce9871c 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -654,9 +654,7 @@ class RemnaWaveWebhookService: changed = True # Sync subscription crypto link (for HAPP_CRYPT4_LINK) - subscription_crypto_link = data.get('subscriptionCryptoLink') or (data.get('happ') or {}).get( - 'cryptoLink', '' - ) + subscription_crypto_link = data.get('subscriptionCryptoLink') or (data.get('happ') or {}).get('cryptoLink', '') if subscription_crypto_link and self._is_valid_link(subscription_crypto_link): if subscription.subscription_crypto_link != subscription_crypto_link: subscription.subscription_crypto_link = subscription_crypto_link @@ -746,9 +744,7 @@ class RemnaWaveWebhookService: ) -> None: if subscription: new_url = data.get('subscriptionUrl') - new_crypto_link = data.get('subscriptionCryptoLink') or (data.get('happ') or {}).get( - 'cryptoLink', '' - ) + new_crypto_link = data.get('subscriptionCryptoLink') or (data.get('happ') or {}).get('cryptoLink', '') changed = False if new_url and self._is_valid_url(new_url) and subscription.subscription_url != new_url: diff --git a/app/services/riopay_service.py b/app/services/riopay_service.py index 20dbc419..91796ea1 100644 --- a/app/services/riopay_service.py +++ b/app/services/riopay_service.py @@ -70,7 +70,6 @@ class RioPayService: external_id: str, purpose: str = 'Пополнение баланса', success_url: str | None = None, - ) -> dict[str, Any]: """ Создает заказ через API RioPay. diff --git a/uv.lock b/uv.lock index 000fc907..d94e229f 100644 --- a/uv.lock +++ b/uv.lock @@ -1115,7 +1115,7 @@ wheels = [ [[package]] name = "remnawave-bedolaga-telegram-bot" -version = "3.32.4" +version = "3.33.0" source = { virtual = "." } dependencies = [ { name = "aiogram" },