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
This commit is contained in:
Fringg
2026-05-04 06:44:03 +03:00
parent e85c40f8cd
commit 17ac3da3c4
6 changed files with 159 additions and 13 deletions
+24
View File
@@ -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()
+63 -8
View File
@@ -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')
+4 -2
View File
@@ -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
+29 -1
View File
@@ -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(
[
+4 -1
View File
@@ -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'),
+35 -1
View File
@@ -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(),
}