diff --git a/app/database/crud/pal24.py b/app/database/crud/pal24.py
index 42a03015..a88c46b4 100644
--- a/app/database/crud/pal24.py
+++ b/app/database/crud/pal24.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
+from datetime import datetime
from typing import Any, Dict, Optional
from sqlalchemy import select, update
@@ -87,6 +88,7 @@ async def update_pal24_payment_status(
status: str,
is_active: Optional[bool] = None,
is_paid: Optional[bool] = None,
+ paid_at: Optional[datetime] = None,
payment_id: Optional[str] = None,
payment_status: Optional[str] = None,
payment_method: Optional[str] = None,
@@ -103,6 +105,8 @@ async def update_pal24_payment_status(
update_values["is_active"] = is_active
if is_paid is not None:
update_values["is_paid"] = is_paid
+ if paid_at is not None:
+ update_values["paid_at"] = paid_at
if payment_id is not None:
update_values["payment_id"] = payment_id
if payment_status is not None:
diff --git a/app/external/pal24_webhook.py b/app/external/pal24_webhook.py
index 28b87ea2..e997ff8e 100644
--- a/app/external/pal24_webhook.py
+++ b/app/external/pal24_webhook.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
+from concurrent.futures import TimeoutError as FuturesTimeoutError
import json
import logging
import threading
@@ -13,7 +14,7 @@ from flask import Flask, jsonify, request
from werkzeug.serving import make_server
from app.config import settings
-from app.database.database import get_db
+from app.database.database import AsyncSessionLocal
from app.services.pal24_service import Pal24Service, Pal24APIError
from app.services.payment_service import PaymentService
@@ -56,6 +57,8 @@ def create_pal24_flask_app(
logger.error("Pal24 webhook получен, но сервис не настроен")
return jsonify({"status": "error", "reason": "service_not_configured"}), 503
+ logger.debug("Получен Pal24 webhook: headers=%s", dict(request.headers))
+
payload = _normalize_payload()
if not payload:
logger.warning("Пустой Pal24 webhook")
@@ -68,15 +71,19 @@ def create_pal24_flask_app(
return jsonify({"status": "error", "reason": str(error)}), 400
async def process() -> bool:
- async for db in get_db():
+ async with AsyncSessionLocal() as db:
try:
return await payment_service.process_pal24_postback(db, parsed_payload)
- finally:
- await db.close()
+ except Exception:
+ await db.rollback()
+ raise
try:
future = asyncio.run_coroutine_threadsafe(process(), loop)
- processed = future.result()
+ processed = future.result(timeout=settings.PAL24_REQUEST_TIMEOUT)
+ except FuturesTimeoutError:
+ logger.error("Обработка Pal24 webhook превысила таймаут %sс", settings.PAL24_REQUEST_TIMEOUT)
+ return jsonify({"status": "error", "reason": "timeout"}), 504
except Exception as error: # pragma: no cover - defensive
logger.exception("Критическая ошибка обработки Pal24 webhook: %s", error)
return jsonify({"status": "error", "reason": "internal_error"}), 500
diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py
index 7c2cf2a0..b8ba775c 100644
--- a/app/handlers/balance/main.py
+++ b/app/handlers/balance/main.py
@@ -652,6 +652,11 @@ def register_balance_handlers(dp: Dispatcher):
start_pal24_payment,
F.data == "topup_pal24"
)
+ from .pal24 import handle_pal24_method_selection
+ dp.callback_query.register(
+ handle_pal24_method_selection,
+ F.data.startswith("pal24_method_"),
+ )
from .yookassa import check_yookassa_payment_status
dp.callback_query.register(
diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py
index 2c68120a..da713960 100644
--- a/app/handlers/balance/pal24.py
+++ b/app/handlers/balance/pal24.py
@@ -1,10 +1,12 @@
import html
import logging
from aiogram import types
+from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
+from app.database.database import AsyncSessionLocal
from app.database.models import User
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
@@ -15,81 +17,27 @@ from app.states import BalanceStates
logger = logging.getLogger(__name__)
-@error_handler
-async def start_pal24_payment(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
-
- if not settings.is_pal24_enabled():
- await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True)
- return
-
- # Формируем текст сообщения в зависимости от доступных способов оплаты
- if settings.is_pal24_sbp_button_visible() and settings.is_pal24_card_button_visible():
- payment_methods_text = "СБП и банковской картой"
- elif settings.is_pal24_sbp_button_visible():
- payment_methods_text = "СБП"
- elif settings.is_pal24_card_button_visible():
- payment_methods_text = "банковской картой"
- else:
- # Если обе кнопки отключены, используем общий текст
- payment_methods_text = "доступными способами"
-
- message_text = texts.t(
- "PAL24_TOPUP_PROMPT",
- (
- f"🏦 Оплата через PayPalych ({payment_methods_text})\n\n"
- "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n"
- f"Оплата проходит через PayPalych ({payment_methods_text})."
- ),
- )
-
- keyboard = get_back_keyboard(db_user.language)
-
- if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS:
- from .main import get_quick_amount_buttons
- quick_amount_buttons = get_quick_amount_buttons(db_user.language)
- 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)
- await state.update_data(payment_method="pal24")
- await callback.answer()
+def _get_available_pal24_methods() -> list[str]:
+ methods: list[str] = []
+ if settings.is_pal24_sbp_button_visible():
+ methods.append("sbp")
+ if settings.is_pal24_card_button_visible():
+ methods.append("card")
+ if not methods:
+ methods.append("sbp")
+ return methods
-@error_handler
-async def process_pal24_payment_amount(
+async def _send_pal24_payment_message(
message: types.Message,
db_user: User,
db: AsyncSession,
amount_kopeks: int,
+ payment_method: str,
state: FSMContext,
-):
+) -> None:
texts = get_texts(db_user.language)
- if not settings.is_pal24_enabled():
- await message.answer("❌ Оплата через PayPalych временно недоступна")
- return
-
- if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
- min_rubles = settings.PAL24_MIN_AMOUNT_KOPEKS / 100
- await message.answer(f"❌ Минимальная сумма для оплаты через PayPalych: {min_rubles:.0f} ₽")
- return
-
- if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
- max_rubles = settings.PAL24_MAX_AMOUNT_KOPEKS / 100
- await message.answer(f"❌ Максимальная сумма для оплаты через PayPalych: {max_rubles:,.0f} ₽".replace(',', ' '))
- return
-
try:
payment_service = PaymentService(message.bot)
payment_result = await payment_service.create_pal24_payment(
@@ -98,6 +46,7 @@ async def process_pal24_payment_amount(
amount_kopeks=amount_kopeks,
description=settings.get_balance_payment_description(amount_kopeks),
language=db_user.language,
+ payment_method=payment_method,
)
if not payment_result:
@@ -262,14 +211,15 @@ async def process_pal24_payment_amount(
await state.clear()
logger.info(
- "Создан PayPalych счет для пользователя %s: %s₽, ID: %s",
+ "Создан PayPalych счет для пользователя %s: %s₽, ID: %s, метод: %s",
db_user.telegram_id,
amount_kopeks / 100,
bill_id,
+ payment_method,
)
- except Exception as e:
- logger.error(f"Ошибка создания PayPalych платежа: {e}")
+ except Exception as error:
+ logger.error(f"Ошибка создания PayPalych платежа: {error}")
await message.answer(
texts.t(
"PAL24_PAYMENT_ERROR",
@@ -278,6 +228,168 @@ async def process_pal24_payment_amount(
)
await state.clear()
+@error_handler
+async def start_pal24_payment(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+):
+ texts = get_texts(db_user.language)
+
+ if not settings.is_pal24_enabled():
+ await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True)
+ return
+
+ # Формируем текст сообщения в зависимости от доступных способов оплаты
+ if settings.is_pal24_sbp_button_visible() and settings.is_pal24_card_button_visible():
+ payment_methods_text = "СБП и банковской картой"
+ elif settings.is_pal24_sbp_button_visible():
+ payment_methods_text = "СБП"
+ elif settings.is_pal24_card_button_visible():
+ payment_methods_text = "банковской картой"
+ else:
+ # Если обе кнопки отключены, используем общий текст
+ payment_methods_text = "доступными способами"
+
+ message_text = texts.t(
+ "PAL24_TOPUP_PROMPT",
+ (
+ f"🏦 Оплата через PayPalych ({payment_methods_text})\n\n"
+ "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n"
+ f"Оплата проходит через PayPalych ({payment_methods_text})."
+ ),
+ )
+
+ keyboard = get_back_keyboard(db_user.language)
+
+ if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS:
+ from .main import get_quick_amount_buttons
+ quick_amount_buttons = get_quick_amount_buttons(db_user.language)
+ 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)
+ await state.update_data(payment_method="pal24")
+ await callback.answer()
+
+
+@error_handler
+async def process_pal24_payment_amount(
+ message: types.Message,
+ db_user: User,
+ db: AsyncSession,
+ amount_kopeks: int,
+ state: FSMContext,
+):
+ texts = get_texts(db_user.language)
+
+ if not settings.is_pal24_enabled():
+ await message.answer("❌ Оплата через PayPalych временно недоступна")
+ return
+
+ if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
+ min_rubles = settings.PAL24_MIN_AMOUNT_KOPEKS / 100
+ await message.answer(f"❌ Минимальная сумма для оплаты через PayPalych: {min_rubles:.0f} ₽")
+ return
+
+ if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
+ max_rubles = settings.PAL24_MAX_AMOUNT_KOPEKS / 100
+ await message.answer(
+ f"❌ Максимальная сумма для оплаты через PayPalych: {max_rubles:,.0f} ₽".replace(',', ' ')
+ )
+ return
+
+ available_methods = _get_available_pal24_methods()
+
+ if len(available_methods) == 1:
+ await _send_pal24_payment_message(
+ message,
+ db_user,
+ db,
+ amount_kopeks,
+ available_methods[0],
+ state,
+ )
+ return
+
+ await state.update_data(pal24_amount_kopeks=amount_kopeks)
+ await state.set_state(BalanceStates.waiting_for_pal24_method)
+
+ method_buttons: list[list[types.InlineKeyboardButton]] = []
+ if "sbp" in available_methods:
+ method_buttons.append(
+ [
+ types.InlineKeyboardButton(
+ text=settings.get_pal24_sbp_button_text(
+ texts.t("PAL24_SBP_PAY_BUTTON", "🏦 Оплатить через PayPalych (СБП)")
+ ),
+ callback_data="pal24_method_sbp",
+ )
+ ]
+ )
+ if "card" in available_methods:
+ method_buttons.append(
+ [
+ types.InlineKeyboardButton(
+ text=settings.get_pal24_card_button_text(
+ texts.t("PAL24_CARD_PAY_BUTTON", "💳 Оплатить банковской картой (PayPalych)")
+ ),
+ callback_data="pal24_method_card",
+ )
+ ]
+ )
+
+ method_buttons.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")])
+
+ await message.answer(
+ texts.t(
+ "PAL24_SELECT_PAYMENT_METHOD",
+ "Выберите способ оплаты PayPalych:",
+ ),
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=method_buttons),
+ )
+
+
+@error_handler
+async def handle_pal24_method_selection(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+):
+ data = await state.get_data()
+ amount_kopeks = data.get("pal24_amount_kopeks")
+ if not amount_kopeks:
+ texts = get_texts(db_user.language)
+ await callback.answer(
+ texts.t(
+ "PAL24_PAYMENT_ERROR",
+ "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.",
+ ),
+ show_alert=True,
+ )
+ await state.clear()
+ return
+
+ method = "sbp" if callback.data.endswith("_sbp") else "card"
+
+ await callback.answer()
+
+ async with AsyncSessionLocal() as db:
+ await _send_pal24_payment_message(
+ callback.message,
+ db_user,
+ db,
+ int(amount_kopeks),
+ method,
+ state,
+ )
+
@error_handler
async def check_pal24_payment_status(
@@ -353,11 +465,17 @@ async def check_pal24_payment_status(
])
await callback.answer()
- await callback.message.edit_text(
- "\n".join(message_lines),
- reply_markup=keyboard,
- disable_web_page_preview=True,
- )
+ try:
+ await callback.message.edit_text(
+ "\n".join(message_lines),
+ reply_markup=keyboard,
+ disable_web_page_preview=True,
+ )
+ except TelegramBadRequest as error:
+ if "message is not modified" in str(error).lower():
+ await callback.answer(texts.t("CHECK_STATUS_NO_CHANGES", "Статус не изменился"))
+ else:
+ raise
except Exception as e:
logger.error(f"Ошибка проверки статуса PayPalych: {e}")
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index 975bf141..62cae4a9 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -758,6 +758,7 @@
"CHANNEL_SUBSCRIBE_REQUIRED_ALERT": "❌ You haven't joined the channel!",
"CHANNEL_SUBSCRIBE_THANKS": "✅ Thanks for subscribing",
"CHECK_STATUS_BUTTON": "📊 Check status",
+ "CHECK_STATUS_NO_CHANGES": "Status has not changed",
"CHOOSE_ANOTHER_DEVICE": "📱 Choose another device",
"CLOSED_TICKETS": "🟢 Closed",
"CLOSED_TICKETS_HEADER": "🟢 Closed tickets",
@@ -953,6 +954,7 @@
"PAL24_PAYMENT_INSTRUCTIONS": "🏦 PayPalych (SBP) payment\n\n💰 Amount: {amount}\n🆔 Invoice ID: {bill_id}\n\n📱 How to pay:\n1. Press ‘Pay with PayPalych (SBP)’\n2. Follow the system prompts\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
"PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
"PAL24_SBP_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
+ "PAL24_SELECT_PAYMENT_METHOD": "Choose a PayPalych payment method:",
"PAL24_TOPUP_PROMPT": "🏦 PayPalych (SBP) payment\n\nEnter an amount between 100 and 1,000,000 ₽.\nThe payment is processed via the PayPalych Faster Payments System.",
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Payment methods are temporarily unavailable",
"PAYMENT_CARD_MULENPAY": "💳 Bank card (Mulen Pay)",
@@ -1101,6 +1103,7 @@
"RESET_ALL_DEVICES_BUTTON": "🔄 Reset all devices",
"RESET_DEVICE_CONFIRM_BUTTON": "✅ Reset this device",
"RESET_TRAFFIC_BUTTON": "🔄 Reset traffic",
+ "NO_SAVED_SUBSCRIPTION_ORDER": "No pending subscription order was found.",
"RETURN_TO_SUBSCRIPTION_CHECKOUT": "⬅️ Return to subscription checkout",
"RULES_ACCEPT": "✅ I accept the rules",
"RULES_ACCEPTED_PROCESSING": "✅ Rules accepted! Completing registration...",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index a0f5f74b..a57c7c3d 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -758,6 +758,7 @@
"CHANNEL_SUBSCRIBE_REQUIRED_ALERT": "❌ Вы не подписались на канал!",
"CHANNEL_SUBSCRIBE_THANKS": "✅ Спасибо за подписку",
"CHECK_STATUS_BUTTON": "📊 Проверить статус",
+ "CHECK_STATUS_NO_CHANGES": "Статус не изменился",
"CHOOSE_ANOTHER_DEVICE": "📱 Выбрать другое устройство",
"CLOSED_TICKETS": "🟢 Закрытые",
"CLOSED_TICKETS_HEADER": "🟢 Закрытые тикеты",
@@ -953,6 +954,7 @@
"PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 Инструкция:\n1. Нажмите кнопку ‘Оплатить через PayPalych (СБП)’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
"PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
"PAL24_SBP_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
+ "PAL24_SELECT_PAYMENT_METHOD": "Выберите способ оплаты PayPalych:",
"PAL24_TOPUP_PROMPT": "🏦 Оплата через PayPalych (СБП)\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.",
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Способы оплаты временно недоступны",
"PAYMENT_CARD_MULENPAY": "💳 Банковская карта (Mulen Pay)",
@@ -1101,6 +1103,7 @@
"RESET_ALL_DEVICES_BUTTON": "🔄 Сбросить все устройства",
"RESET_DEVICE_CONFIRM_BUTTON": "✅ Да, сбросить это устройство",
"RESET_TRAFFIC_BUTTON": "🔄 Сбросить трафик",
+ "NO_SAVED_SUBSCRIPTION_ORDER": "❗️ Сохраненный заказ не найден.",
"RETURN_TO_SUBSCRIPTION_CHECKOUT": "⬅️ Вернуться к оформлению подписки",
"RULES_ACCEPT": "✅ Принимаю правила",
"RULES_ACCEPTED_PROCESSING": "✅ Правила приняты! Завершаем регистрацию...",
diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py
index 0fa786f1..2283aca2 100644
--- a/app/services/payment/pal24.py
+++ b/app/services/payment/pal24.py
@@ -63,7 +63,7 @@ class Pal24PaymentMixin:
"language": language,
}
- normalized_payment_method = (payment_method or "SBP").upper()
+ normalized_payment_method = self._normalize_payment_method(payment_method)
payment_module = import_module("app.services.payment_service")
@@ -147,6 +147,7 @@ class Pal24PaymentMixin:
"description": description,
"links": metadata_links,
"raw_response": response,
+ "selected_method": normalized_payment_method,
}
payment = await payment_module.create_pal24_payment(
@@ -186,6 +187,10 @@ class Pal24PaymentMixin:
"payment_method": normalized_payment_method,
"metadata_links": metadata_links,
"status": payment_status,
+ "sbp_url": transfer_url,
+ "transfer_url": transfer_url,
+ "link_page_url": link_page_url,
+ "payment_url": primary_link,
}
async def process_pal24_postback(
@@ -240,7 +245,7 @@ class Pal24PaymentMixin:
logger.info("Pal24 платеж %s уже обработан", payment.bill_id)
return True
- if status in {"PAID", "SUCCESS"}:
+ if status in {"PAID", "SUCCESS", "OVERPAID"}:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
@@ -253,8 +258,17 @@ class Pal24PaymentMixin:
db,
payment,
status=status,
+ is_paid=True,
+ paid_at=datetime.utcnow(),
postback_payload=postback,
payment_id=payment_id,
+ payment_status=postback.get("Status") or status,
+ payment_method=(
+ postback.get("payment_method")
+ or postback.get("PaymentMethod")
+ or (payment.metadata_json or {}).get("selected_method")
+ or getattr(payment, "payment_method", None)
+ ),
)
if payment.transaction_id:
@@ -407,8 +421,15 @@ class Pal24PaymentMixin:
db,
payment,
status=status or "UNKNOWN",
+ is_paid=False,
postback_payload=postback,
payment_id=payment_id,
+ payment_status=postback.get("Status") or status,
+ payment_method=(
+ postback.get("payment_method")
+ or postback.get("PaymentMethod")
+ or getattr(payment, "payment_method", None)
+ ),
)
logger.info(
"Обновили Pal24 платеж %s до статуса %s",
@@ -446,12 +467,29 @@ class Pal24PaymentMixin:
"bill", {}
).get("status")
- if remote_status and remote_status != payment.status:
- await payment_module.update_pal24_payment_status(
- db,
- payment,
- status=str(remote_status).upper(),
- )
+ if remote_status:
+ normalized_remote = str(remote_status).upper()
+ if normalized_remote != payment.status:
+ update_kwargs: Dict[str, Any] = {
+ "status": normalized_remote,
+ "payment_status": remote_status,
+ }
+ if normalized_remote in getattr(
+ service, "BILL_SUCCESS_STATES", {"SUCCESS"}
+ ):
+ update_kwargs["is_paid"] = True
+ if not payment.paid_at:
+ update_kwargs["paid_at"] = datetime.utcnow()
+ elif normalized_remote in getattr(
+ service, "BILL_FAILED_STATES", {"FAIL"}
+ ):
+ update_kwargs["is_paid"] = False
+
+ await payment_module.update_pal24_payment_status(
+ db,
+ payment,
+ **update_kwargs,
+ )
payment = await payment_module.get_pal24_payment_by_id(
db, local_payment_id
)
@@ -471,3 +509,22 @@ class Pal24PaymentMixin:
except Exception as error:
logger.error("Ошибка получения статуса Pal24: %s", error, exc_info=True)
return None
+
+ @staticmethod
+ def _normalize_payment_method(payment_method: Optional[str]) -> str:
+ mapping = {
+ "sbp": "sbp",
+ "fast": "sbp",
+ "fastpay": "sbp",
+ "fast_payment": "sbp",
+ "card": "card",
+ "bank_card": "card",
+ "bankcard": "card",
+ "bank-card": "card",
+ }
+
+ if not payment_method:
+ return "sbp"
+
+ normalized = payment_method.strip().lower()
+ return mapping.get(normalized, "sbp")
diff --git a/app/states.py b/app/states.py
index 7ab82d76..98343446 100644
--- a/app/states.py
+++ b/app/states.py
@@ -21,6 +21,7 @@ class SubscriptionStates(StatesGroup):
class BalanceStates(StatesGroup):
waiting_for_amount = State()
+ waiting_for_pal24_method = State()
waiting_for_stars_payment = State()
waiting_for_support_request = State()
diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py
index e690f522..85da7f3c 100644
--- a/app/webapi/routes/miniapp.py
+++ b/app/webapi/routes/miniapp.py
@@ -958,7 +958,7 @@ async def create_payment_link(
option = (payload.payment_option or "").strip().lower()
if option not in {"card", "sbp"}:
option = "sbp"
- provider_method = "CARD" if option == "card" else "SBP"
+ provider_method = "card" if option == "card" else "sbp"
payment_service = PaymentService()
result = await payment_service.create_pal24_payment(
@@ -974,7 +974,7 @@ async def create_payment_link(
preferred_urls: List[Optional[str]] = []
if option == "sbp":
- preferred_urls.append(result.get("sbp_url"))
+ preferred_urls.append(result.get("sbp_url") or result.get("transfer_url"))
elif option == "card":
preferred_urls.append(result.get("card_url"))
preferred_urls.extend(
@@ -998,7 +998,7 @@ async def create_payment_link(
"bill_id": result.get("bill_id"),
"order_id": result.get("order_id"),
"payment_method": result.get("payment_method") or provider_method,
- "sbp_url": result.get("sbp_url"),
+ "sbp_url": result.get("sbp_url") or result.get("transfer_url"),
"card_url": result.get("card_url"),
"link_url": result.get("link_url"),
"link_page_url": result.get("link_page_url"),
diff --git a/tests/services/test_pal24_service_adapter.py b/tests/services/test_pal24_service_adapter.py
index 714a0d1c..f371039a 100644
--- a/tests/services/test_pal24_service_adapter.py
+++ b/tests/services/test_pal24_service_adapter.py
@@ -70,7 +70,7 @@ async def test_create_bill_success(monkeypatch: pytest.MonkeyPatch) -> None:
ttl_seconds=600,
custom_payload={"extra": "value"},
payer_email="user@example.com",
- payment_method="CARD",
+ payment_method="card",
)
assert result["bill_id"] == "BILL42"
diff --git a/tests/services/test_payment_service_pal24.py b/tests/services/test_payment_service_pal24.py
index 2b5a0f81..0e588b53 100644
--- a/tests/services/test_payment_service_pal24.py
+++ b/tests/services/test_payment_service_pal24.py
@@ -101,7 +101,7 @@ async def test_create_pal24_payment_success(monkeypatch: pytest.MonkeyPatch) ->
assert result is not None
assert result["local_payment_id"] == 321
assert result["bill_id"] == "BILL-1"
- assert result["payment_method"] == "CARD"
+ assert result["payment_method"] == "card"
assert result["link_url"] == "https://pal24/sbp"
assert result["card_url"] == "https://pal24/card"
assert stub.calls and stub.calls[0]["amount_kopeks"] == 50000
diff --git a/tests/test_miniapp_payments.py b/tests/test_miniapp_payments.py
index 848b8225..fc30664a 100644
--- a/tests/test_miniapp_payments.py
+++ b/tests/test_miniapp_payments.py
@@ -86,8 +86,8 @@ async def test_create_payment_link_pal24_uses_selected_option(monkeypatch):
assert response.payment_url == 'https://card'
assert response.extra['selected_option'] == 'card'
- assert response.extra['payment_method'] == 'CARD'
- assert captured_calls and captured_calls[0]['payment_method'] == 'CARD'
+ assert response.extra['payment_method'] == 'card'
+ assert captured_calls and captured_calls[0]['payment_method'] == 'card'
@pytest.mark.anyio("asyncio")
@@ -265,7 +265,7 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
transaction_id=777,
bill_id='BILL99',
order_id='ORD99',
- payment_method='SBP',
+ payment_method='sbp',
)
class StubPal24Service:
@@ -297,7 +297,7 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
assert result.extra['local_payment_id'] == 321
assert result.extra['bill_id'] == 'BILL99'
assert result.extra['order_id'] == 'ORD99'
- assert result.extra['payment_method'] == 'SBP'
+ assert result.extra['payment_method'] == 'sbp'
assert result.extra['payload'] == 'pal24_payload'
assert result.extra['started_at'] == '2024-01-01T00:00:00Z'
assert result.extra['remote_status'] == 'PAID'