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/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/config.py b/app/config.py index 491f013b..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 self.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not self.DISABLE_TOPUP_BUTTONS - def get_available_languages(self) -> list[str]: defaults = ['ru', 'en', 'ua', 'zh', 'fa'] 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/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/cloudpayments.py b/app/handlers/balance/cloudpayments.py index aa34b137..7aa6420a 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) @@ -375,123 +375,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..5aa28b92 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) @@ -370,127 +370,3 @@ async def process_freekassa_custom_amount( state=state, payment_method=data.get('payment_method'), ) - - -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..b1555ca5 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, @@ -418,14 +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') - - -@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 415de81d..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): # Проверяем, доступно ли сообщение @@ -379,9 +283,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): @@ -627,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, @@ -784,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 @@ -849,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..0c79403c 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) @@ -282,75 +282,3 @@ async def process_riopay_custom_amount( amount_kopeks=amount_kopeks, state=state, ) - - -@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..6d994d09 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) @@ -243,75 +243,3 @@ async def start_severpay_topup( parse_mode='HTML', reply_markup=keyboard, ) - - -@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/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 # Проверяем тариф подписки 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 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/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 595a6eb1..dce9871c 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -654,13 +654,14 @@ 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 +744,18 @@ 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 diff --git a/app/services/riopay_service.py b/app/services/riopay_service.py index 70221123..91796ea1 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,9 @@ 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 +80,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: 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', 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( 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" },