Fix Pal24 payment flows and webhook handling

This commit is contained in:
Egor
2025-10-15 23:38:51 +03:00
parent 07546da0fd
commit b492437b54
12 changed files with 294 additions and 96 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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(

View File

@@ -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"🏦 <b>Оплата через PayPalych ({payment_methods_text})</b>\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"🏦 <b>Оплата через PayPalych ({payment_methods_text})</b>\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}")

View File

@@ -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": "🏦 <b>PayPalych (SBP) payment</b>\n\n💰 Amount: {amount}\n🆔 Invoice ID: {bill_id}\n\n📱 <b>How to pay:</b>\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": "🏦 <b>PayPalych (SBP) payment</b>\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...",

View File

@@ -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": "🏦 <b>Оплата через PayPalych (СБП)</b>\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 <b>Инструкция:</b>\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": "🏦 <b>Оплата через PayPalych (СБП)</b>\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": "✅ Правила приняты! Завершаем регистрацию...",

View File

@@ -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")

View File

@@ -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()

View File

@@ -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"),

View File

@@ -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"

View File

@@ -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

View File

@@ -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'