From 04419fdff7dc244eb2c9553ea1501e2de454010b Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Mar 2026 14:21:54 +0000 Subject: [PATCH 1/6] feat: add SBP and Card sub-options to kassa_ai payment method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kassa_ai shows single admin entry with СБП/Карта sub-option checkboxes - SBP routes to payment_system_id=44, Card to payment_system_id=36 - Bot: added kassa_ai_sbp/card handlers and FSM flow (mirrors freekassa pattern) - Cabinet: KASSA_AI_OPTION_MAP reads payment_option to select correct ps_id - Config: KASSA_AI_SBP_ENABLED / KASSA_AI_CARD_ENABLED env vars + helpers - Guest payments: kassa_ai_sbp/card supported in landing page checkout - payment_method_config_service: kassa_ai has available_sub_options=[sbp,card] --- app/cabinet/routes/balance.py | 6 + app/config.py | 24 ++ app/handlers/balance/kassa_ai.py | 234 +++++++++++++++++- app/handlers/balance/main.py | 17 +- app/keyboards/inline.py | 30 ++- app/services/payment/kassa_ai.py | 5 +- app/services/payment_method_config_service.py | 5 +- app/services/payment_service.py | 7 +- 8 files changed, 318 insertions(+), 10 deletions(-) diff --git a/app/cabinet/routes/balance.py b/app/cabinet/routes/balance.py index 37ef85a7..34bb1ca7 100644 --- a/app/cabinet/routes/balance.py +++ b/app/cabinet/routes/balance.py @@ -701,6 +701,11 @@ async def create_topup( detail='KassaAI payment method is unavailable', ) + # Use payment_option to select sbp or card + KASSA_AI_OPTION_MAP = {'sbp': 44, 'card': 36} + option = (request.payment_option or '').strip().lower() + ps_id = KASSA_AI_OPTION_MAP.get(option) # None = use env default + payment_service = PaymentService() result = await payment_service.create_kassa_ai_payment( db=db, @@ -709,6 +714,7 @@ async def create_topup( description=settings.get_balance_payment_description(request.amount_kopeks), email=getattr(user, 'email', None), language=getattr(user, 'language', None) or settings.DEFAULT_LANGUAGE, + payment_system_id=ps_id, ) if result and result.get('payment_url'): diff --git a/app/config.py b/app/config.py index 54dfd07b..69bab8f5 100644 --- a/app/config.py +++ b/app/config.py @@ -535,6 +535,11 @@ class Settings(BaseSettings): KASSA_AI_WEBHOOK_PORT: int = 8089 # Способ оплаты: 44 = СБП (QR код), 36 = Карты РФ, 43 = SberPay KASSA_AI_PAYMENT_SYSTEM_ID: int = 44 + # Раздельные методы оплаты KassaAI (отображаются как отдельные кнопки) + KASSA_AI_SBP_ENABLED: bool = False # СБП — payment_system_id=44 + KASSA_AI_SBP_DISPLAY_NAME: str = 'СБП (KassaAI)' + KASSA_AI_CARD_ENABLED: bool = False # Карты РФ — payment_system_id=36 + KASSA_AI_CARD_DISPLAY_NAME: str = 'Карта (KassaAI)' # RioPay (api.riopay.online) v2.0.1 RIOPAY_ENABLED: bool = False @@ -1849,6 +1854,25 @@ class Settings(BaseSettings): def get_riopay_display_name_html(self) -> str: return html.escape(self.get_riopay_display_name()) + def is_kassa_ai_sbp_enabled(self) -> bool: + return self.KASSA_AI_SBP_ENABLED and self.is_kassa_ai_enabled() + + def get_kassa_ai_sbp_display_name(self) -> str: + name = (self.KASSA_AI_SBP_DISPLAY_NAME or '').strip() + return name if name else 'СБП (KassaAI)' + + def get_kassa_ai_sbp_display_name_html(self) -> str: + return html.escape(self.get_kassa_ai_sbp_display_name()) + + def is_kassa_ai_card_enabled(self) -> bool: + return self.KASSA_AI_CARD_ENABLED and self.is_kassa_ai_enabled() + + def get_kassa_ai_card_display_name(self) -> str: + name = (self.KASSA_AI_CARD_DISPLAY_NAME or '').strip() + return name if name else 'Карта (KassaAI)' + + def get_kassa_ai_card_display_name_html(self) -> str: + return html.escape(self.get_kassa_ai_card_display_name()) def is_payment_verification_auto_check_enabled(self) -> bool: return self.PAYMENT_VERIFICATION_AUTO_CHECK_ENABLED diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index 55c449af..4c73ce33 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -18,12 +18,20 @@ from app.utils.decorators import error_handler logger = structlog.get_logger(__name__) +KASSA_AI_SUB_METHODS = { + 'kassa_ai_sbp': {'payment_system_id': 44}, + 'kassa_ai_card': {'payment_system_id': 36}, +} +KASSA_AI_PAYMENT_METHODS = {'kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'} + + async def _create_kassa_ai_payment_and_respond( message_or_callback, db_user: User, db: AsyncSession, amount_kopeks: int, edit_message: bool = False, + payment_method: str = 'kassa_ai', ): """ Common logic for creating KassaAI payment and sending response. @@ -34,6 +42,7 @@ async def _create_kassa_ai_payment_and_respond( db: Database session amount_kopeks: Amount in kopeks edit_message: Whether to edit existing message or send new one + payment_method: 'kassa_ai', 'kassa_ai_sbp', or 'kassa_ai_card' """ texts = get_texts(db_user.language) amount_rub = amount_kopeks / 100 @@ -46,6 +55,9 @@ async def _create_kassa_ai_payment_and_respond( description='Пополнение баланса', ) + sub = KASSA_AI_SUB_METHODS.get(payment_method) + payment_system_id = sub['payment_system_id'] if sub else settings.KASSA_AI_PAYMENT_SYSTEM_ID + result = await payment_service.create_kassa_ai_payment( db=db, user_id=db_user.id, @@ -53,6 +65,7 @@ async def _create_kassa_ai_payment_and_respond( description=description, email=getattr(db_user, 'email', None), language=db_user.language, + payment_system_id=payment_system_id, ) if not result: @@ -74,7 +87,12 @@ async def _create_kassa_ai_payment_and_respond( return payment_url = result.get('payment_url') - display_name = settings.get_kassa_ai_display_name() + if payment_method == 'kassa_ai_sbp': + display_name = settings.get_kassa_ai_sbp_display_name() + elif payment_method == 'kassa_ai_card': + display_name = settings.get_kassa_ai_card_display_name() + else: + display_name = settings.get_kassa_ai_display_name() # Create keyboard with payment button keyboard = InlineKeyboardMarkup( @@ -128,6 +146,7 @@ async def process_kassa_ai_payment_amount( db: AsyncSession, amount_kopeks: int, state: FSMContext, + payment_method: str = 'kassa_ai', ): """ Process payment amount directly (called from quick_amount handlers). @@ -183,6 +202,7 @@ async def process_kassa_ai_payment_amount( db=db, amount_kopeks=amount_kopeks, edit_message=False, + payment_method=payment_method, ) @@ -260,7 +280,8 @@ async def process_kassa_ai_custom_amount( Process custom amount input for KassaAI payment. """ data = await state.get_data() - if data.get('payment_method') != 'kassa_ai': + pm = data.get('payment_method', 'kassa_ai') + if pm not in ('kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'): return texts = get_texts(db_user.language) @@ -285,6 +306,7 @@ async def process_kassa_ai_custom_amount( db=db, amount_kopeks=amount_kopeks, state=state, + payment_method=pm, ) @@ -364,3 +386,211 @@ async def process_kassa_ai_quick_amount( amount_kopeks=amount_kopeks, edit_message=True, ) + + +@error_handler +async def start_kassa_ai_sbp_topup( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + """Start KassaAI SBP top-up process.""" + texts = get_texts(db_user.language) + + if getattr(db_user, 'restriction_topup', False): + reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' + support_url = settings.get_support_contact_url() + keyboard = [] + if support_url: + keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)]) + keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')]) + await callback.message.edit_text( + f'🚫 Пополнение ограничено\n\n{reason}', + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), + ) + return + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method='kassa_ai_sbp') + + min_amount = settings.KASSA_AI_MIN_AMOUNT_KOPEKS // 100 + max_amount = settings.KASSA_AI_MAX_AMOUNT_KOPEKS // 100 + display_name = settings.get_kassa_ai_sbp_display_name() + + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')] + ] + ) + + await callback.message.edit_text( + texts.t( + 'KASSA_AI_ENTER_AMOUNT', + '💳 Пополнение через {name}\n\nВведите сумму пополнения в рублях.\n\nМинимум: {min_amount}₽\nМаксимум: {max_amount}₽', + ).format(name=display_name, min_amount=min_amount, max_amount=f'{max_amount:,}'.replace(',', ' ')), + parse_mode='HTML', + reply_markup=keyboard, + ) + + +@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.""" + texts = get_texts(db_user.language) + if not settings.is_kassa_ai_sbp_enabled(): + await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI СБП временно недоступен'), 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 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 + + 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='kassa_ai_sbp', + ) + + +@error_handler +async def start_kassa_ai_card_topup( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + """Start KassaAI Card top-up process.""" + texts = get_texts(db_user.language) + + if getattr(db_user, 'restriction_topup', False): + reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' + support_url = settings.get_support_contact_url() + keyboard = [] + if support_url: + keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)]) + keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')]) + await callback.message.edit_text( + f'🚫 Пополнение ограничено\n\n{reason}', + parse_mode='HTML', + reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), + ) + return + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method='kassa_ai_card') + + min_amount = settings.KASSA_AI_MIN_AMOUNT_KOPEKS // 100 + max_amount = settings.KASSA_AI_MAX_AMOUNT_KOPEKS // 100 + display_name = settings.get_kassa_ai_card_display_name() + + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')] + ] + ) + + await callback.message.edit_text( + texts.t( + 'KASSA_AI_ENTER_AMOUNT', + '💳 Пополнение через {name}\n\nВведите сумму пополнения в рублях.\n\nМинимум: {min_amount}₽\nМаксимум: {max_amount}₽', + ).format(name=display_name, min_amount=min_amount, max_amount=f'{max_amount:,}'.replace(',', ' ')), + parse_mode='HTML', + reply_markup=keyboard, + ) + + +@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.""" + texts = get_texts(db_user.language) + if not settings.is_kassa_ai_card_enabled(): + await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI Карта временно недоступна'), 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 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 + + 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='kassa_ai_card', + ) diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 3fdd1f15..b2594fd6 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -131,11 +131,11 @@ async def route_payment_by_method( ) return True - if payment_method == 'kassa_ai': + if payment_method in ('kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'): from .kassa_ai import process_kassa_ai_payment_amount async with AsyncSessionLocal() as db: - await process_kassa_ai_payment_amount(message, db_user, db, amount_kopeks, state) + await process_kassa_ai_payment_amount(message, db_user, db, amount_kopeks, state, payment_method=payment_method) return True if payment_method == 'riopay': @@ -790,10 +790,21 @@ def register_balance_handlers(dp: Dispatcher): 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_quick_amount, start_kassa_ai_topup + 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 diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 1196e93e..d7208536 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1695,7 +1695,35 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LAN ) has_direct_payment_methods = True - if settings.is_kassa_ai_enabled(): + if settings.is_kassa_ai_sbp_enabled(): + sbp_name = settings.get_kassa_ai_sbp_display_name() + keyboard.append( + [ + InlineKeyboardButton( + text=texts.t('PAYMENT_KASSA_AI_SBP', f'📱 {sbp_name}'), + callback_data=_build_callback('kassa_ai_sbp'), + ) + ] + ) + has_direct_payment_methods = True + + if settings.is_kassa_ai_card_enabled(): + card_name = settings.get_kassa_ai_card_display_name() + keyboard.append( + [ + InlineKeyboardButton( + text=texts.t('PAYMENT_KASSA_AI_CARD', f'💳 {card_name}'), + callback_data=_build_callback('kassa_ai_card'), + ) + ] + ) + has_direct_payment_methods = True + + if ( + settings.is_kassa_ai_enabled() + and not settings.is_kassa_ai_sbp_enabled() + and not settings.is_kassa_ai_card_enabled() + ): kassa_ai_name = settings.get_kassa_ai_display_name() keyboard.append( [ diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py index 9f7273c8..7292e85d 100644 --- a/app/services/payment/kassa_ai.py +++ b/app/services/payment/kassa_ai.py @@ -28,6 +28,7 @@ class KassaAiPaymentMixin: description: str = 'Пополнение баланса', email: str | None = None, language: str = 'ru', + payment_system_id: int | None = None, ) -> dict[str, Any] | None: """ Создает платеж KassaAI. @@ -96,7 +97,7 @@ class KassaAiPaymentMixin: amount=amount_rubles, currency=currency, email=email, - payment_system_id=settings.KASSA_AI_PAYMENT_SYSTEM_ID, + payment_system_id=payment_system_id if payment_system_id is not None else settings.KASSA_AI_PAYMENT_SYSTEM_ID, ) payment_url = result.get('location') @@ -118,7 +119,7 @@ class KassaAiPaymentMixin: currency=currency, description=description, payment_url=payment_url, - payment_system_id=settings.KASSA_AI_PAYMENT_SYSTEM_ID, + payment_system_id=payment_system_id if payment_system_id is not None else settings.KASSA_AI_PAYMENT_SYSTEM_ID, expires_at=expires_at, metadata_json=metadata, ) diff --git a/app/services/payment_method_config_service.py b/app/services/payment_method_config_service.py index b4d79cb4..7cb25a84 100644 --- a/app/services/payment_method_config_service.py +++ b/app/services/payment_method_config_service.py @@ -126,7 +126,10 @@ def _get_method_defaults() -> dict: 'is_configured': settings.is_kassa_ai_enabled(), 'default_min': settings.KASSA_AI_MIN_AMOUNT_KOPEKS, 'default_max': settings.KASSA_AI_MAX_AMOUNT_KOPEKS, - 'available_sub_options': None, + 'available_sub_options': [ + {'id': 'sbp', 'name': 'СБП'}, + {'id': 'card', 'name': 'Карта'}, + ], }, 'riopay': { 'default_display_name': settings.get_riopay_display_name(), diff --git a/app/services/payment_service.py b/app/services/payment_service.py index bd979756..e0d59095 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -666,16 +666,21 @@ class PaymentService( return None # --- KassaAI ---------------------------------------------------------- - if payment_method == 'kassa_ai': + if payment_method in ('kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'): if not settings.is_kassa_ai_enabled(): logger.warning('KassaAI is not enabled, cannot create guest payment') return None + from app.handlers.balance.kassa_ai import KASSA_AI_SUB_METHODS + sub = KASSA_AI_SUB_METHODS.get(payment_method) + ps_id = sub['payment_system_id'] if sub else None + result = await self.create_kassa_ai_payment( db=db, user_id=None, amount_kopeks=amount_kopeks, description=description, + payment_system_id=ps_id, ) if result: await _patch_guest_metadata(result['local_payment_id'], 'kassa_ai') From cda239241122ae1dd02a252b3eadc47453c0c48b Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Mar 2026 15:21:08 +0000 Subject: [PATCH 2/6] refactor: move KASSA_AI_SUB_METHODS to service layer, add early enabled checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move KASSA_AI_SUB_METHODS from handler to kassa_ai_service.py (fixes service→handler import violation) - Remove KASSA_AI_PAYMENT_METHODS set (was defined but unused) - Import KASSA_AI_SUB_METHODS in payment_service.py from service layer - Add is_kassa_ai_sbp/card_enabled() checks at start of entry handler functions --- app/handlers/balance/kassa_ai.py | 14 +++++++++----- app/services/kassa_ai_service.py | 6 ++++++ app/services/payment_service.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index 4c73ce33..7f7730e2 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -18,11 +18,7 @@ from app.utils.decorators import error_handler logger = structlog.get_logger(__name__) -KASSA_AI_SUB_METHODS = { - 'kassa_ai_sbp': {'payment_system_id': 44}, - 'kassa_ai_card': {'payment_system_id': 36}, -} -KASSA_AI_PAYMENT_METHODS = {'kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'} +from app.services.kassa_ai_service import KASSA_AI_SUB_METHODS async def _create_kassa_ai_payment_and_respond( @@ -398,6 +394,10 @@ async def start_kassa_ai_sbp_topup( """Start KassaAI SBP top-up process.""" texts = get_texts(db_user.language) + if not settings.is_kassa_ai_sbp_enabled(): + await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI СБП временно недоступен'), 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() @@ -502,6 +502,10 @@ async def start_kassa_ai_card_topup( """Start KassaAI Card top-up process.""" texts = get_texts(db_user.language) + if not settings.is_kassa_ai_card_enabled(): + await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI Карта временно недоступна'), 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() diff --git a/app/services/kassa_ai_service.py b/app/services/kassa_ai_service.py index 3b37f838..bb51e35a 100644 --- a/app/services/kassa_ai_service.py +++ b/app/services/kassa_ai_service.py @@ -14,6 +14,12 @@ from app.config import settings logger = structlog.get_logger(__name__) +# Sub-method to payment_system_id mapping +KASSA_AI_SUB_METHODS = { + "kassa_ai_sbp": {"payment_system_id": 44}, + "kassa_ai_card": {"payment_system_id": 36}, +} + # Кэш для публичного IP _cached_public_ip: str | None = None _ip_fetch_lock = asyncio.Lock() diff --git a/app/services/payment_service.py b/app/services/payment_service.py index e0d59095..63e140b2 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -671,7 +671,7 @@ class PaymentService( logger.warning('KassaAI is not enabled, cannot create guest payment') return None - from app.handlers.balance.kassa_ai import KASSA_AI_SUB_METHODS + from app.services.kassa_ai_service import KASSA_AI_SUB_METHODS sub = KASSA_AI_SUB_METHODS.get(payment_method) ps_id = sub['payment_system_id'] if sub else None From 6a3e9d92b5daa03c4e93f9d051678b0035f20328 Mon Sep 17 00:00:00 2001 From: "sMedia.tech" <81699471+smediainfo@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:23:45 +0300 Subject: [PATCH 3/6] style: ruff format kassa_ai_service.py --- app/services/kassa_ai_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/kassa_ai_service.py b/app/services/kassa_ai_service.py index bb51e35a..24d33b42 100644 --- a/app/services/kassa_ai_service.py +++ b/app/services/kassa_ai_service.py @@ -16,8 +16,8 @@ logger = structlog.get_logger(__name__) # Sub-method to payment_system_id mapping KASSA_AI_SUB_METHODS = { - "kassa_ai_sbp": {"payment_system_id": 44}, - "kassa_ai_card": {"payment_system_id": 36}, + 'kassa_ai_sbp': {'payment_system_id': 44}, + 'kassa_ai_card': {'payment_system_id': 36}, } # Кэш для публичного IP From 557af5994d8208e1c1904d32bb190c1b87c3d3ee Mon Sep 17 00:00:00 2001 From: root Date: Mon, 16 Mar 2026 04:31:04 +0000 Subject: [PATCH 4/6] style: ruff format kassa_ai files --- app/config.py | 1 + app/handlers/balance/kassa_ai.py | 8 ++------ app/handlers/balance/main.py | 4 +++- app/services/payment/kassa_ai.py | 8 ++++++-- app/services/payment_service.py | 1 + 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/config.py b/app/config.py index 69bab8f5..938e1c7d 100644 --- a/app/config.py +++ b/app/config.py @@ -1854,6 +1854,7 @@ class Settings(BaseSettings): def get_riopay_display_name_html(self) -> str: return html.escape(self.get_riopay_display_name()) + def is_kassa_ai_sbp_enabled(self) -> bool: return self.KASSA_AI_SBP_ENABLED and self.is_kassa_ai_enabled() diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index 7f7730e2..0234f1d2 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -420,9 +420,7 @@ async def start_kassa_ai_sbp_topup( display_name = settings.get_kassa_ai_sbp_display_name() keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')] - ] + inline_keyboard=[[InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')]] ) await callback.message.edit_text( @@ -528,9 +526,7 @@ async def start_kassa_ai_card_topup( display_name = settings.get_kassa_ai_card_display_name() keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')] - ] + inline_keyboard=[[InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')]] ) await callback.message.edit_text( diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index b2594fd6..df127607 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -135,7 +135,9 @@ async def route_payment_by_method( from .kassa_ai import process_kassa_ai_payment_amount async with AsyncSessionLocal() as db: - await process_kassa_ai_payment_amount(message, db_user, db, amount_kopeks, state, payment_method=payment_method) + await process_kassa_ai_payment_amount( + message, db_user, db, amount_kopeks, state, payment_method=payment_method + ) return True if payment_method == 'riopay': diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py index 7292e85d..7a88162d 100644 --- a/app/services/payment/kassa_ai.py +++ b/app/services/payment/kassa_ai.py @@ -97,7 +97,9 @@ class KassaAiPaymentMixin: amount=amount_rubles, currency=currency, email=email, - payment_system_id=payment_system_id if payment_system_id is not None else settings.KASSA_AI_PAYMENT_SYSTEM_ID, + payment_system_id=payment_system_id + if payment_system_id is not None + else settings.KASSA_AI_PAYMENT_SYSTEM_ID, ) payment_url = result.get('location') @@ -119,7 +121,9 @@ class KassaAiPaymentMixin: currency=currency, description=description, payment_url=payment_url, - payment_system_id=payment_system_id if payment_system_id is not None else settings.KASSA_AI_PAYMENT_SYSTEM_ID, + payment_system_id=payment_system_id + if payment_system_id is not None + else settings.KASSA_AI_PAYMENT_SYSTEM_ID, expires_at=expires_at, metadata_json=metadata, ) diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 63e140b2..e8554742 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -672,6 +672,7 @@ class PaymentService( return None from app.services.kassa_ai_service import KASSA_AI_SUB_METHODS + sub = KASSA_AI_SUB_METHODS.get(payment_method) ps_id = sub['payment_system_id'] if sub else None From 603b9a1f4610a5b288d6fba78c98474c439529aa Mon Sep 17 00:00:00 2001 From: root Date: Mon, 16 Mar 2026 04:40:36 +0000 Subject: [PATCH 5/6] fix: sub-method enabled check, guest payment provider, silent FSM return --- app/handlers/balance/kassa_ai.py | 12 ++++++++++++ app/services/payment_service.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index 0234f1d2..72a9af28 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -149,6 +149,16 @@ async def process_kassa_ai_payment_amount( """ texts = get_texts(db_user.language) + # Проверка что sub-method включён + if payment_method == 'kassa_ai_sbp' and not settings.is_kassa_ai_sbp_enabled(): + await message.answer('Метод оплаты СБП через KassaAI временно недоступен.', parse_mode='HTML') + await state.clear() + return + if payment_method == 'kassa_ai_card' and not settings.is_kassa_ai_card_enabled(): + await message.answer('Метод оплаты картой через KassaAI временно недоступен.', parse_mode='HTML') + await state.clear() + return + # Проверка ограничения на пополнение if getattr(db_user, 'restriction_topup', False): reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' @@ -278,6 +288,8 @@ async def process_kassa_ai_custom_amount( data = await state.get_data() pm = data.get('payment_method', 'kassa_ai') if pm not in ('kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'): + await state.clear() + await message.answer('Неизвестный метод оплаты. Попробуйте снова.', parse_mode='HTML') return texts = get_texts(db_user.language) diff --git a/app/services/payment_service.py b/app/services/payment_service.py index e8554742..c952030b 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -684,11 +684,11 @@ class PaymentService( payment_system_id=ps_id, ) if result: - await _patch_guest_metadata(result['local_payment_id'], 'kassa_ai') + await _patch_guest_metadata(result['local_payment_id'], payment_method) return { 'payment_url': result.get('payment_url'), 'payment_id': result.get('order_id'), - 'provider': 'kassa_ai', + 'provider': payment_method, } return None From e4bb0430fb9cc4f55013767ceda6d1c214bd80a6 Mon Sep 17 00:00:00 2001 From: "sMedia.tech" <81699471+smediainfo@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:53:34 +0300 Subject: [PATCH 6/6] refactor: deduplicate KassaAI handlers with config dict and shared helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract _KASSA_AI_METHOD_CONFIG dict, _check_topup_restriction() helper, and generic _start_kassa_ai_sub_topup / _process_kassa_ai_sub_quick_amount implementations. Public handlers become thin wrappers. 608 → 429 lines (-30%), eliminates 5 copies of restriction check block and 3 pairs of nearly-identical start/quick-amount handlers. Co-Authored-By: Claude Opus 4.6 --- app/handlers/balance/kassa_ai.py | 433 +++++++++---------------------- 1 file changed, 127 insertions(+), 306 deletions(-) diff --git a/app/handlers/balance/kassa_ai.py b/app/handlers/balance/kassa_ai.py index 72a9af28..4a1fda47 100644 --- a/app/handlers/balance/kassa_ai.py +++ b/app/handlers/balance/kassa_ai.py @@ -10,6 +10,7 @@ from app.config import settings from app.database.models import User from app.keyboards.inline import get_back_keyboard from app.localization.texts import get_texts +from app.services.kassa_ai_service import KASSA_AI_SUB_METHODS from app.services.payment_service import PaymentService from app.states import BalanceStates from app.utils.decorators import error_handler @@ -18,7 +19,44 @@ from app.utils.decorators import error_handler logger = structlog.get_logger(__name__) -from app.services.kassa_ai_service import KASSA_AI_SUB_METHODS +# --- Enabled check + display name lookup by payment method --- + +_KASSA_AI_METHOD_CONFIG = { + 'kassa_ai': { + 'is_enabled': settings.is_kassa_ai_enabled, + 'display_name': settings.get_kassa_ai_display_name, + 'unavailable_text': 'KassaAI временно недоступен', + }, + 'kassa_ai_sbp': { + 'is_enabled': settings.is_kassa_ai_sbp_enabled, + 'display_name': settings.get_kassa_ai_sbp_display_name, + 'unavailable_text': 'KassaAI СБП временно недоступен', + }, + 'kassa_ai_card': { + 'is_enabled': settings.is_kassa_ai_card_enabled, + 'display_name': settings.get_kassa_ai_card_display_name, + 'unavailable_text': 'KassaAI Карта временно недоступна', + }, +} + + +async def _check_topup_restriction(callback: types.CallbackQuery, db_user: User) -> bool: + """Check if user has topup restriction. Returns True if restricted (handler should abort).""" + if not getattr(db_user, 'restriction_topup', False): + return False + texts = get_texts(db_user.language) + 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 True async def _create_kassa_ai_payment_and_respond( @@ -29,17 +67,7 @@ async def _create_kassa_ai_payment_and_respond( edit_message: bool = False, payment_method: str = 'kassa_ai', ): - """ - Common logic for creating KassaAI payment and sending response. - - Args: - message_or_callback: Either a Message or CallbackQuery object - db_user: User object - db: Database session - amount_kopeks: Amount in kopeks - edit_message: Whether to edit existing message or send new one - payment_method: 'kassa_ai', 'kassa_ai_sbp', or 'kassa_ai_card' - """ + """Common logic for creating KassaAI payment and sending response.""" texts = get_texts(db_user.language) amount_rub = amount_kopeks / 100 @@ -83,12 +111,8 @@ async def _create_kassa_ai_payment_and_respond( return payment_url = result.get('payment_url') - if payment_method == 'kassa_ai_sbp': - display_name = settings.get_kassa_ai_sbp_display_name() - elif payment_method == 'kassa_ai_card': - display_name = settings.get_kassa_ai_card_display_name() - else: - display_name = settings.get_kassa_ai_display_name() + cfg = _KASSA_AI_METHOD_CONFIG.get(payment_method, _KASSA_AI_METHOD_CONFIG['kassa_ai']) + display_name = cfg['display_name']() # Create keyboard with payment button keyboard = InlineKeyboardMarkup( @@ -144,21 +168,9 @@ async def process_kassa_ai_payment_amount( state: FSMContext, payment_method: str = 'kassa_ai', ): - """ - Process payment amount directly (called from quick_amount handlers). - """ + """Process payment amount directly (called from custom_amount and quick_amount handlers).""" texts = get_texts(db_user.language) - # Проверка что sub-method включён - if payment_method == 'kassa_ai_sbp' and not settings.is_kassa_ai_sbp_enabled(): - await message.answer('Метод оплаты СБП через KassaAI временно недоступен.', parse_mode='HTML') - await state.clear() - return - if payment_method == 'kassa_ai_card' and not settings.is_kassa_ai_card_enabled(): - await message.answer('Метод оплаты картой через KassaAI временно недоступен.', parse_mode='HTML') - await state.clear() - return - # Проверка ограничения на пополнение if getattr(db_user, 'restriction_topup', False): reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' @@ -212,50 +224,36 @@ async def process_kassa_ai_payment_amount( ) -@error_handler -async def start_kassa_ai_topup( +# --- Generic start/quick-amount implementations --- + + +async def _start_kassa_ai_sub_topup( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, + payment_method: str, ): - """ - Start KassaAI top-up process - ask for amount. - """ + """Generic start topup handler for any KassaAI sub-method.""" + cfg = _KASSA_AI_METHOD_CONFIG[payment_method] texts = get_texts(db_user.language) - # Проверка ограничения на пополнение - if getattr(db_user, 'restriction_topup', False): - reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором' - support_url = settings.get_support_contact_url() - keyboard = [] - if support_url: - keyboard.append([InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)]) - keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')]) + if not cfg['is_enabled'](): + await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', cfg['unavailable_text']), show_alert=True) + return - await callback.message.edit_text( - f'🚫 Пополнение ограничено\n\n{reason}', - parse_mode='HTML', - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), - ) + if await _check_topup_restriction(callback, db_user): return await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data(payment_method='kassa_ai') + await state.update_data(payment_method=payment_method) min_amount = settings.KASSA_AI_MIN_AMOUNT_KOPEKS // 100 max_amount = settings.KASSA_AI_MAX_AMOUNT_KOPEKS // 100 - display_name = settings.get_kassa_ai_display_name() + display_name = cfg['display_name']() keyboard = InlineKeyboardMarkup( - inline_keyboard=[ - [ - InlineKeyboardButton( - text=texts.t('BACK_BUTTON', '◀️ Назад'), - callback_data='menu_balance', - ) - ] - ] + inline_keyboard=[[InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')]] ) await callback.message.edit_text( @@ -275,6 +273,68 @@ async def start_kassa_ai_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) --- + + +@error_handler +async def start_kassa_ai_topup( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + """Start KassaAI top-up process - ask for amount.""" + await _start_kassa_ai_sub_topup(callback, db_user, db, state, 'kassa_ai') + + @error_handler async def process_kassa_ai_custom_amount( message: types.Message, @@ -282,14 +342,10 @@ async def process_kassa_ai_custom_amount( db: AsyncSession, state: FSMContext, ): - """ - Process custom amount input for KassaAI payment. - """ + """Process custom amount input for KassaAI payment.""" data = await state.get_data() pm = data.get('payment_method', 'kassa_ai') - if pm not in ('kassa_ai', 'kassa_ai_sbp', 'kassa_ai_card'): - await state.clear() - await message.answer('Неизвестный метод оплаты. Попробуйте снова.', parse_mode='HTML') + if pm not in _KASSA_AI_METHOD_CONFIG: return texts = get_texts(db_user.language) @@ -325,75 +381,8 @@ async def process_kassa_ai_quick_amount( db: AsyncSession, state: FSMContext, ): - """ - Process quick amount selection for KassaAI payment. - Called when user clicks a predefined amount button. - """ - texts = get_texts(db_user.language) - - if not settings.is_kassa_ai_enabled(): - await callback.answer( - texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI временно недоступен'), - show_alert=True, - ) - return - - # Extract amount from callback data: topup_amount|kassa_ai|{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.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, - ) + """Process quick amount selection for KassaAI payment.""" + await _process_kassa_ai_sub_quick_amount(callback, db_user, db, state, 'kassa_ai') @error_handler @@ -404,45 +393,7 @@ async def start_kassa_ai_sbp_topup( state: FSMContext, ): """Start KassaAI SBP top-up process.""" - texts = get_texts(db_user.language) - - if not settings.is_kassa_ai_sbp_enabled(): - await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI СБП временно недоступен'), 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 - - await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data(payment_method='kassa_ai_sbp') - - min_amount = settings.KASSA_AI_MIN_AMOUNT_KOPEKS // 100 - max_amount = settings.KASSA_AI_MAX_AMOUNT_KOPEKS // 100 - display_name = settings.get_kassa_ai_sbp_display_name() - - keyboard = InlineKeyboardMarkup( - inline_keyboard=[[InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')]] - ) - - await callback.message.edit_text( - texts.t( - 'KASSA_AI_ENTER_AMOUNT', - '💳 Пополнение через {name}\n\nВведите сумму пополнения в рублях.\n\nМинимум: {min_amount}₽\nМаксимум: {max_amount}₽', - ).format(name=display_name, min_amount=min_amount, max_amount=f'{max_amount:,}'.replace(',', ' ')), - parse_mode='HTML', - reply_markup=keyboard, - ) + await _start_kassa_ai_sub_topup(callback, db_user, db, state, 'kassa_ai_sbp') @error_handler @@ -453,53 +404,7 @@ async def process_kassa_ai_sbp_quick_amount( state: FSMContext, ): """Process quick amount for KassaAI SBP.""" - texts = get_texts(db_user.language) - if not settings.is_kassa_ai_sbp_enabled(): - await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI СБП временно недоступен'), 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 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 - - 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='kassa_ai_sbp', - ) + await _process_kassa_ai_sub_quick_amount(callback, db_user, db, state, 'kassa_ai_sbp') @error_handler @@ -510,45 +415,7 @@ async def start_kassa_ai_card_topup( state: FSMContext, ): """Start KassaAI Card top-up process.""" - texts = get_texts(db_user.language) - - if not settings.is_kassa_ai_card_enabled(): - await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI Карта временно недоступна'), 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 - - await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data(payment_method='kassa_ai_card') - - min_amount = settings.KASSA_AI_MIN_AMOUNT_KOPEKS // 100 - max_amount = settings.KASSA_AI_MAX_AMOUNT_KOPEKS // 100 - display_name = settings.get_kassa_ai_card_display_name() - - keyboard = InlineKeyboardMarkup( - inline_keyboard=[[InlineKeyboardButton(text=texts.t('BACK_BUTTON', '◀️ Назад'), callback_data='menu_balance')]] - ) - - await callback.message.edit_text( - texts.t( - 'KASSA_AI_ENTER_AMOUNT', - '💳 Пополнение через {name}\n\nВведите сумму пополнения в рублях.\n\nМинимум: {min_amount}₽\nМаксимум: {max_amount}₽', - ).format(name=display_name, min_amount=min_amount, max_amount=f'{max_amount:,}'.replace(',', ' ')), - parse_mode='HTML', - reply_markup=keyboard, - ) + await _start_kassa_ai_sub_topup(callback, db_user, db, state, 'kassa_ai_card') @error_handler @@ -559,50 +426,4 @@ async def process_kassa_ai_card_quick_amount( state: FSMContext, ): """Process quick amount for KassaAI Card.""" - texts = get_texts(db_user.language) - if not settings.is_kassa_ai_card_enabled(): - await callback.answer(texts.t('KASSA_AI_NOT_AVAILABLE', 'KassaAI Карта временно недоступна'), 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 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 - - 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='kassa_ai_card', - ) + await _process_kassa_ai_sub_quick_amount(callback, db_user, db, state, 'kassa_ai_card')