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" },