From b492437b54beac3ec9ef2c972fdac4cb81efd09a Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 15 Oct 2025 23:38:51 +0300 Subject: [PATCH] Fix Pal24 payment flows and webhook handling --- app/database/crud/pal24.py | 4 + app/external/pal24_webhook.py | 17 +- app/handlers/balance/main.py | 5 + app/handlers/balance/pal24.py | 266 +++++++++++++------ app/localization/locales/en.json | 3 + app/localization/locales/ru.json | 3 + app/services/payment/pal24.py | 73 ++++- app/states.py | 1 + app/webapi/routes/miniapp.py | 6 +- tests/services/test_pal24_service_adapter.py | 2 +- tests/services/test_payment_service_pal24.py | 2 +- tests/test_miniapp_payments.py | 8 +- 12 files changed, 294 insertions(+), 96 deletions(-) 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'