diff --git a/app/config.py b/app/config.py
index 286434d3..a2cbaf80 100644
--- a/app/config.py
+++ b/app/config.py
@@ -208,6 +208,8 @@ class Settings(BaseSettings):
PAL24_MIN_AMOUNT_KOPEKS: int = 10000
PAL24_MAX_AMOUNT_KOPEKS: int = 100000000
PAL24_REQUEST_TIMEOUT: int = 30
+ PAL24_SBP_BUTTON_TEXT: Optional[str] = None
+ PAL24_CARD_BUTTON_TEXT: Optional[str] = None
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
@@ -399,6 +401,14 @@ class Settings(BaseSettings):
"password": self.REMNAWAVE_PASSWORD,
"auth_type": self.REMNAWAVE_AUTH_TYPE
}
+
+ def get_pal24_sbp_button_text(self, fallback: str) -> str:
+ value = (self.PAL24_SBP_BUTTON_TEXT or "").strip()
+ return value or fallback
+
+ def get_pal24_card_button_text(self, fallback: str) -> str:
+ value = (self.PAL24_CARD_BUTTON_TEXT or "").strip()
+ return value or fallback
def get_remnawave_user_delete_mode(self) -> str:
"""Возвращает режим удаления пользователей: 'delete' или 'disable'"""
diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py
index 6e0fadfc..b129ea68 100644
--- a/app/handlers/admin/bot_configuration.py
+++ b/app/handlers/admin/bot_configuration.py
@@ -851,33 +851,79 @@ async def test_payment_provider(
language=language or "ru",
)
- if not payment_result or not payment_result.get("link_url") and not payment_result.get("link_page_url"):
+ if not payment_result:
await callback.answer("❌ Не удалось создать платеж PayPalych", show_alert=True)
await _refresh_markup()
return
- payment_url = payment_result.get("link_page_url") or payment_result.get("link_url")
+ sbp_url = (
+ payment_result.get("sbp_url")
+ or payment_result.get("transfer_url")
+ or payment_result.get("link_url")
+ )
+ card_url = payment_result.get("card_url")
+ fallback_url = payment_result.get("link_page_url") or payment_result.get("link_url")
+
+ if not (sbp_url or card_url or fallback_url):
+ await callback.answer("❌ Не удалось создать платеж PayPalych", show_alert=True)
+ await _refresh_markup()
+ return
+
+ if not sbp_url:
+ sbp_url = fallback_url
+
+ default_sbp_text = texts.t(
+ "PAL24_SBP_PAY_BUTTON",
+ "🏦 Оплатить через PayPalych (СБП)",
+ )
+ sbp_button_text = settings.get_pal24_sbp_button_text(default_sbp_text)
+
+ default_card_text = texts.t(
+ "PAL24_CARD_PAY_BUTTON",
+ "💳 Оплатить банковской картой (PayPalych)",
+ )
+ card_button_text = settings.get_pal24_card_button_text(default_card_text)
+
+ pay_rows: list[list[types.InlineKeyboardButton]] = []
+ if sbp_url:
+ pay_rows.append([
+ types.InlineKeyboardButton(
+ text=sbp_button_text,
+ url=sbp_url,
+ )
+ ])
+
+ if card_url and card_url != sbp_url:
+ pay_rows.append([
+ types.InlineKeyboardButton(
+ text=card_button_text,
+ url=card_url,
+ )
+ ])
+
+ if not pay_rows and fallback_url:
+ pay_rows.append([
+ types.InlineKeyboardButton(
+ text=sbp_button_text,
+ url=fallback_url,
+ )
+ ])
+
message_text = (
"🧪 Тестовый платеж PayPalych\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"🆔 Bill ID: {payment_result['bill_id']}"
)
- reply_markup = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text="🏦 Перейти к оплате (СБП)",
- url=payment_url,
- )
- ],
- [
- types.InlineKeyboardButton(
- text="📊 Проверить статус",
- callback_data=f"check_pal24_{payment_result['local_payment_id']}",
- )
- ],
- ]
- )
+ keyboard_rows = pay_rows + [
+ [
+ types.InlineKeyboardButton(
+ text="📊 Проверить статус",
+ callback_data=f"check_pal24_{payment_result['local_payment_id']}",
+ )
+ ],
+ ]
+
+ reply_markup = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
await callback.message.answer(message_text, reply_markup=reply_markup, parse_mode="HTML")
await callback.answer("✅ Ссылка на платеж PayPalych отправлена", show_alert=True)
await _refresh_markup()
diff --git a/app/handlers/balance.py b/app/handlers/balance.py
index e6f0b832..adcabebd 100644
--- a/app/handlers/balance.py
+++ b/app/handlers/balance.py
@@ -1,3 +1,4 @@
+import html
import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
@@ -1002,9 +1003,7 @@ async def process_pal24_payment_amount(
language=db_user.language,
)
- if not payment_result or not (
- payment_result.get("link_url") or payment_result.get("link_page_url")
- ):
+ if not payment_result:
await message.answer(
texts.t(
"PAL24_PAYMENT_ERROR",
@@ -1014,49 +1013,146 @@ async def process_pal24_payment_amount(
await state.clear()
return
- payment_url = (
+ sbp_url = (
+ payment_result.get("sbp_url")
+ or payment_result.get("transfer_url")
+ )
+ card_url = payment_result.get("card_url")
+ fallback_url = (
payment_result.get("link_page_url")
or payment_result.get("link_url")
)
+
+ if not (sbp_url or card_url or fallback_url):
+ await message.answer(
+ texts.t(
+ "PAL24_PAYMENT_ERROR",
+ "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.",
+ )
+ )
+ await state.clear()
+ return
+
+ if not sbp_url:
+ sbp_url = fallback_url
+
bill_id = payment_result.get("bill_id")
local_payment_id = payment_result.get("local_payment_id")
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("PAL24_PAY_BUTTON", "🏦 Оплатить через PayPalych (СБП)"),
- url=payment_url,
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"),
- callback_data=f"check_pal24_{local_payment_id}",
- )
- ],
- [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")],
- ]
+ pay_buttons: list[list[types.InlineKeyboardButton]] = []
+ steps: list[str] = []
+ step_counter = 1
+
+ default_sbp_text = texts.t(
+ "PAL24_SBP_PAY_BUTTON",
+ "🏦 Оплатить через PayPalych (СБП)",
)
+ sbp_button_text = settings.get_pal24_sbp_button_text(default_sbp_text)
+
+ if sbp_url:
+ pay_buttons.append(
+ [
+ types.InlineKeyboardButton(
+ text=sbp_button_text,
+ url=sbp_url,
+ )
+ ]
+ )
+ steps.append(
+ texts.t(
+ "PAL24_INSTRUCTION_BUTTON",
+ "{step}. Нажмите кнопку «{button}»",
+ ).format(step=step_counter, button=html.escape(sbp_button_text))
+ )
+ step_counter += 1
+
+ default_card_text = texts.t(
+ "PAL24_CARD_PAY_BUTTON",
+ "💳 Оплатить банковской картой (PayPalych)",
+ )
+ card_button_text = settings.get_pal24_card_button_text(default_card_text)
+
+ if card_url and card_url != sbp_url:
+ pay_buttons.append(
+ [
+ types.InlineKeyboardButton(
+ text=card_button_text,
+ url=card_url,
+ )
+ ]
+ )
+ steps.append(
+ texts.t(
+ "PAL24_INSTRUCTION_BUTTON",
+ "{step}. Нажмите кнопку «{button}»",
+ ).format(step=step_counter, button=html.escape(card_button_text))
+ )
+ step_counter += 1
+
+ if not pay_buttons and fallback_url:
+ pay_buttons.append(
+ [
+ types.InlineKeyboardButton(
+ text=sbp_button_text,
+ url=fallback_url,
+ )
+ ]
+ )
+ steps.append(
+ texts.t(
+ "PAL24_INSTRUCTION_BUTTON",
+ "{step}. Нажмите кнопку «{button}»",
+ ).format(step=step_counter, button=html.escape(sbp_button_text))
+ )
+ step_counter += 1
+
+ follow_template = texts.t(
+ "PAL24_INSTRUCTION_FOLLOW",
+ "{step}. Следуйте подсказкам платёжной системы",
+ )
+ steps.append(follow_template.format(step=step_counter))
+ step_counter += 1
+
+ confirm_template = texts.t(
+ "PAL24_INSTRUCTION_CONFIRM",
+ "{step}. Подтвердите перевод",
+ )
+ steps.append(confirm_template.format(step=step_counter))
+ step_counter += 1
+
+ success_template = texts.t(
+ "PAL24_INSTRUCTION_COMPLETE",
+ "{step}. Средства зачислятся автоматически",
+ )
+ steps.append(success_template.format(step=step_counter))
message_template = texts.t(
"PAL24_PAYMENT_INSTRUCTIONS",
(
- "🏦 Оплата через PayPalych (СБП)\n\n"
+ "🏦 Оплата через PayPalych\n\n"
"💰 Сумма: {amount}\n"
"🆔 ID счета: {bill_id}\n\n"
- "📱 Инструкция:\n"
- "1. Нажмите кнопку ‘Оплатить через PayPalych (СБП)’\n"
- "2. Следуйте подсказкам платежной системы\n"
- "3. Подтвердите перевод\n"
- "4. Средства зачислятся автоматически\n\n"
+ "📱 Инструкция:\n{steps}\n\n"
"❓ Если возникнут проблемы, обратитесь в {support}"
),
)
+ keyboard_rows = pay_buttons + [
+ [
+ types.InlineKeyboardButton(
+ text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"),
+ callback_data=f"check_pal24_{local_payment_id}",
+ )
+ ],
+ [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")],
+ ]
+
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
+
message_text = message_template.format(
amount=settings.format_price(amount_kopeks),
bill_id=bill_id,
+ steps="\n".join(steps),
support=settings.get_support_contact_display_html(),
)
@@ -1231,28 +1327,48 @@ async def check_pal24_payment_status(
emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно"))
- payment_link = payment.link_page_url or payment.link_url
+ metadata = payment.metadata_json or {}
+ links_meta = metadata.get("links") if isinstance(metadata, dict) else None
+ if not isinstance(links_meta, dict):
+ links_meta = {}
+
+ sbp_link = links_meta.get("sbp") or payment.link_url
+ card_link = links_meta.get("card")
+
+ if not card_link and payment.link_page_url and payment.link_page_url != sbp_link:
+ card_link = payment.link_page_url
message_lines = [
- "🏦 Статус платежа PayPalych (СБП):\n\n",
- f"🆔 ID счета: {payment.bill_id}\n",
- f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n",
- f"📊 Статус: {emoji} {status_text}\n",
- f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n",
+ "🏦 Статус платежа PayPalych:",
+ "",
+ f"🆔 ID счета: {payment.bill_id}",
+ f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}",
+ f"📊 Статус: {emoji} {status_text}",
+ f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}",
]
if payment.is_paid:
- message_lines.append("\n✅ Платеж успешно завершен! Средства уже на балансе.")
+ message_lines.append("")
+ message_lines.append("✅ Платеж успешно завершен! Средства уже на балансе.")
elif payment.status in {"NEW", "PROCESS"}:
- message_lines.append("\n⏳ Платеж еще не завершен. Оплатите счет и проверьте статус позже.")
- if payment_link:
- message_lines.append(f"\n🔗 Ссылка на оплату: {payment_link}")
+ message_lines.append("")
+ message_lines.append("⏳ Платеж еще не завершен. Оплатите счет и проверьте статус позже.")
+ if sbp_link:
+ message_lines.append("")
+ message_lines.append(f"🏦 СБП: {sbp_link}")
+ if card_link and card_link != sbp_link:
+ message_lines.append(f"💳 Банковская карта: {card_link}")
elif payment.status in {"FAIL", "UNDERPAID", "OVERPAID"}:
+ message_lines.append("")
message_lines.append(
- f"\n❌ Платеж не завершен корректно. Обратитесь в {settings.get_support_contact_display()}"
+ f"❌ Платеж не завершен корректно. Обратитесь в {settings.get_support_contact_display()}"
)
- await callback.answer("".join(message_lines), show_alert=True)
+ await callback.answer()
+ await callback.message.answer(
+ "\n".join(message_lines),
+ disable_web_page_preview=True,
+ )
except Exception as e:
logger.error(f"Ошибка проверки статуса PayPalych: {e}")
diff --git a/app/services/payment_service.py b/app/services/payment_service.py
index c615e8cc..6523375d 100644
--- a/app/services/payment_service.py
+++ b/app/services/payment_service.py
@@ -872,9 +872,55 @@ class PaymentService:
logger.error("Pal24 не вернул bill_id: %s", response)
return None
- link_url = response.get("link_url")
- link_page_url = response.get("link_page_url")
- primary_link = link_page_url or link_url
+ 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
+ }
payment = await create_pal24_payment(
db,
@@ -887,11 +933,12 @@ class PaymentService:
type_=response.get("type", "normal"),
currency=response.get("currency", "RUB"),
link_url=primary_link,
- link_page_url=link_page_url or link_url,
+ link_page_url=secondary_link,
ttl=ttl_seconds,
metadata={
"raw_response": response,
"language": language,
+ **({"links": metadata_links} if metadata_links else {}),
},
)
@@ -899,9 +946,12 @@ class PaymentService:
"bill_id": bill_id,
"order_id": order_id,
"link_url": primary_link,
- "link_page_url": link_page_url or link_url,
+ "link_page_url": secondary_link,
"local_payment_id": payment.id,
"amount_kopeks": amount_kopeks,
+ "sbp_url": transfer_url or primary_link,
+ "card_url": card_url,
+ "transfer_url": transfer_url,
}
logger.info(
diff --git a/locales/en.json b/locales/en.json
index 8d289061..3b95753a 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -86,7 +86,13 @@
"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.",
"PAL24_PAYMENT_ERROR": "❌ Failed to create a PayPalych payment. Please try again later or contact support.",
"PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
- "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_SBP_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
+ "PAL24_CARD_PAY_BUTTON": "💳 Pay with a bank card (PayPalych)",
+ "PAL24_PAYMENT_INSTRUCTIONS": "🏦 PayPalych payment\n\n💰 Amount: {amount}\n🆔 Invoice ID: {bill_id}\n\n📱 How to pay:\n{steps}\n\n❓ Need help? Contact {support}",
+ "PAL24_INSTRUCTION_BUTTON": "{step}. Press “{button}”",
+ "PAL24_INSTRUCTION_FOLLOW": "{step}. Follow the payment page instructions",
+ "PAL24_INSTRUCTION_CONFIRM": "{step}. Confirm the transfer",
+ "PAL24_INSTRUCTION_COMPLETE": "{step}. The funds will be credited automatically",
"PENDING_CANCEL_BUTTON": "⌛ Cancel",
"POST_REGISTRATION_TRIAL_BUTTON": "🚀 Activate free trial 🚀",
"REFERRAL_ANALYTICS_BUTTON": "📊 Analytics",
diff --git a/locales/ru.json b/locales/ru.json
index 5f0bdc0b..c36d4516 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -276,7 +276,13 @@
"PAL24_TOPUP_PROMPT": "🏦 Оплата через PayPalych (СБП)\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.",
"PAL24_PAYMENT_ERROR": "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.",
"PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
- "PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 Инструкция:\n1. Нажмите кнопку ‘Оплатить через PayPalych (СБП)’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
+ "PAL24_SBP_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
+ "PAL24_CARD_PAY_BUTTON": "💳 Оплатить банковской картой (PayPalych)",
+ "PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 Инструкция:\n{steps}\n\n❓ Если возникнут проблемы, обратитесь в {support}",
+ "PAL24_INSTRUCTION_BUTTON": "{step}. Нажмите кнопку «{button}»",
+ "PAL24_INSTRUCTION_FOLLOW": "{step}. Следуйте подсказкам платёжной системы",
+ "PAL24_INSTRUCTION_CONFIRM": "{step}. Подтвердите перевод",
+ "PAL24_INSTRUCTION_COMPLETE": "{step}. Средства зачислятся автоматически",
"PENDING_CANCEL_BUTTON": "⌛ Отмена",
"PERIOD_14_DAYS": "📅 14 дней - {settings.format_price(settings.PRICE_14_DAYS)}",
"PERIOD_180_DAYS": "📅 180 дней - {settings.format_price(settings.PRICE_180_DAYS)}",