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