From 17ac3da3c4da0f36015e338451a22a1a7a433524 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 4 May 2026 06:44:03 +0300 Subject: [PATCH] fix: AuraPay webhook signature + add SBP/Card payment method selection - Fix webhook signature: str(None) produced "None" (4 chars) instead of "" like PHP implode() does, causing all webhooks with custom_fields=null to fail signature verification - Add AURAPAY_SBP_ENABLED / AURAPAY_CARD_ENABLED env vars with display names, following Freekassa pattern for sub-method selection - Add aurapay_sbp / aurapay_card buttons in payment keyboard - Add start_aurapay_sbp_topup / start_aurapay_card_topup handlers - Route dispatch handles aurapay / aurapay_sbp / aurapay_card - payment_utils updated with SBP/Card availability checks - service parameter ("sbp"/"card") now passed through to AuraPay API --- app/config.py | 24 +++++++++++ app/handlers/balance/aurapay.py | 71 +++++++++++++++++++++++++++++---- app/handlers/balance/main.py | 6 ++- app/keyboards/inline.py | 30 +++++++++++++- app/services/aurapay_service.py | 5 ++- app/utils/payment_utils.py | 36 ++++++++++++++++- 6 files changed, 159 insertions(+), 13 deletions(-) diff --git a/app/config.py b/app/config.py index db025767..f589ed6c 100644 --- a/app/config.py +++ b/app/config.py @@ -681,6 +681,10 @@ class Settings(BaseSettings): AURAPAY_WEBHOOK_PATH: str = '/aurapay-webhook' AURAPAY_RETURN_URL: str | None = None AURAPAY_PAYMENT_LIFETIME_MINUTES: int = 60 + AURAPAY_SBP_ENABLED: bool = False + AURAPAY_SBP_DISPLAY_NAME: str = 'СБП (AuraPay)' + AURAPAY_CARD_ENABLED: bool = False + AURAPAY_CARD_DISPLAY_NAME: str = 'Карта (AuraPay)' MAIN_MENU_MODE: str = 'default' # 'default' | 'cabinet' # Стиль кнопок Cabinet: primary (синий), success (зелёный), danger (красный), '' (по умолчанию для каждой секции) @@ -2118,6 +2122,26 @@ class Settings(BaseSettings): def get_aurapay_display_name_html(self) -> str: return html.escape(self.get_aurapay_display_name()) + def is_aurapay_sbp_enabled(self) -> bool: + return self.AURAPAY_SBP_ENABLED and self.is_aurapay_enabled() + + def get_aurapay_sbp_display_name(self) -> str: + name = (self.AURAPAY_SBP_DISPLAY_NAME or '').strip() + return name or 'СБП (AuraPay)' + + def get_aurapay_sbp_display_name_html(self) -> str: + return html.escape(self.get_aurapay_sbp_display_name()) + + def is_aurapay_card_enabled(self) -> bool: + return self.AURAPAY_CARD_ENABLED and self.is_aurapay_enabled() + + def get_aurapay_card_display_name(self) -> str: + name = (self.AURAPAY_CARD_DISPLAY_NAME or '').strip() + return name or 'Карта (AuraPay)' + + def get_aurapay_card_display_name_html(self) -> str: + return html.escape(self.get_aurapay_card_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/aurapay.py b/app/handlers/balance/aurapay.py index 1947cab7..3a7e42c3 100644 --- a/app/handlers/balance/aurapay.py +++ b/app/handlers/balance/aurapay.py @@ -39,6 +39,7 @@ async def _create_aurapay_payment_and_respond( db: AsyncSession, amount_kopeks: int, edit_message: bool = False, + payment_method_type: str | None = None, ): """ Common logic for creating AuraPay payment and sending response. @@ -61,6 +62,7 @@ async def _create_aurapay_payment_and_respond( description=description, email=getattr(db_user, 'email', None), language=db_user.language, + payment_method_type=payment_method_type, ) if not result: @@ -179,6 +181,11 @@ async def process_aurapay_payment_amount( ) return + data = await state.get_data() + payment_method = data.get('payment_method', 'aurapay') + # aurapay_sbp → 'sbp', aurapay_card → 'card', aurapay → None + payment_method_type = _extract_service_type(payment_method) + await state.clear() await _create_aurapay_payment_and_respond( @@ -187,19 +194,30 @@ async def process_aurapay_payment_amount( db=db, amount_kopeks=amount_kopeks, edit_message=False, + payment_method_type=payment_method_type, ) -@error_handler -async def start_aurapay_topup( +AURAPAY_PAYMENT_METHODS = {'aurapay', 'aurapay_sbp', 'aurapay_card'} + +AURAPAY_SERVICE_MAP: dict[str, str | None] = { + 'aurapay': None, + 'aurapay_sbp': 'sbp', + 'aurapay_card': 'card', +} + + +def _extract_service_type(payment_method: str) -> str | None: + return AURAPAY_SERVICE_MAP.get(payment_method) + + +async def _start_aurapay_topup_impl( callback: types.CallbackQuery, db_user: User, - db: AsyncSession, state: FSMContext, + payment_method: str, ): - """ - Start AuraPay top-up process - ask for amount. - """ + """Common logic for starting AuraPay top-up (generic / SBP / card).""" texts = get_texts(db_user.language) restriction_kb = _check_topup_restriction(db_user, texts) @@ -213,11 +231,18 @@ async def start_aurapay_topup( return await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data(payment_method='aurapay') + await state.update_data(payment_method=payment_method) min_amount = settings.AURAPAY_MIN_AMOUNT_KOPEKS // 100 max_amount = settings.AURAPAY_MAX_AMOUNT_KOPEKS // 100 - display_name = settings.get_aurapay_display_name() + + # Choose display name based on sub-method + if payment_method == 'aurapay_sbp': + display_name = settings.get_aurapay_sbp_display_name() + elif payment_method == 'aurapay_card': + display_name = settings.get_aurapay_card_display_name() + else: + display_name = settings.get_aurapay_display_name() keyboard = InlineKeyboardMarkup( inline_keyboard=[ @@ -245,3 +270,33 @@ async def start_aurapay_topup( parse_mode='HTML', reply_markup=keyboard, ) + + +@error_handler +async def start_aurapay_topup( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + await _start_aurapay_topup_impl(callback, db_user, state, 'aurapay') + + +@error_handler +async def start_aurapay_sbp_topup( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + await _start_aurapay_topup_impl(callback, db_user, state, 'aurapay_sbp') + + +@error_handler +async def start_aurapay_card_topup( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +): + await _start_aurapay_topup_impl(callback, db_user, state, 'aurapay_card') diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 98a365c6..f931be07 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -170,7 +170,7 @@ async def route_payment_by_method( await process_overpay_payment_amount(message, db_user, db, amount_kopeks, state) return True - if payment_method == 'aurapay': + if payment_method in ('aurapay', 'aurapay_sbp', 'aurapay_card'): from .aurapay import process_aurapay_payment_amount async with AsyncSessionLocal() as db: @@ -768,9 +768,11 @@ def register_balance_handlers(dp: Dispatcher): dp.callback_query.register(start_overpay_topup, F.data == 'topup_overpay') - from .aurapay import start_aurapay_topup + from .aurapay import start_aurapay_card_topup, start_aurapay_sbp_topup, start_aurapay_topup dp.callback_query.register(start_aurapay_topup, F.data == 'topup_aurapay') + dp.callback_query.register(start_aurapay_sbp_topup, F.data == 'topup_aurapay_sbp') + dp.callback_query.register(start_aurapay_card_topup, F.data == 'topup_aurapay_card') from .mulenpay import check_mulenpay_payment_status diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 7b9a676b..040cc154 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1850,7 +1850,35 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LAN ) has_direct_payment_methods = True - if settings.is_aurapay_enabled(): + if settings.is_aurapay_sbp_enabled(): + sbp_name = settings.get_aurapay_sbp_display_name() + keyboard.append( + [ + InlineKeyboardButton( + text=texts.t('PAYMENT_AURAPAY_SBP', f'📱 {sbp_name}'), + callback_data=_build_callback('aurapay_sbp'), + ) + ] + ) + has_direct_payment_methods = True + + if settings.is_aurapay_card_enabled(): + card_name = settings.get_aurapay_card_display_name() + keyboard.append( + [ + InlineKeyboardButton( + text=texts.t('PAYMENT_AURAPAY_CARD', f'💳 {card_name}'), + callback_data=_build_callback('aurapay_card'), + ) + ] + ) + has_direct_payment_methods = True + + if ( + settings.is_aurapay_enabled() + and not settings.is_aurapay_sbp_enabled() + and not settings.is_aurapay_card_enabled() + ): aurapay_name = settings.get_aurapay_display_name() keyboard.append( [ diff --git a/app/services/aurapay_service.py b/app/services/aurapay_service.py index 2dde0457..372c887c 100644 --- a/app/services/aurapay_service.py +++ b/app/services/aurapay_service.py @@ -197,8 +197,11 @@ class AuraPayService: return False # Сортируем ключи по алфавиту и конкатенируем значения + # None → '' (PHP implode() converts null to empty string, not "None") sorted_keys = sorted(payload.keys()) - concatenated_values = ''.join(str(payload[key]) for key in sorted_keys) + concatenated_values = ''.join( + str(payload[key]) if payload[key] is not None else '' for key in sorted_keys + ) expected = hmac.new( self.secret_key.encode('utf-8'), diff --git a/app/utils/payment_utils.py b/app/utils/payment_utils.py index 5cabdcac..1b2e25ca 100644 --- a/app/utils/payment_utils.py +++ b/app/utils/payment_utils.py @@ -232,7 +232,35 @@ def get_available_payment_methods() -> list[dict[str, str]]: } ) - if settings.is_aurapay_enabled(): + if settings.is_aurapay_sbp_enabled(): + sbp_name = settings.get_aurapay_sbp_display_name() + methods.append( + { + 'id': 'aurapay_sbp', + 'name': sbp_name, + 'icon': '📱', + 'description': f'через {sbp_name}', + 'callback': 'topup_aurapay_sbp', + } + ) + + if settings.is_aurapay_card_enabled(): + card_name = settings.get_aurapay_card_display_name() + methods.append( + { + 'id': 'aurapay_card', + 'name': card_name, + 'icon': '💳', + 'description': f'через {card_name}', + 'callback': 'topup_aurapay_card', + } + ) + + if ( + settings.is_aurapay_enabled() + and not settings.is_aurapay_sbp_enabled() + and not settings.is_aurapay_card_enabled() + ): aurapay_name = settings.get_aurapay_display_name() methods.append( { @@ -381,6 +409,10 @@ def is_payment_method_available(method_id: str) -> bool: return settings.is_overpay_enabled() if method_id == 'aurapay': return settings.is_aurapay_enabled() + if method_id == 'aurapay_sbp': + return settings.is_aurapay_sbp_enabled() + if method_id == 'aurapay_card': + return settings.is_aurapay_card_enabled() if method_id == 'support': return settings.is_support_topup_enabled() return False @@ -409,6 +441,8 @@ def get_payment_method_status() -> dict[str, bool]: 'rollypay': settings.is_rollypay_enabled(), 'overpay': settings.is_overpay_enabled(), 'aurapay': settings.is_aurapay_enabled(), + 'aurapay_sbp': settings.is_aurapay_sbp_enabled(), + 'aurapay_card': settings.is_aurapay_card_enabled(), 'support': settings.is_support_topup_enabled(), }