feat: make MulenPay display name configurable

This commit is contained in:
Egor
2025-10-19 01:55:10 +03:00
parent bf126bdadb
commit 0c93516056
20 changed files with 239 additions and 98 deletions
+10
View File
@@ -212,6 +212,7 @@ class Settings(BaseSettings):
MULENPAY_SHOP_ID: Optional[int] = None
MULENPAY_BASE_URL: str = "https://mulenpay.ru/api"
MULENPAY_WEBHOOK_PATH: str = "/mulenpay-webhook"
MULENPAY_DISPLAY_NAME: str = "Mulen Pay"
MULENPAY_DESCRIPTION: str = "Пополнение баланса"
MULENPAY_LANGUAGE: str = "ru"
MULENPAY_VAT_CODE: int = 0
@@ -793,6 +794,15 @@ class Settings(BaseSettings):
and self.MULENPAY_SHOP_ID is not None
)
def get_mulenpay_display_name(self) -> str:
name = (self.MULENPAY_DISPLAY_NAME or "").strip()
if not name:
return "Mulen Pay"
return name
def get_mulenpay_display_name_html(self) -> str:
return html.escape(self.get_mulenpay_display_name())
def is_pal24_enabled(self) -> bool:
return (
self.PAL24_ENABLED
+3 -1
View File
@@ -5,6 +5,7 @@ from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import MulenPayPayment
logger = logging.getLogger(__name__)
@@ -40,7 +41,8 @@ async def create_mulenpay_payment(
await db.refresh(payment)
logger.info(
"Создан MulenPay платеж #%s (uuid=%s) на сумму %s копеек для пользователя %s",
"Создан %s платеж #%s (uuid=%s) на сумму %s копеек для пользователя %s",
settings.get_mulenpay_display_name(),
payment.mulen_payment_id,
uuid,
amount_kopeks,
+6 -5
View File
@@ -3427,18 +3427,19 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей CryptoBot payments")
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ MULEN PAY ===")
mulenpay_name = settings.get_mulenpay_display_name()
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ %s ===", mulenpay_name)
mulenpay_created = await create_mulenpay_payments_table()
if mulenpay_created:
logger.info("✅ Таблица Mulen Pay payments готова")
logger.info("✅ Таблица %s payments готова", mulenpay_name)
else:
logger.warning("⚠️ Проблемы с таблицей Mulen Pay payments")
logger.warning("⚠️ Проблемы с таблицей %s payments", mulenpay_name)
mulenpay_schema_ok = await ensure_mulenpay_payment_schema()
if mulenpay_schema_ok:
logger.info("✅ Схема Mulen Pay payments актуальна")
logger.info("✅ Схема %s payments актуальна", mulenpay_name)
else:
logger.warning("⚠️ Не удалось обновить схему Mulen Pay payments")
logger.warning("⚠️ Не удалось обновить схему %s payments", mulenpay_name)
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PAL24 ===")
pal24_created = await create_pal24_payments_table()
+44 -15
View File
@@ -48,7 +48,12 @@ class WebhookServer:
logger.info(f"Webhook сервер настроен:")
logger.info(f" - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}")
if settings.is_mulenpay_enabled():
logger.info(f" - Mulen Pay webhook: POST {settings.MULENPAY_WEBHOOK_PATH}")
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
" - %s webhook: POST %s",
mulenpay_name,
settings.MULENPAY_WEBHOOK_PATH,
)
if settings.is_cryptobot_enabled():
logger.info(f" - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}")
logger.info(f" - Health check: GET /health")
@@ -84,8 +89,10 @@ class WebhookServer:
settings.TRIBUTE_WEBHOOK_PATH,
)
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
"Mulen Pay webhook URL: http://%s:%s%s",
"%s webhook URL: http://%s:%s%s",
mulenpay_name,
settings.TRIBUTE_WEBHOOK_HOST,
settings.TRIBUTE_WEBHOOK_PORT,
settings.MULENPAY_WEBHOOK_PATH,
@@ -128,24 +135,37 @@ class WebhookServer:
async def _mulenpay_webhook_handler(self, request: web.Request) -> web.Response:
try:
logger.info(f"Получен Mulen Pay webhook: {request.method} {request.path}")
logger.info(f"MulenPay webhook headers: {dict(request.headers)}")
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
"%s webhook: %s %s",
mulenpay_name,
request.method,
request.path,
)
logger.info(
"%s webhook headers: %s",
mulenpay_name,
dict(request.headers),
)
raw_body = await request.read()
if not raw_body:
logger.warning("Пустой Mulen Pay webhook")
logger.warning("Пустой %s webhook", mulenpay_name)
return web.json_response({"status": "error", "reason": "empty_body"}, status=400)
# Временно отключаем проверку подписи для отладки
# TODO: Включить обратно после настройки MulenPay
if not self._verify_mulenpay_signature(request, raw_body):
logger.warning("MulenPay webhook signature verification failed, but processing anyway for debugging")
logger.warning(
"%s webhook signature verification failed, but processing anyway for debugging",
mulenpay_name,
)
# return web.json_response({"status": "error", "reason": "invalid_signature"}, status=401)
try:
payload = json.loads(raw_body.decode('utf-8'))
except json.JSONDecodeError as error:
logger.error(f"Ошибка парсинга Mulen Pay webhook: {error}")
logger.error(f"Ошибка парсинга {mulenpay_name} webhook: {error}")
return web.json_response({"status": "error", "reason": "invalid_json"}, status=400)
payment_service = PaymentService(self.bot)
@@ -160,7 +180,10 @@ class WebhookServer:
return web.json_response({"status": "ok"}, status=200)
return web.json_response({"status": "error", "reason": "processing_failed"}, status=400)
except Exception as error:
logger.error(f"Ошибка обработки Mulen Pay webhook: {error}", exc_info=True)
logger.error(
f"Ошибка обработки {mulenpay_name} webhook: {error}",
exc_info=True,
)
return web.json_response({"status": "error", "reason": "internal_error"}, status=500)
finally:
try:
@@ -169,7 +192,11 @@ class WebhookServer:
pass
except Exception as error:
logger.error(f"Критическая ошибка Mulen Pay webhook: {error}", exc_info=True)
mulenpay_name = settings.get_mulenpay_display_name()
logger.error(
f"Критическая ошибка {mulenpay_name} webhook: {error}",
exc_info=True,
)
return web.json_response({"status": "error", "reason": "internal_error", "message": str(error)}, status=500)
@staticmethod
@@ -183,12 +210,13 @@ class WebhookServer:
@staticmethod
def _verify_mulenpay_signature(request: web.Request, raw_body: bytes) -> bool:
secret_key = settings.MULENPAY_SECRET_KEY
display_name = settings.get_mulenpay_display_name()
if not secret_key:
logger.error("Mulen Pay secret key is not configured")
logger.error("%s secret key is not configured", display_name)
return False
# Логируем все заголовки для отладки
logger.info("MulenPay webhook headers for signature verification:")
logger.info("%s webhook headers for signature verification:", display_name)
for header_name, header_value in request.headers.items():
if any(keyword in header_name.lower() for keyword in ['signature', 'sign', 'token', 'auth']):
logger.info(f" {header_name}: {header_value}")
@@ -239,7 +267,7 @@ class WebhookServer:
if hmac.compare_digest(normalized_signature_no_padding, expected_urlsafe_base64_signature.rstrip('=')):
return True
logger.error("Неверная подпись Mulen Pay webhook")
logger.error("Неверная подпись %s webhook", display_name)
return False
authorization_header = request.headers.get('Authorization')
@@ -252,7 +280,7 @@ class WebhookServer:
if hmac.compare_digest(token, secret_key):
return True
logger.error("Неверный %s токен Mulen Pay webhook", scheme)
logger.error("Неверный %s токен %s webhook", scheme, display_name)
return False
if not value and hmac.compare_digest(token, secret_key):
@@ -270,11 +298,12 @@ class WebhookServer:
return True
logger.info(
"Mulen Pay webhook headers received: %s",
"%s webhook headers received: %s",
display_name,
{key: value for key, value in request.headers.items() if 'authorization' not in key.lower()}
)
logger.error("Отсутствует подпись Mulen Pay webhook")
logger.error("Отсутствует подпись %s webhook", display_name)
return False
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
+20 -6
View File
@@ -1303,7 +1303,10 @@ def _build_settings_keyboard(
label = texts.t("PAYMENT_CARD_TRIBUTE", "💳 Банковская карта (Tribute)")
test_payment_buttons.append([_test_button(f"{label} · тест", "tribute")])
elif category_key == "MULENPAY":
label = texts.t("PAYMENT_CARD_MULENPAY", "💳 Банковская карта (Mulen Pay)")
label = texts.t(
"PAYMENT_CARD_MULENPAY",
"💳 Банковская карта ({mulenpay_name})",
).format(mulenpay_name=settings.get_mulenpay_display_name())
test_payment_buttons.append([_test_button(f"{label} · тест", "mulenpay")])
elif category_key == "WATA":
label = texts.t("PAYMENT_CARD_WATA", "💳 Банковская карта (WATA)")
@@ -1834,8 +1837,13 @@ async def test_payment_provider(
return
if method == "mulenpay":
mulenpay_name = settings.get_mulenpay_display_name()
mulenpay_name_html = settings.get_mulenpay_display_name_html()
if not settings.is_mulenpay_enabled():
await callback.answer("❌ MulenPay отключен", show_alert=True)
await callback.answer(
f"{mulenpay_name} отключен",
show_alert=True,
)
return
amount_kopeks = 1 * 100
@@ -1843,18 +1851,21 @@ async def test_payment_provider(
db=db,
user_id=db_user.id,
amount_kopeks=amount_kopeks,
description="Тестовый платеж MulenPay (админ)",
description=f"Тестовый платеж {mulenpay_name} (админ)",
language=language,
)
if not payment_result or not payment_result.get("payment_url"):
await callback.answer("❌ Не удалось создать платеж MulenPay", show_alert=True)
await callback.answer(
f"❌ Не удалось создать платеж {mulenpay_name}",
show_alert=True,
)
await _refresh_markup()
return
payment_url = payment_result["payment_url"]
message_text = (
"🧪 <b>Тестовый платеж MulenPay</b>\n\n"
f"🧪 <b>Тестовый платеж {mulenpay_name_html}</b>\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"🆔 ID: {payment_result['mulen_payment_id']}"
)
@@ -1875,7 +1886,10 @@ async def test_payment_provider(
]
)
await callback.message.answer(message_text, reply_markup=reply_markup, parse_mode="HTML")
await callback.answer("✅ Ссылка на платеж MulenPay отправлена", show_alert=True)
await callback.answer(
f"✅ Ссылка на платеж {mulenpay_name} отправлена",
show_alert=True,
)
await _refresh_markup()
return
+34 -17
View File
@@ -21,19 +21,28 @@ async def start_mulenpay_payment(
state: FSMContext,
):
texts = get_texts(db_user.language)
mulenpay_name = settings.get_mulenpay_display_name()
mulenpay_name_html = settings.get_mulenpay_display_name_html()
if not settings.is_mulenpay_enabled():
await callback.answer("❌ Оплата через Mulen Pay временно недоступна", show_alert=True)
await callback.answer(
f"❌ Оплата через {mulenpay_name} временно недоступна",
show_alert=True,
)
return
message_text = texts.t(
message_template = texts.t(
"MULENPAY_TOPUP_PROMPT",
(
"💳 <b>Оплата через Mulen Pay</b>\n\n"
"💳 <b>Оплата через {mulenpay_name_html}</b>\n\n"
"Введите сумму для пополнения от 100 до 100 000 ₽.\n"
"Оплата происходит через защищенную платформу Mulen Pay."
"Оплата происходит через защищенную платформу {mulenpay_name}."
),
)
message_text = message_template.format(
mulenpay_name=mulenpay_name,
mulenpay_name_html=mulenpay_name_html,
)
keyboard = get_back_keyboard(db_user.language)
@@ -63,9 +72,11 @@ async def process_mulenpay_payment_amount(
state: FSMContext,
):
texts = get_texts(db_user.language)
mulenpay_name = settings.get_mulenpay_display_name()
mulenpay_name_html = settings.get_mulenpay_display_name_html()
if not settings.is_mulenpay_enabled():
await message.answer("❌ Оплата через Mulen Pay временно недоступна")
await message.answer(f"❌ Оплата через {mulenpay_name} временно недоступна")
return
if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS:
@@ -96,8 +107,8 @@ async def process_mulenpay_payment_amount(
await message.answer(
texts.t(
"MULENPAY_PAYMENT_ERROR",
"❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.",
)
"❌ Ошибка создания платежа {mulenpay_name}. Попробуйте позже или обратитесь в поддержку.",
).format(mulenpay_name=mulenpay_name)
)
await state.clear()
return
@@ -112,8 +123,8 @@ async def process_mulenpay_payment_amount(
types.InlineKeyboardButton(
text=texts.t(
"MULENPAY_PAY_BUTTON",
"💳 Оплатить через Mulen Pay",
),
"💳 Оплатить через {mulenpay_name}",
).format(mulenpay_name=mulenpay_name),
url=payment_url,
)
],
@@ -132,11 +143,11 @@ async def process_mulenpay_payment_amount(
message_template = texts.t(
"MULENPAY_PAYMENT_INSTRUCTIONS",
(
"💳 <b>Оплата через Mulen Pay</b>\n\n"
"💳 <b>Оплата через {mulenpay_name_html}</b>\n\n"
"💰 Сумма: {amount}\n"
"🆔 ID платежа: {payment_id}\n\n"
"📱 <b>Инструкция:</b>\n"
"1. Нажмите кнопку 'Оплатить через Mulen Pay'\n"
"1. Нажмите кнопку 'Оплатить через {mulenpay_name}'\n"
"2. Следуйте подсказкам платежной системы\n"
"3. Подтвердите перевод\n"
"4. Средства зачислятся автоматически\n\n"
@@ -148,6 +159,8 @@ async def process_mulenpay_payment_amount(
amount=settings.format_price(amount_kopeks),
payment_id=payment_id_display,
support=settings.get_support_contact_display_html(),
mulenpay_name=mulenpay_name,
mulenpay_name_html=mulenpay_name_html,
)
await message.answer(
@@ -159,19 +172,20 @@ async def process_mulenpay_payment_amount(
await state.clear()
logger.info(
"Создан MulenPay платеж для пользователя %s: %s₽, ID: %s",
"Создан %s платеж для пользователя %s: %s₽, ID: %s",
mulenpay_name,
db_user.telegram_id,
amount_rubles,
payment_id_display,
)
except Exception as e:
logger.error(f"Ошибка создания MulenPay платежа: {e}")
logger.error(f"Ошибка создания {mulenpay_name} платежа: {e}")
await message.answer(
texts.t(
"MULENPAY_PAYMENT_ERROR",
"❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.",
)
"❌ Ошибка создания платежа {mulenpay_name}. Попробуйте позже или обратитесь в поддержку.",
).format(mulenpay_name=mulenpay_name)
)
await state.clear()
@@ -204,8 +218,9 @@ async def check_mulenpay_payment_status(
emoji, status_text = status_labels.get(payment.status, ("", "Неизвестно"))
mulenpay_name = settings.get_mulenpay_display_name()
message_lines = [
"💳 Статус платежа Mulen Pay:\n\n",
f"💳 Статус платежа {mulenpay_name}:\n\n",
f"🆔 ID: {payment.mulen_payment_id or payment.id}\n",
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n",
f"📊 Статус: {emoji} {status_text}\n",
@@ -234,5 +249,7 @@ async def check_mulenpay_payment_status(
await callback.answer(message_text, show_alert=True)
except Exception as e:
logger.error(f"Ошибка проверки статуса MulenPay: {e}")
logger.error(
f"Ошибка проверки статуса {settings.get_mulenpay_display_name()}: {e}"
)
await callback.answer("❌ Ошибка проверки статуса", show_alert=True)
+12 -4
View File
@@ -186,8 +186,9 @@ def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyb
)])
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
keyboard.append([types.InlineKeyboardButton(
text="💳 MulenPay",
text=f"💳 {mulenpay_name}",
callback_data="simple_subscription_mulenpay"
)])
@@ -773,12 +774,19 @@ async def handle_simple_subscription_payment_method(
elif payment_method == "mulenpay":
# Оплата через MulenPay
mulenpay_name = settings.get_mulenpay_display_name()
if not settings.is_mulenpay_enabled():
await callback.answer("❌ Оплата через MulenPay временно недоступна", show_alert=True)
await callback.answer(
f"❌ Оплата через {mulenpay_name} временно недоступна",
show_alert=True,
)
return
# Здесь должна быть реализация оплаты через MulenPay
await callback.answer("❌ Оплата через MulenPay пока не реализована", show_alert=True)
await callback.answer(
f"❌ Оплата через {mulenpay_name} пока не реализована",
show_alert=True,
)
elif payment_method == "pal24":
# Оплата через PayPalych
+2 -1
View File
@@ -2412,8 +2412,9 @@ def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyb
)])
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
keyboard.append([types.InlineKeyboardButton(
text="💳 MulenPay",
text=f"💳 {mulenpay_name}",
callback_data="simple_subscription_mulenpay"
)])
+5 -1
View File
@@ -1101,9 +1101,13 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LAN
])
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
keyboard.append([
InlineKeyboardButton(
text=texts.t("PAYMENT_CARD_MULENPAY", "💳 Банковская карта (Mulen Pay)"),
text=texts.t(
"PAYMENT_CARD_MULENPAY",
"💳 Банковская карта ({mulenpay_name})",
).format(mulenpay_name=mulenpay_name),
callback_data=_build_callback("mulenpay")
)
])
+7 -7
View File
@@ -922,10 +922,10 @@
"MINIAPP_PURCHASE_DISCOUNT_TRAFFIC": "Traffic discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_STATUS_INSUFFICIENT": "Not enough funds on balance",
"MINIAPP_PURCHASE_SUMMARY_DISCOUNT": "You save {amount}",
"MULENPAY_PAYMENT_ERROR": "❌ Failed to create Mulen Pay payment. Please try again later or contact support.",
"MULENPAY_PAYMENT_INSTRUCTIONS": "💳 <b>Mulen Pay payment</b>\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 <b>How to pay:</b>\n1. Press Pay with Mulen Pay\n2. Follow the instructions on the payment page\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
"MULENPAY_PAY_BUTTON": "💳 Pay with Mulen Pay",
"MULENPAY_TOPUP_PROMPT": "💳 <b>Mulen Pay payment</b>\n\nEnter an amount between 100 and 100,000 ₽.\nThe payment is processed by the secure Mulen Pay platform.",
"MULENPAY_PAYMENT_ERROR": "❌ Failed to create a {mulenpay_name} payment. Please try again later or contact support.",
"MULENPAY_PAYMENT_INSTRUCTIONS": "💳 <b>{mulenpay_name_html} payment</b>\n\n💰 Amount: {amount}\n🆔 Payment ID: {payment_id}\n\n📱 <b>How to pay:</b>\n1. Press Pay with {mulenpay_name}\n2. Follow the instructions on the payment page\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
"MULENPAY_PAY_BUTTON": "💳 Pay with {mulenpay_name}",
"MULENPAY_TOPUP_PROMPT": "💳 <b>{mulenpay_name_html} payment</b>\n\nEnter an amount between 100 and 100,000 ₽.\nThe payment is processed by the secure {mulenpay_name} platform.",
"MY_BALANCE_BUTTON": "💰 My balance",
"MY_SUBSCRIPTION_BUTTON": "📱 My subscription",
"MY_TICKETS_BUTTON": "📋 My tickets",
@@ -965,7 +965,7 @@
"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)",
"PAYMENT_CARD_MULENPAY": "💳 Bank card ({mulenpay_name})",
"PAYMENT_CARD_PAL24": "🏦 SBP (PayPalych)",
"PAYMENT_CARD_TRIBUTE": "💳 Bank card (Tribute)",
"PAYMENT_CARD_WATA": "💳 Bank card (WATA)",
@@ -979,8 +979,8 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ Automated payment methods are temporarily unavailable. Contact support to top up your balance.",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "via CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 <b>Cryptocurrency</b>",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via Mulen Pay",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Bank card (Mulen Pay)</b>",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via {mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Bank card ({mulenpay_name})</b>",
"PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System",
"PAYMENT_METHOD_PAL24_NAME": "🏦 <b>SBP (PayPalych)</b>",
"PAYMENT_METHOD_STARS_DESCRIPTION": "fast and convenient",
+7 -7
View File
@@ -922,10 +922,10 @@
"MINIAPP_PURCHASE_DISCOUNT_TRAFFIC": "Скидка на трафик: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_STATUS_INSUFFICIENT": "Недостаточно средств на балансе",
"MINIAPP_PURCHASE_SUMMARY_DISCOUNT": "Экономия {amount}",
"MULENPAY_PAYMENT_ERROR": "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.",
"MULENPAY_PAYMENT_INSTRUCTIONS": "💳 <b>Оплата через Mulen Pay</b>\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 <b>Инструкция:</b>\n1. Нажмите кнопку ‘Оплатить через Mulen Pay’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
"MULENPAY_PAY_BUTTON": "💳 Оплатить через Mulen Pay",
"MULENPAY_TOPUP_PROMPT": "💳 <b>Оплата через Mulen Pay</b>\n\nВведите сумму для пополнения от 100 до 100 000 ₽.\nОплата происходит через защищенную платформу Mulen Pay.",
"MULENPAY_PAYMENT_ERROR": "❌ Ошибка создания платежа {mulenpay_name}. Попробуйте позже или обратитесь в поддержку.",
"MULENPAY_PAYMENT_INSTRUCTIONS": "💳 <b>Оплата через {mulenpay_name_html}</b>\n\n💰 Сумма: {amount}\n🆔 ID платежа: {payment_id}\n\n📱 <b>Инструкция:</b>\n1. Нажмите кнопку ‘Оплатить через {mulenpay_name}’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
"MULENPAY_PAY_BUTTON": "💳 Оплатить через {mulenpay_name}",
"MULENPAY_TOPUP_PROMPT": "💳 <b>Оплата через {mulenpay_name_html}</b>\n\nВведите сумму для пополнения от 100 до 100 000 ₽.\nОплата происходит через защищенную платформу {mulenpay_name}.",
"MY_BALANCE_BUTTON": "💰 Мой баланс",
"MY_SUBSCRIPTION_BUTTON": "📱 Моя подписка",
"MY_TICKETS_BUTTON": "📋 Мои тикеты",
@@ -965,7 +965,7 @@
"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)",
"PAYMENT_CARD_MULENPAY": "💳 Банковская карта ({mulenpay_name})",
"PAYMENT_CARD_PAL24": "🏦 СБП (PayPalych)",
"PAYMENT_CARD_TRIBUTE": "💳 Банковская карта (Tribute)",
"PAYMENT_CARD_WATA": "💳 Банковская карта (WATA)",
@@ -979,8 +979,8 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 <b>Криптовалюта</b>",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через Mulen Pay",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Банковская карта (Mulen Pay)</b>",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Банковская карта ({mulenpay_name})</b>",
"PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей",
"PAYMENT_METHOD_PAL24_NAME": "🏦 <b>СБП (PayPalych)</b>",
"PAYMENT_METHOD_STARS_DESCRIPTION": "быстро и удобно",
+2 -1
View File
@@ -802,11 +802,12 @@ class AdminNotificationService:
return self.enabled and bool(self.chat_id)
def _get_payment_method_display(self, payment_method: Optional[str]) -> str:
mulenpay_name = settings.get_mulenpay_display_name()
method_names = {
'telegram_stars': '⭐ Telegram Stars',
'yookassa': '💳 YooKassa (карта)',
'tribute': '💎 Tribute (карта)',
'mulenpay': '💳 Mulen Pay (карта)',
'mulenpay': f'💳 {mulenpay_name} (карта)',
'pal24': '🏦 PayPalych (СБП)',
'manual': '🛠️ Вручную (админ)',
'balance': '💰 С баланса'
+46 -22
View File
@@ -28,13 +28,16 @@ class MulenPayPaymentMixin:
language: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""Создаёт локальный платеж и инициализирует сессию в MulenPay."""
display_name = settings.get_mulenpay_display_name()
display_name_html = settings.get_mulenpay_display_name_html()
if not getattr(self, "mulenpay_service", None):
logger.error("MulenPay сервис не инициализирован")
logger.error("%s сервис не инициализирован", display_name)
return None
if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS:
logger.warning(
"Сумма MulenPay меньше минимальной: %s < %s",
"Сумма %s меньше минимальной: %s < %s",
display_name,
amount_kopeks,
settings.MULENPAY_MIN_AMOUNT_KOPEKS,
)
@@ -42,7 +45,8 @@ class MulenPayPaymentMixin:
if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS:
logger.warning(
"Сумма MulenPay больше максимальной: %s > %s",
"Сумма %s больше максимальной: %s > %s",
display_name,
amount_kopeks,
settings.MULENPAY_MAX_AMOUNT_KOPEKS,
)
@@ -74,7 +78,7 @@ class MulenPayPaymentMixin:
)
if not response:
logger.error("Ошибка создания MulenPay платежа")
logger.error("Ошибка создания %s платежа", display_name)
return None
mulen_payment_id = response.get("id")
@@ -100,7 +104,8 @@ class MulenPayPaymentMixin:
)
logger.info(
"Создан MulenPay платеж %s на %s₽ для пользователя %s",
"Создан %s платеж %s на %s₽ для пользователя %s",
display_name,
mulen_payment_id,
amount_rubles,
user_id,
@@ -116,7 +121,7 @@ class MulenPayPaymentMixin:
}
except Exception as error:
logger.error("Ошибка создания MulenPay платежа: %s", error)
logger.error("Ошибка создания %s платежа: %s", display_name, error)
return None
async def process_mulenpay_callback(
@@ -125,6 +130,7 @@ class MulenPayPaymentMixin:
callback_data: Dict[str, Any],
) -> bool:
"""Обрабатывает callback от MulenPay, обновляет статус и начисляет баланс."""
display_name = settings.get_mulenpay_display_name()
try:
payment_module = import_module("app.services.payment_service")
uuid_value = callback_data.get("uuid")
@@ -138,14 +144,15 @@ class MulenPayPaymentMixin:
mulen_payment_id_int = None
amount_value = callback_data.get("amount")
logger.debug(
"MulenPay callback: uuid=%s, status=%s, amount=%s",
"%s callback: uuid=%s, status=%s, amount=%s",
display_name,
uuid_value,
payment_status,
amount_value,
)
if not uuid_value and mulen_payment_id_raw is None:
logger.error("MulenPay callback без uuid и id")
logger.error("%s callback без uuid и id", display_name)
return False
payment = None
@@ -159,7 +166,8 @@ class MulenPayPaymentMixin:
if not payment:
logger.error(
"MulenPay платеж не найден (uuid=%s, id=%s)",
"%s платеж не найден (uuid=%s, id=%s)",
display_name,
uuid_value,
mulen_payment_id_raw,
)
@@ -167,7 +175,8 @@ class MulenPayPaymentMixin:
if payment.is_paid:
logger.info(
"MulenPay платеж %s уже обработан, игнорируем повторный callback",
"%s платеж %s уже обработан, игнорируем повторный callback",
display_name,
payment.uuid,
)
return True
@@ -183,7 +192,8 @@ class MulenPayPaymentMixin:
if payment.transaction_id:
logger.info(
"Для MulenPay платежа %s уже создана транзакция",
"Для %s платежа %s уже создана транзакция",
display_name,
payment.uuid,
)
return True
@@ -199,7 +209,7 @@ class MulenPayPaymentMixin:
user_id=payment.user_id,
type=TransactionType.DEPOSIT,
amount_kopeks=payment.amount_kopeks,
description=f"Пополнение через MulenPay: {payment_description}",
description=f"Пополнение через {display_name}: {payment_description}",
payment_method=PaymentMethod.MULENPAY,
external_id=payment.uuid,
is_completed=True,
@@ -214,8 +224,9 @@ class MulenPayPaymentMixin:
user = await payment_module.get_user_by_id(db, payment.user_id)
if not user:
logger.error(
"Пользователь %s не найден при обработке MulenPay",
"Пользователь %s не найден при обработке %s",
payment.user_id,
display_name,
)
return False
@@ -226,7 +237,7 @@ class MulenPayPaymentMixin:
db,
user,
payment.amount_kopeks,
f"Пополнение MulenPay: {payment.amount_kopeks // 100}",
f"Пополнение {display_name}: {payment.amount_kopeks // 100}",
)
if was_first_topup and not user.has_made_first_topup:
@@ -261,7 +272,8 @@ class MulenPayPaymentMixin:
)
except Exception as error:
logger.error(
"Ошибка отправки уведомления о пополнении MulenPay: %s",
"Ошибка отправки уведомления о пополнении %s: %s",
display_name,
error,
)
@@ -273,7 +285,7 @@ class MulenPayPaymentMixin:
(
"✅ <b>Пополнение успешно!</b>\n\n"
f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n"
"🦊 Способ: Mulen Pay\n"
f"🦊 Способ: {display_name_html}\n"
f"🆔 Транзакция: {transaction.id}\n\n"
"Баланс пополнен автоматически!"
),
@@ -282,7 +294,8 @@ class MulenPayPaymentMixin:
)
except Exception as error:
logger.error(
"Ошибка отправки уведомления пользователю MulenPay: %s",
"Ошибка отправки уведомления пользователю %s: %s",
display_name,
error,
)
@@ -329,7 +342,8 @@ class MulenPayPaymentMixin:
logger.error(f"Ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}", exc_info=True)
logger.info(
"✅ Обработан MulenPay платеж %s для пользователя %s",
"✅ Обработан %s платеж %s для пользователя %s",
display_name,
payment.uuid,
payment.user_id,
)
@@ -343,7 +357,7 @@ class MulenPayPaymentMixin:
callback_payload=callback_data,
mulen_payment_id=mulen_payment_id_int,
)
logger.info("MulenPay платеж %s отменен", payment.uuid)
logger.info("%s платеж %s отменен", display_name, payment.uuid)
return True
await payment_module.update_mulenpay_payment_status(
@@ -354,14 +368,20 @@ class MulenPayPaymentMixin:
mulen_payment_id=mulen_payment_id_int,
)
logger.info(
"Получен MulenPay callback со статусом %s для платежа %s",
"Получен %s callback со статусом %s для платежа %s",
display_name,
payment_status,
payment.uuid,
)
return True
except Exception as error:
logger.error("Ошибка обработки MulenPay callback: %s", error, exc_info=True)
logger.error(
"Ошибка обработки %s callback: %s",
display_name,
error,
exc_info=True,
)
return False
def _map_mulenpay_status(self, status_code: Optional[int]) -> str:
@@ -383,6 +403,7 @@ class MulenPayPaymentMixin:
local_payment_id: int,
) -> Optional[Dict[str, Any]]:
"""Возвращает текущее состояние платежа и при необходимости синхронизирует его."""
display_name = settings.get_mulenpay_display_name()
try:
payment_module = import_module("app.services.payment_service")
@@ -437,6 +458,9 @@ class MulenPayPaymentMixin:
except Exception as error:
logger.error(
"Ошибка получения статуса MulenPay: %s", error, exc_info=True
"Ошибка получения статуса %s: %s",
display_name,
error,
exc_info=True,
)
return None
+3 -2
View File
@@ -209,12 +209,13 @@ class PaymentService(
)
self.wata_service = WataService() if settings.is_wata_enabled() else None
mulenpay_name = settings.get_mulenpay_display_name()
logger.debug(
"PaymentService инициализирован (YooKassa=%s, Stars=%s, CryptoBot=%s, "
"MulenPay=%s, Pal24=%s, Wata=%s)",
"PaymentService инициализирован (YooKassa=%s, Stars=%s, CryptoBot=%s, %s=%s, Pal24=%s, Wata=%s)",
bool(self.yookassa_service),
bool(self.stars_service),
bool(self.cryptobot_service),
mulenpay_name,
bool(self.mulenpay_service),
bool(self.pal24_service),
bool(self.wata_service),
+13 -3
View File
@@ -76,7 +76,7 @@ class BotConfigurationService:
"CRYPTOBOT": "🪙 CryptoBot",
"YOOKASSA": "🟣 YooKassa",
"TRIBUTE": "🎁 Tribute",
"MULENPAY": "💰 MulenPay",
"MULENPAY": "💰 {mulenpay_name}",
"PAL24": "🏦 PAL24 / PayPalych",
"WATA": "💠 Wata",
"EXTERNAL_ADMIN": "🛡️ Внешняя админка",
@@ -124,7 +124,7 @@ class BotConfigurationService:
"PAYMENT": "Общие тексты платежей, описания чеков и шаблоны.",
"YOOKASSA": "Интеграция с YooKassa: идентификаторы магазина и вебхуки.",
"CRYPTOBOT": "CryptoBot и криптоплатежи через Telegram.",
"MULENPAY": "Платежи MulenPay и параметры магазина.",
"MULENPAY": "Платежи {mulenpay_name} и параметры магазина.",
"PAL24": "PAL24 / PayPalych подключения и лимиты.",
"TRIBUTE": "Tribute и донат-сервисы.",
"TELEGRAM": "Telegram Stars и их стоимость.",
@@ -166,6 +166,14 @@ class BotConfigurationService:
"MODERATION": "Настройки фильтров отображаемых имен и защиты от фишинга.",
}
@staticmethod
def _format_dynamic_copy(category_key: Optional[str], value: str) -> str:
if not value:
return value
if category_key == "MULENPAY":
return value.format(mulenpay_name=settings.get_mulenpay_display_name())
return value
CATEGORY_KEY_OVERRIDES: Dict[str, str] = {
"DATABASE_URL": "DATABASE",
"DATABASE_MODE": "DATABASE",
@@ -459,7 +467,8 @@ class BotConfigurationService:
@classmethod
def get_category_description(cls, category_key: str) -> str:
return cls.CATEGORY_DESCRIPTIONS.get(category_key, "")
description = cls.CATEGORY_DESCRIPTIONS.get(category_key, "")
return cls._format_dynamic_copy(category_key, description)
@classmethod
def is_toggle(cls, key: str) -> bool:
@@ -598,6 +607,7 @@ class BotConfigurationService:
category_key,
category_key.capitalize() if category_key else "Прочее",
)
category_label = cls._format_dynamic_copy(category_key, category_label)
cls._definitions[key] = SettingDefinition(
key=key,
+7 -2
View File
@@ -672,7 +672,10 @@ class UserService:
mulenpay_payments = mulenpay_result.scalars().all()
if mulenpay_payments:
logger.info(f"🔄 Удаляем {len(mulenpay_payments)} MulenPay платежей")
mulenpay_name = settings.get_mulenpay_display_name()
logger.info(
f"🔄 Удаляем {len(mulenpay_payments)} {mulenpay_name} платежей"
)
await db.execute(
update(MulenPayPayment)
.where(MulenPayPayment.user_id == user_id)
@@ -684,7 +687,9 @@ class UserService:
)
await db.flush()
except Exception as e:
logger.error(f"❌ Ошибка удаления MulenPay платежей: {e}")
logger.error(
f"❌ Ошибка удаления {settings.get_mulenpay_display_name()} платежей: {e}"
)
try:
pal24_result = await db.execute(
+8 -1
View File
@@ -46,11 +46,12 @@ def get_available_payment_methods() -> List[Dict[str, str]]:
})
if settings.is_mulenpay_enabled():
mulenpay_name = settings.get_mulenpay_display_name()
methods.append({
"id": "mulenpay",
"name": "Банковская карта",
"icon": "💳",
"description": "через Mulen Pay",
"description": f"через {mulenpay_name}",
"callback": "topup_mulenpay"
})
@@ -129,6 +130,12 @@ def get_payment_methods_text(language: str) -> str:
f"PAYMENT_METHOD_{method_id}_DESCRIPTION",
method['description'],
)
if method_id == "MULENPAY":
mulenpay_name = settings.get_mulenpay_display_name()
mulenpay_name_html = settings.get_mulenpay_display_name_html()
name = name.format(mulenpay_name=mulenpay_name_html)
description = description.format(mulenpay_name=mulenpay_name)
text += f"{name} - {description}\n"
text += "\n" + texts.t(
+1
View File
@@ -668,6 +668,7 @@ async def get_payment_methods(
methods.append(
MiniAppPaymentMethod(
id="mulenpay",
name=settings.get_mulenpay_display_name(),
icon="💳",
requires_amount=True,
currency="RUB",
+1
View File
@@ -361,6 +361,7 @@ class MiniAppPaymentMethodsRequest(BaseModel):
class MiniAppPaymentMethod(BaseModel):
id: str
name: Optional[str] = None
icon: Optional[str] = None
requires_amount: bool = False
currency: str = "RUB"
+8 -3
View File
@@ -248,7 +248,7 @@ async def main():
if settings.TRIBUTE_ENABLED:
enabled_services.append("Tribute")
if settings.is_mulenpay_enabled():
enabled_services.append("Mulen Pay")
enabled_services.append(settings.get_mulenpay_display_name())
if settings.is_cryptobot_enabled():
enabled_services.append("CryptoBot")
@@ -257,7 +257,11 @@ async def main():
stage.log(f"Активированы: {', '.join(enabled_services)}")
stage.success("Webhook сервера запущены")
else:
stage.skip("Tribute, Mulen Pay и CryptoBot отключены")
stage.skip(
"Tribute, "
f"{settings.get_mulenpay_display_name()}"
" и CryptoBot отключены"
)
async with timeline.stage(
"YooKassa webhook",
@@ -372,7 +376,8 @@ async def main():
)
if settings.is_mulenpay_enabled():
webhook_lines.append(
f"Mulen Pay: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.MULENPAY_WEBHOOK_PATH}"
f"{settings.get_mulenpay_display_name()}: "
f"{settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.MULENPAY_WEBHOOK_PATH}"
)
if settings.is_cryptobot_enabled():
webhook_lines.append(