"""Mixin для интеграции с PayPalych (Pal24)."""
from __future__ import annotations
from datetime import datetime
from importlib import import_module
import uuid
from typing import Any, Dict, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.pal24_service import Pal24APIError
from app.services.subscription_auto_purchase_service import (
auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.user_utils import format_referrer_info
from app.utils.payment_logger import payment_logger as logger
class Pal24PaymentMixin:
"""Mixin с созданием счетов Pal24, обработкой callback и запросом статуса."""
async def create_pal24_payment(
self,
db: AsyncSession,
*,
user_id: int,
amount_kopeks: int,
description: str,
language: str,
ttl_seconds: Optional[int] = None,
payer_email: Optional[str] = None,
payment_method: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""Создаёт счёт в Pal24 и сохраняет локальную запись."""
service = getattr(self, "pal24_service", None)
if not service or not service.is_configured:
logger.error("Pal24 сервис не инициализирован")
return None
if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
logger.warning(
"Сумма Pal24 меньше минимальной: %s < %s",
amount_kopeks,
settings.PAL24_MIN_AMOUNT_KOPEKS,
)
return None
if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
logger.warning(
"Сумма Pal24 больше максимальной: %s > %s",
amount_kopeks,
settings.PAL24_MAX_AMOUNT_KOPEKS,
)
return None
order_id = f"pal24_{user_id}_{uuid.uuid4().hex}"
custom_payload = {
"user_id": user_id,
"amount_kopeks": amount_kopeks,
"language": language,
}
normalized_payment_method = self._normalize_payment_method(payment_method)
api_payment_method = self._map_api_payment_method(normalized_payment_method)
payment_module = import_module("app.services.payment_service")
try:
response = await service.create_bill(
amount_kopeks=amount_kopeks,
user_id=user_id,
order_id=order_id,
description=description,
ttl_seconds=ttl_seconds,
custom_payload=custom_payload,
payer_email=payer_email,
payment_method=api_payment_method,
)
except Pal24APIError as error:
logger.error("Ошибка Pal24 API при создании счета: %s", error)
return None
if not response.get("success", True):
logger.error("Pal24 вернул ошибку при создании счета: %s", response)
return None
bill_id = response.get("bill_id")
if not bill_id:
logger.error("Pal24 не вернул bill_id: %s", response)
return None
def _pick_url(*keys: str) -> Optional[str]:
for key in keys:
value = response.get(key)
if value:
return str(value)
return None
transfer_url = _pick_url(
"transfer_url",
"transferUrl",
"transfer_link",
"transferLink",
"transfer",
"sbp_url",
"sbpUrl",
"sbp_link",
"sbpLink",
)
card_url = _pick_url(
"link_url",
"linkUrl",
"link",
"card_url",
"cardUrl",
"card_link",
"cardLink",
"payment_url",
"paymentUrl",
"url",
)
link_page_url = _pick_url(
"link_page_url",
"linkPageUrl",
"page_url",
"pageUrl",
)
primary_link = transfer_url or link_page_url or card_url
secondary_link = link_page_url or card_url or transfer_url
metadata_links = {
key: value
for key, value in {
"sbp": transfer_url,
"card": card_url,
"page": link_page_url,
}.items()
if value
}
metadata_payload = {
"user_id": user_id,
"amount_kopeks": amount_kopeks,
"description": description,
"links": metadata_links,
"raw_response": response,
"selected_method": normalized_payment_method,
}
payment = await payment_module.create_pal24_payment(
db,
user_id=user_id,
bill_id=bill_id,
amount_kopeks=amount_kopeks,
description=description,
status=response.get("status", "NEW"),
type_=response.get("type", "normal"),
currency=response.get("currency", "RUB"),
link_url=transfer_url or card_url,
link_page_url=link_page_url or primary_link,
order_id=order_id,
ttl=ttl_seconds,
metadata=metadata_payload,
)
logger.info(
"Создан Pal24 счет %s для пользователя %s (%s₽)",
bill_id,
user_id,
amount_kopeks / 100,
)
payment_status = getattr(payment, "status", response.get("status", "NEW"))
return {
"local_payment_id": payment.id,
"bill_id": bill_id,
"order_id": order_id,
"amount_kopeks": amount_kopeks,
"primary_url": primary_link,
"secondary_url": secondary_link,
"link_url": transfer_url,
"card_url": card_url,
"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_callback(
self,
db: AsyncSession,
callback: Dict[str, Any],
) -> bool:
"""Обрабатывает callback от Pal24 и начисляет баланс при успехе."""
try:
payment_module = import_module("app.services.payment_service")
def _first_non_empty(*values: Optional[str]) -> Optional[str]:
for value in values:
if value:
return value
return None
payment_id = _first_non_empty(
callback.get("id"),
callback.get("TrsId"),
callback.get("TrsID"),
)
bill_id = _first_non_empty(
callback.get("bill_id"),
callback.get("billId"),
callback.get("BillId"),
callback.get("BillID"),
)
order_id = _first_non_empty(
callback.get("order_id"),
callback.get("orderId"),
callback.get("InvId"),
callback.get("InvID"),
)
status = (callback.get("status") or callback.get("Status") or "").upper()
if not bill_id and not order_id:
logger.error("Pal24 callback без идентификаторов: %s", callback)
return False
payment = None
if bill_id:
payment = await payment_module.get_pal24_payment_by_bill_id(db, bill_id)
if not payment and order_id:
payment = await payment_module.get_pal24_payment_by_order_id(db, order_id)
if not payment:
logger.error("Pal24 платеж не найден: %s / %s", bill_id, order_id)
return False
if payment.is_paid:
logger.info("Pal24 платеж %s уже обработан", payment.bill_id)
return True
if status in {"PAID", "SUCCESS", "OVERPAID"}:
metadata = getattr(payment, "metadata_json", {}) or {}
if not isinstance(metadata, dict):
metadata = {}
payment = await payment_module.update_pal24_payment_status(
db,
payment,
status=status,
is_paid=True,
paid_at=datetime.utcnow(),
callback_payload=callback,
payment_id=payment_id,
payment_status=callback.get("Status") or status,
payment_method=(
callback.get("payment_method")
or callback.get("PaymentMethod")
or metadata.get("selected_method")
or getattr(payment, "payment_method", None)
),
balance_amount=callback.get("BalanceAmount")
or callback.get("balance_amount"),
balance_currency=callback.get("BalanceCurrency")
or callback.get("balance_currency"),
payer_account=callback.get("AccountNumber")
or callback.get("account")
or callback.get("Account"),
)
return await self._finalize_pal24_payment(
db,
payment,
payment_id=payment_id,
trigger="callback",
)
metadata = getattr(payment, "metadata_json", {}) or {}
if not isinstance(metadata, dict):
metadata = {}
await payment_module.update_pal24_payment_status(
db,
payment,
status=status or "UNKNOWN",
is_paid=False,
callback_payload=callback,
payment_id=payment_id,
payment_status=callback.get("Status") or status,
payment_method=(
callback.get("payment_method")
or callback.get("PaymentMethod")
or getattr(payment, "payment_method", None)
),
balance_amount=callback.get("BalanceAmount")
or callback.get("balance_amount"),
balance_currency=callback.get("BalanceCurrency")
or callback.get("balance_currency"),
payer_account=callback.get("AccountNumber")
or callback.get("account")
or callback.get("Account"),
)
logger.info(
"Обновили Pal24 платеж %s до статуса %s",
payment.bill_id,
status,
)
return True
except Exception as error:
logger.error("Ошибка обработки Pal24 callback: %s", error, exc_info=True)
return False
async def _finalize_pal24_payment(
self,
db: AsyncSession,
payment: Any,
*,
payment_id: Optional[str],
trigger: str,
) -> bool:
"""Создаёт транзакцию, начисляет баланс и отправляет уведомления."""
payment_module = import_module("app.services.payment_service")
metadata = dict(getattr(payment, "metadata_json", {}) or {})
invoice_message = metadata.get("invoice_message") or {}
invoice_message_removed = False
if getattr(self, "bot", None) and invoice_message:
chat_id = invoice_message.get("chat_id")
message_id = invoice_message.get("message_id")
if chat_id and message_id:
try:
await self.bot.delete_message(chat_id, message_id)
except Exception as delete_error: # pragma: no cover - depends on rights
logger.warning(
"Не удалось удалить счёт PayPalych %s: %s",
message_id,
delete_error,
)
else:
metadata.pop("invoice_message", None)
invoice_message_removed = True
if invoice_message_removed:
try:
await payment_module.update_pal24_payment_status(
db,
payment,
status=payment.status,
metadata=metadata,
)
payment.metadata_json = metadata
except Exception as error: # pragma: no cover - diagnostics
logger.warning(
"Не удалось обновить метаданные PayPalych после удаления счёта: %s",
error,
)
if payment.transaction_id:
logger.info(
"Pal24 платеж %s уже привязан к транзакции (trigger=%s)",
payment.bill_id,
trigger,
)
return True
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
"Пользователь %s не найден для Pal24 платежа %s (trigger=%s)",
payment.user_id,
payment.bill_id,
trigger,
)
return False
transaction = await payment_module.create_transaction(
db,
user_id=payment.user_id,
type=TransactionType.DEPOSIT,
amount_kopeks=payment.amount_kopeks,
description=f"Пополнение через Pal24 ({payment_id or payment.bill_id})",
payment_method=PaymentMethod.PAL24,
external_id=str(payment_id) if payment_id else payment.bill_id,
is_completed=True,
)
await payment_module.link_pal24_payment_to_transaction(db, payment, transaction.id)
old_balance = user.balance_kopeks
was_first_topup = not user.has_made_first_topup
user.balance_kopeks += payment.amount_kopeks
user.updated_at = datetime.utcnow()
promo_group = user.get_primary_promo_group()
subscription = getattr(user, "subscription", None)
referrer_info = format_referrer_info(user)
topup_status = "🆕 Первое пополнение" if was_first_topup else "🔄 Пополнение"
await db.commit()
try:
from app.services.referral_service import process_referral_topup
await process_referral_topup(
db, user.id, payment.amount_kopeks, getattr(self, "bot", None)
)
except Exception as error:
logger.error(
"Ошибка обработки реферального пополнения Pal24: %s",
error,
)
if was_first_topup and not user.has_made_first_topup:
user.has_made_first_topup = True
await db.commit()
await db.refresh(user)
await db.refresh(payment)
if getattr(self, "bot", None):
try:
from app.services.admin_notification_service import (
AdminNotificationService,
)
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
user,
transaction,
old_balance,
topup_status=topup_status,
referrer_info=referrer_info,
subscription=subscription,
promo_group=promo_group,
db=db,
)
except Exception as error:
logger.error(
"Ошибка отправки админ уведомления Pal24: %s",
error,
)
if getattr(self, "bot", None):
try:
keyboard = await self.build_topup_success_keyboard(user)
await self.bot.send_message(
user.telegram_id,
(
"✅ Пополнение успешно!\n\n"
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
"🦊 Способ: PayPalych\n"
f"🆔 Транзакция: {transaction.id}\n\n"
"Баланс пополнен автоматически!"
),
parse_mode="HTML",
reply_markup=keyboard,
)
except Exception as error:
logger.error(
"Ошибка отправки уведомления пользователю Pal24: %s",
error,
)
try:
from app.services.user_cart_service import user_cart_service
from aiogram import types
has_saved_cart = await user_cart_service.has_user_cart(user.id)
auto_purchase_success = False
if has_saved_cart:
try:
auto_purchase_success = await auto_purchase_saved_cart_after_topup(
db,
user,
bot=getattr(self, "bot", None),
)
except Exception as auto_error:
logger.error(
"Ошибка автоматической покупки подписки для пользователя %s: %s",
user.id,
auto_error,
exc_info=True,
)
if auto_purchase_success:
has_saved_cart = False
# Умная автоактивация если автопокупка не сработала
activation_notification_sent = False
if not auto_purchase_success:
try:
_, activation_notification_sent = await auto_activate_subscription_after_topup(
db, user, bot=getattr(self, "bot", None), topup_amount=payment.amount_kopeks
)
except Exception as auto_activate_error:
logger.error(
"Ошибка умной автоактивации для пользователя %s: %s",
user.id,
auto_activate_error,
exc_info=True,
)
# Отправляем уведомление только если его ещё не отправили
if has_saved_cart and getattr(self, "bot", None) and not activation_notification_sent:
from app.localization.texts import get_texts
texts = get_texts(user.language)
cart_message = texts.t(
"BALANCE_TOPUP_CART_REMINDER",
"У вас есть незавершенное оформление подписки. Вернуться?",
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t(
"BALANCE_TOPUP_CART_BUTTON",
"🛒 Продолжить оформление",
),
callback_data="return_to_saved_cart",
)
],
[
types.InlineKeyboardButton(
text="🏠 Главное меню",
callback_data="back_to_menu",
)
],
]
)
await self.bot.send_message(
chat_id=user.telegram_id,
text=(
"✅ Баланс пополнен на "
f"{settings.format_price(payment.amount_kopeks)}!\n\n"
f"⚠️ Важно: Пополнение баланса не активирует подписку автоматически. "
f"Обязательно активируйте подписку отдельно!\n\n"
f"🔄 При наличии сохранённой корзины подписки и включенной автопокупке, "
f"подписка будет приобретена автоматически после пополнения баланса.\n\n{cart_message}"
),
reply_markup=keyboard,
)
logger.info(
"Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю %s",
user.id,
)
else:
logger.info(
"У пользователя %s нет сохраненной корзины или автопокупка выполнена",
user.id,
)
except Exception as error:
logger.error(
"Ошибка при работе с сохраненной корзиной для пользователя %s: %s",
user.id,
error,
exc_info=True,
)
logger.info(
"✅ Обработан Pal24 платеж %s для пользователя %s (trigger=%s)",
payment.bill_id,
payment.user_id,
trigger,
)
return True
async def get_pal24_payment_status(
self,
db: AsyncSession,
local_payment_id: int,
) -> Optional[Dict[str, Any]]:
"""Запрашивает актуальный статус платежа у Pal24 и синхронизирует локальную запись."""
try:
payment_module = import_module("app.services.payment_service")
payment = await payment_module.get_pal24_payment_by_id(db, local_payment_id)
if not payment:
return None
remote_status: Optional[str] = None
remote_payloads: Dict[str, Any] = {}
payment_info_candidates: List[Dict[str, Optional[str]]] = []
service = getattr(self, "pal24_service", None)
if service and payment.bill_id:
bill_id_str = str(payment.bill_id)
try:
response = await service.get_bill_status(bill_id_str)
except Pal24APIError as error:
logger.error("Ошибка Pal24 API при получении статуса счёта: %s", error)
else:
if response:
remote_payloads["bill_status"] = response
status_value = response.get("status") or (response.get("bill") or {}).get("status")
if status_value:
remote_status = str(status_value).upper()
extracted = self._extract_remote_payment_info(response)
if extracted:
payment_info_candidates.append(extracted)
if payment.payment_id:
payment_id_str = str(payment.payment_id)
try:
payment_response = await service.get_payment_status(payment_id_str)
except Pal24APIError as error:
logger.error("Ошибка Pal24 API при получении статуса платежа: %s", error)
else:
if payment_response:
remote_payloads["payment_status"] = payment_response
extracted = self._extract_remote_payment_info(payment_response)
if extracted:
payment_info_candidates.append(extracted)
try:
payments_response = await service.get_bill_payments(bill_id_str)
except Pal24APIError as error:
logger.error("Ошибка Pal24 API при получении списка платежей: %s", error)
else:
if payments_response:
remote_payloads["bill_payments"] = payments_response
for candidate in self._collect_payment_candidates(payments_response):
extracted = self._extract_remote_payment_info(candidate)
if extracted:
payment_info_candidates.append(extracted)
payment_info = self._select_best_payment_info(payment, payment_info_candidates)
if payment_info:
remote_payloads.setdefault("selected_payment", payment_info)
bill_success = getattr(service, "BILL_SUCCESS_STATES", {"SUCCESS"}) if service else {"SUCCESS"}
bill_failed = getattr(service, "BILL_FAILED_STATES", {"FAIL"}) if service else {"FAIL"}
bill_pending = getattr(service, "BILL_PENDING_STATES", {"NEW", "PROCESS"}) if service else {"NEW", "PROCESS"}
update_status = payment.status or "NEW"
update_kwargs: Dict[str, Any] = {}
is_paid_update: Optional[bool] = None
if remote_status:
update_status = remote_status
if remote_status in bill_success:
is_paid_update = True
elif remote_status in bill_failed:
is_paid_update = False
elif remote_status in bill_pending and is_paid_update is None:
is_paid_update = False
payment_status_code: Optional[str] = None
if payment_info:
payment_status_code = (payment_info.get("status") or "").upper() or None
if payment_status_code:
existing_status = (getattr(payment, "payment_status", "") or "").upper()
if payment_status_code != existing_status:
update_kwargs["payment_status"] = payment_status_code
payment_id_value = payment_info.get("id")
if payment_id_value and payment_id_value != (payment.payment_id or ""):
update_kwargs["payment_id"] = payment_id_value
method_value = payment_info.get("method")
if method_value:
normalized_method = self._normalize_payment_method(method_value)
if normalized_method != (payment.payment_method or ""):
update_kwargs["payment_method"] = normalized_method
balance_amount = payment_info.get("balance_amount")
if balance_amount and balance_amount != (payment.balance_amount or ""):
update_kwargs["balance_amount"] = balance_amount
balance_currency = payment_info.get("balance_currency")
if balance_currency and balance_currency != (payment.balance_currency or ""):
update_kwargs["balance_currency"] = balance_currency
payer_account = payment_info.get("account")
if payer_account and payer_account != (payment.payer_account or ""):
update_kwargs["payer_account"] = payer_account
if payment_status_code:
success_states = {"SUCCESS", "OVERPAID"}
failed_states = {"FAIL"}
pending_states = {"NEW", "PROCESS", "UNDERPAID"}
if payment_status_code in success_states:
is_paid_update = True
elif payment_status_code in failed_states and is_paid_update is not True:
is_paid_update = False
elif payment_status_code in pending_states and is_paid_update is None:
is_paid_update = False
if not remote_status and payment_status_code:
update_status = payment_status_code
if is_paid_update is not None and is_paid_update != bool(payment.is_paid):
update_kwargs["is_paid"] = is_paid_update
if is_paid_update and not payment.paid_at:
update_kwargs.setdefault("paid_at", datetime.utcnow())
current_status = payment.status or ""
effective_status = update_status or current_status or "NEW"
needs_update = bool(update_kwargs) or effective_status != current_status
if needs_update:
payment = await payment_module.update_pal24_payment_status(
db,
payment,
status=effective_status,
**update_kwargs,
)
remote_status_for_return = remote_status or payment_status_code
remote_data = remote_payloads or None
if payment.is_paid and not payment.transaction_id:
try:
finalized = await self._finalize_pal24_payment(
db,
payment,
payment_id=getattr(payment, "payment_id", None),
trigger="status_check",
)
if finalized:
payment = await payment_module.get_pal24_payment_by_id(db, local_payment_id)
except Exception as error:
logger.error(
"Ошибка автоматического начисления по Pal24 статусу: %s",
error,
exc_info=True,
)
links_map, selected_method = self._build_links_map(payment, remote_payloads)
primary_url = (
links_map.get(selected_method)
or links_map.get("sbp")
or links_map.get("page")
or links_map.get("card")
)
secondary_url = (
links_map.get("page")
or links_map.get("card")
or links_map.get("sbp")
)
return {
"payment": payment,
"status": payment.status,
"is_paid": payment.is_paid,
"remote_status": remote_status_for_return,
"remote_data": remote_data,
"links": links_map or None,
"primary_url": primary_url,
"secondary_url": secondary_url,
"sbp_url": links_map.get("sbp"),
"card_url": links_map.get("card"),
"link_page_url": links_map.get("page")
or getattr(payment, "link_page_url", None),
"link_url": getattr(payment, "link_url", None),
"selected_method": selected_method,
}
except Exception as error:
logger.error("Ошибка получения статуса Pal24: %s", error, exc_info=True)
return None
@staticmethod
def _extract_remote_payment_info(remote_data: Any) -> Dict[str, Optional[str]]:
"""Извлекает данные о платеже из ответа Pal24."""
def _pick_candidate(value: Any) -> Optional[Dict[str, Any]]:
if isinstance(value, dict):
return value
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
return item
return None
def _normalize(candidate: Dict[str, Any]) -> Dict[str, Optional[str]]:
def _stringify(value: Any) -> Optional[str]:
if value is None:
return None
return str(value)
return {
"id": _stringify(candidate.get("id") or candidate.get("payment_id")),
"status": _stringify(candidate.get("status")),
"method": _stringify(candidate.get("method") or candidate.get("payment_method")),
"balance_amount": _stringify(
candidate.get("balance_amount")
or candidate.get("amount")
or candidate.get("BalanceAmount")
),
"balance_currency": _stringify(
candidate.get("balance_currency") or candidate.get("BalanceCurrency")
),
"account": _stringify(
candidate.get("account")
or candidate.get("payer_account")
or candidate.get("AccountNumber")
),
"bill_id": _stringify(
candidate.get("bill_id")
or candidate.get("BillId")
or candidate.get("billId")
),
}
if not isinstance(remote_data, dict):
return {}
lower_keys = {str(key).lower() for key in remote_data.keys()}
has_status = any(key in lower_keys for key in ("status", "payment_status"))
has_identifier = any(
key in lower_keys
for key in ("payment_id", "from_card", "account_amount", "id")
) or "bill_id" in lower_keys
if has_status and has_identifier and "bill" not in lower_keys:
return _normalize(remote_data)
search_spaces = [remote_data]
bill_section = remote_data.get("bill") or remote_data.get("Bill")
if isinstance(bill_section, dict):
search_spaces.append(bill_section)
for space in search_spaces:
for key in ("payment", "Payment", "payment_info", "PaymentInfo"):
candidate = _pick_candidate(space.get(key))
if candidate:
return _normalize(candidate)
for key in ("payments", "Payments"):
candidate = _pick_candidate(space.get(key))
if candidate:
return _normalize(candidate)
data_section = remote_data.get("data") or remote_data.get("Data")
candidate = _pick_candidate(data_section)
if candidate:
return _normalize(candidate)
return {}
@staticmethod
def _collect_payment_candidates(remote_data: Any) -> List[Dict[str, Any]]:
candidates: List[Dict[str, Any]] = []
def _visit(value: Any) -> None:
if isinstance(value, dict):
lower_keys = {str(key).lower() for key in value.keys()}
has_status = any(key in lower_keys for key in ("status", "payment_status"))
has_identifier = any(
key in lower_keys
for key in ("id", "payment_id", "bill_id", "from_card", "account_amount")
)
if has_status and has_identifier and value not in candidates:
candidates.append(value)
for nested in value.values():
_visit(nested)
elif isinstance(value, list):
for item in value:
_visit(item)
_visit(remote_data)
return candidates
@staticmethod
def _select_best_payment_info(
payment: Any,
candidates: List[Dict[str, Optional[str]]],
) -> Dict[str, Optional[str]]:
if not candidates:
return {}
payment_id = str(getattr(payment, "payment_id", "") or "")
bill_id = str(getattr(payment, "bill_id", "") or "")
for candidate in candidates:
candidate_id = str(candidate.get("id") or "")
if payment_id and candidate_id == payment_id:
return candidate
for candidate in candidates:
candidate_bill = str(candidate.get("bill_id") or "")
if bill_id and candidate_bill == bill_id:
return candidate
return candidates[0]
@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")
@staticmethod
def _pick_first(mapping: Dict[str, Any], *keys: str) -> Optional[str]:
for key in keys:
value = mapping.get(key)
if value:
return str(value)
return None
@classmethod
def _build_links_map(
cls,
payment: Any,
remote_payloads: Dict[str, Any],
) -> tuple[Dict[str, str], str]:
links: Dict[str, str] = {}
metadata = getattr(payment, "metadata_json", {}) or {}
if not isinstance(metadata, dict):
metadata = {}
if metadata:
links_meta = metadata.get("links")
if isinstance(links_meta, dict):
for key, value in links_meta.items():
if value:
links[key] = str(value)
selected_method = cls._normalize_payment_method(
(metadata.get("selected_method") if isinstance(metadata, dict) else None)
or getattr(payment, "payment_method", None)
)
def _visit(value: Any) -> List[Dict[str, Any]]:
stack: List[Any] = [value]
result: List[Dict[str, Any]] = []
while stack:
current = stack.pop()
if isinstance(current, dict):
result.append(current)
stack.extend(current.values())
elif isinstance(current, list):
stack.extend(current)
return result
payload_sources: List[Any] = []
if metadata:
payload_sources.append(metadata.get("raw_response"))
payload_sources.append(getattr(payment, "callback_payload", None))
payload_sources.extend(remote_payloads.values())
sbp_keys = (
"transfer_url",
"transferUrl",
"transfer_link",
"transferLink",
"transfer",
"sbp_url",
"sbpUrl",
"sbp_link",
"sbpLink",
)
card_keys = (
"link_url",
"linkUrl",
"link",
"card_url",
"cardUrl",
"card_link",
"cardLink",
"payment_url",
"paymentUrl",
"url",
)
page_keys = (
"link_page_url",
"linkPageUrl",
"page_url",
"pageUrl",
)
for source in payload_sources:
if not source:
continue
for candidate in _visit(source):
sbp_url = cls._pick_first(candidate, *sbp_keys)
if sbp_url and "sbp" not in links:
links["sbp"] = sbp_url
card_url = cls._pick_first(candidate, *card_keys)
if card_url and "card" not in links:
links["card"] = card_url
page_url = cls._pick_first(candidate, *page_keys)
if page_url and "page" not in links:
links["page"] = page_url
if getattr(payment, "link_page_url", None):
links.setdefault("page", str(payment.link_page_url))
if getattr(payment, "link_url", None):
link_url_value = str(payment.link_url)
if selected_method == "card":
links.setdefault("card", link_url_value)
else:
links.setdefault("sbp", link_url_value)
return links, selected_method
@staticmethod
def _map_api_payment_method(normalized_payment_method: str) -> Optional[str]:
"""Преобразует нормализованный метод оплаты в значение для Pal24 API."""
api_mapping = {
"sbp": "SBP",
"card": "BANK_CARD",
}
return api_mapping.get(normalized_payment_method)