mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-26 06:12:00 +00:00
Fix Pal24 payment flows and webhook handling
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
@@ -87,6 +88,7 @@ async def update_pal24_payment_status(
|
||||
status: str,
|
||||
is_active: Optional[bool] = None,
|
||||
is_paid: Optional[bool] = None,
|
||||
paid_at: Optional[datetime] = None,
|
||||
payment_id: Optional[str] = None,
|
||||
payment_status: Optional[str] = None,
|
||||
payment_method: Optional[str] = None,
|
||||
@@ -103,6 +105,8 @@ async def update_pal24_payment_status(
|
||||
update_values["is_active"] = is_active
|
||||
if is_paid is not None:
|
||||
update_values["is_paid"] = is_paid
|
||||
if paid_at is not None:
|
||||
update_values["paid_at"] = paid_at
|
||||
if payment_id is not None:
|
||||
update_values["payment_id"] = payment_id
|
||||
if payment_status is not None:
|
||||
|
||||
17
app/external/pal24_webhook.py
vendored
17
app/external/pal24_webhook.py
vendored
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
@@ -13,7 +14,7 @@ from flask import Flask, jsonify, request
|
||||
from werkzeug.serving import make_server
|
||||
|
||||
from app.config import settings
|
||||
from app.database.database import get_db
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from app.services.pal24_service import Pal24Service, Pal24APIError
|
||||
from app.services.payment_service import PaymentService
|
||||
|
||||
@@ -56,6 +57,8 @@ def create_pal24_flask_app(
|
||||
logger.error("Pal24 webhook получен, но сервис не настроен")
|
||||
return jsonify({"status": "error", "reason": "service_not_configured"}), 503
|
||||
|
||||
logger.debug("Получен Pal24 webhook: headers=%s", dict(request.headers))
|
||||
|
||||
payload = _normalize_payload()
|
||||
if not payload:
|
||||
logger.warning("Пустой Pal24 webhook")
|
||||
@@ -68,15 +71,19 @@ def create_pal24_flask_app(
|
||||
return jsonify({"status": "error", "reason": str(error)}), 400
|
||||
|
||||
async def process() -> bool:
|
||||
async for db in get_db():
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
return await payment_service.process_pal24_postback(db, parsed_payload)
|
||||
finally:
|
||||
await db.close()
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
try:
|
||||
future = asyncio.run_coroutine_threadsafe(process(), loop)
|
||||
processed = future.result()
|
||||
processed = future.result(timeout=settings.PAL24_REQUEST_TIMEOUT)
|
||||
except FuturesTimeoutError:
|
||||
logger.error("Обработка Pal24 webhook превысила таймаут %sс", settings.PAL24_REQUEST_TIMEOUT)
|
||||
return jsonify({"status": "error", "reason": "timeout"}), 504
|
||||
except Exception as error: # pragma: no cover - defensive
|
||||
logger.exception("Критическая ошибка обработки Pal24 webhook: %s", error)
|
||||
return jsonify({"status": "error", "reason": "internal_error"}), 500
|
||||
|
||||
@@ -652,6 +652,11 @@ def register_balance_handlers(dp: Dispatcher):
|
||||
start_pal24_payment,
|
||||
F.data == "topup_pal24"
|
||||
)
|
||||
from .pal24 import handle_pal24_method_selection
|
||||
dp.callback_query.register(
|
||||
handle_pal24_method_selection,
|
||||
F.data.startswith("pal24_method_"),
|
||||
)
|
||||
|
||||
from .yookassa import check_yookassa_payment_status
|
||||
dp.callback_query.register(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import html
|
||||
import logging
|
||||
from aiogram import types
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.database import AsyncSessionLocal
|
||||
from app.database.models import User
|
||||
from app.keyboards.inline import get_back_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
@@ -15,81 +17,27 @@ from app.states import BalanceStates
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def start_pal24_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_pal24_enabled():
|
||||
await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
# Формируем текст сообщения в зависимости от доступных способов оплаты
|
||||
if settings.is_pal24_sbp_button_visible() and settings.is_pal24_card_button_visible():
|
||||
payment_methods_text = "СБП и банковской картой"
|
||||
elif settings.is_pal24_sbp_button_visible():
|
||||
payment_methods_text = "СБП"
|
||||
elif settings.is_pal24_card_button_visible():
|
||||
payment_methods_text = "банковской картой"
|
||||
else:
|
||||
# Если обе кнопки отключены, используем общий текст
|
||||
payment_methods_text = "доступными способами"
|
||||
|
||||
message_text = texts.t(
|
||||
"PAL24_TOPUP_PROMPT",
|
||||
(
|
||||
f"🏦 <b>Оплата через PayPalych ({payment_methods_text})</b>\n\n"
|
||||
"Введите сумму для пополнения от 100 до 1 000 000 ₽.\n"
|
||||
f"Оплата проходит через PayPalych ({payment_methods_text})."
|
||||
),
|
||||
)
|
||||
|
||||
keyboard = get_back_keyboard(db_user.language)
|
||||
|
||||
if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS:
|
||||
from .main import get_quick_amount_buttons
|
||||
quick_amount_buttons = get_quick_amount_buttons(db_user.language)
|
||||
if quick_amount_buttons:
|
||||
keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard
|
||||
|
||||
await callback.message.edit_text(
|
||||
message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await state.set_state(BalanceStates.waiting_for_amount)
|
||||
await state.update_data(payment_method="pal24")
|
||||
await callback.answer()
|
||||
def _get_available_pal24_methods() -> list[str]:
|
||||
methods: list[str] = []
|
||||
if settings.is_pal24_sbp_button_visible():
|
||||
methods.append("sbp")
|
||||
if settings.is_pal24_card_button_visible():
|
||||
methods.append("card")
|
||||
if not methods:
|
||||
methods.append("sbp")
|
||||
return methods
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_pal24_payment_amount(
|
||||
async def _send_pal24_payment_message(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
amount_kopeks: int,
|
||||
payment_method: str,
|
||||
state: FSMContext,
|
||||
):
|
||||
) -> None:
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_pal24_enabled():
|
||||
await message.answer("❌ Оплата через PayPalych временно недоступна")
|
||||
return
|
||||
|
||||
if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
|
||||
min_rubles = settings.PAL24_MIN_AMOUNT_KOPEKS / 100
|
||||
await message.answer(f"❌ Минимальная сумма для оплаты через PayPalych: {min_rubles:.0f} ₽")
|
||||
return
|
||||
|
||||
if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
|
||||
max_rubles = settings.PAL24_MAX_AMOUNT_KOPEKS / 100
|
||||
await message.answer(f"❌ Максимальная сумма для оплаты через PayPalych: {max_rubles:,.0f} ₽".replace(',', ' '))
|
||||
return
|
||||
|
||||
try:
|
||||
payment_service = PaymentService(message.bot)
|
||||
payment_result = await payment_service.create_pal24_payment(
|
||||
@@ -98,6 +46,7 @@ async def process_pal24_payment_amount(
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=settings.get_balance_payment_description(amount_kopeks),
|
||||
language=db_user.language,
|
||||
payment_method=payment_method,
|
||||
)
|
||||
|
||||
if not payment_result:
|
||||
@@ -262,14 +211,15 @@ async def process_pal24_payment_amount(
|
||||
await state.clear()
|
||||
|
||||
logger.info(
|
||||
"Создан PayPalych счет для пользователя %s: %s₽, ID: %s",
|
||||
"Создан PayPalych счет для пользователя %s: %s₽, ID: %s, метод: %s",
|
||||
db_user.telegram_id,
|
||||
amount_kopeks / 100,
|
||||
bill_id,
|
||||
payment_method,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания PayPalych платежа: {e}")
|
||||
except Exception as error:
|
||||
logger.error(f"Ошибка создания PayPalych платежа: {error}")
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"PAL24_PAYMENT_ERROR",
|
||||
@@ -278,6 +228,168 @@ async def process_pal24_payment_amount(
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
@error_handler
|
||||
async def start_pal24_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_pal24_enabled():
|
||||
await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
# Формируем текст сообщения в зависимости от доступных способов оплаты
|
||||
if settings.is_pal24_sbp_button_visible() and settings.is_pal24_card_button_visible():
|
||||
payment_methods_text = "СБП и банковской картой"
|
||||
elif settings.is_pal24_sbp_button_visible():
|
||||
payment_methods_text = "СБП"
|
||||
elif settings.is_pal24_card_button_visible():
|
||||
payment_methods_text = "банковской картой"
|
||||
else:
|
||||
# Если обе кнопки отключены, используем общий текст
|
||||
payment_methods_text = "доступными способами"
|
||||
|
||||
message_text = texts.t(
|
||||
"PAL24_TOPUP_PROMPT",
|
||||
(
|
||||
f"🏦 <b>Оплата через PayPalych ({payment_methods_text})</b>\n\n"
|
||||
"Введите сумму для пополнения от 100 до 1 000 000 ₽.\n"
|
||||
f"Оплата проходит через PayPalych ({payment_methods_text})."
|
||||
),
|
||||
)
|
||||
|
||||
keyboard = get_back_keyboard(db_user.language)
|
||||
|
||||
if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS:
|
||||
from .main import get_quick_amount_buttons
|
||||
quick_amount_buttons = get_quick_amount_buttons(db_user.language)
|
||||
if quick_amount_buttons:
|
||||
keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard
|
||||
|
||||
await callback.message.edit_text(
|
||||
message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await state.set_state(BalanceStates.waiting_for_amount)
|
||||
await state.update_data(payment_method="pal24")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@error_handler
|
||||
async def process_pal24_payment_amount(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
amount_kopeks: int,
|
||||
state: FSMContext,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_pal24_enabled():
|
||||
await message.answer("❌ Оплата через PayPalych временно недоступна")
|
||||
return
|
||||
|
||||
if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
|
||||
min_rubles = settings.PAL24_MIN_AMOUNT_KOPEKS / 100
|
||||
await message.answer(f"❌ Минимальная сумма для оплаты через PayPalych: {min_rubles:.0f} ₽")
|
||||
return
|
||||
|
||||
if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
|
||||
max_rubles = settings.PAL24_MAX_AMOUNT_KOPEKS / 100
|
||||
await message.answer(
|
||||
f"❌ Максимальная сумма для оплаты через PayPalych: {max_rubles:,.0f} ₽".replace(',', ' ')
|
||||
)
|
||||
return
|
||||
|
||||
available_methods = _get_available_pal24_methods()
|
||||
|
||||
if len(available_methods) == 1:
|
||||
await _send_pal24_payment_message(
|
||||
message,
|
||||
db_user,
|
||||
db,
|
||||
amount_kopeks,
|
||||
available_methods[0],
|
||||
state,
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(pal24_amount_kopeks=amount_kopeks)
|
||||
await state.set_state(BalanceStates.waiting_for_pal24_method)
|
||||
|
||||
method_buttons: list[list[types.InlineKeyboardButton]] = []
|
||||
if "sbp" in available_methods:
|
||||
method_buttons.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=settings.get_pal24_sbp_button_text(
|
||||
texts.t("PAL24_SBP_PAY_BUTTON", "🏦 Оплатить через PayPalych (СБП)")
|
||||
),
|
||||
callback_data="pal24_method_sbp",
|
||||
)
|
||||
]
|
||||
)
|
||||
if "card" in available_methods:
|
||||
method_buttons.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=settings.get_pal24_card_button_text(
|
||||
texts.t("PAL24_CARD_PAY_BUTTON", "💳 Оплатить банковской картой (PayPalych)")
|
||||
),
|
||||
callback_data="pal24_method_card",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
method_buttons.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")])
|
||||
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"PAL24_SELECT_PAYMENT_METHOD",
|
||||
"Выберите способ оплаты PayPalych:",
|
||||
),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=method_buttons),
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def handle_pal24_method_selection(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext,
|
||||
):
|
||||
data = await state.get_data()
|
||||
amount_kopeks = data.get("pal24_amount_kopeks")
|
||||
if not amount_kopeks:
|
||||
texts = get_texts(db_user.language)
|
||||
await callback.answer(
|
||||
texts.t(
|
||||
"PAL24_PAYMENT_ERROR",
|
||||
"❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.",
|
||||
),
|
||||
show_alert=True,
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
method = "sbp" if callback.data.endswith("_sbp") else "card"
|
||||
|
||||
await callback.answer()
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await _send_pal24_payment_message(
|
||||
callback.message,
|
||||
db_user,
|
||||
db,
|
||||
int(amount_kopeks),
|
||||
method,
|
||||
state,
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
async def check_pal24_payment_status(
|
||||
@@ -353,11 +465,17 @@ async def check_pal24_payment_status(
|
||||
])
|
||||
|
||||
await callback.answer()
|
||||
await callback.message.edit_text(
|
||||
"\n".join(message_lines),
|
||||
reply_markup=keyboard,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"\n".join(message_lines),
|
||||
reply_markup=keyboard,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except TelegramBadRequest as error:
|
||||
if "message is not modified" in str(error).lower():
|
||||
await callback.answer(texts.t("CHECK_STATUS_NO_CHANGES", "Статус не изменился"))
|
||||
else:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки статуса PayPalych: {e}")
|
||||
|
||||
@@ -758,6 +758,7 @@
|
||||
"CHANNEL_SUBSCRIBE_REQUIRED_ALERT": "❌ You haven't joined the channel!",
|
||||
"CHANNEL_SUBSCRIBE_THANKS": "✅ Thanks for subscribing",
|
||||
"CHECK_STATUS_BUTTON": "📊 Check status",
|
||||
"CHECK_STATUS_NO_CHANGES": "Status has not changed",
|
||||
"CHOOSE_ANOTHER_DEVICE": "📱 Choose another device",
|
||||
"CLOSED_TICKETS": "🟢 Closed",
|
||||
"CLOSED_TICKETS_HEADER": "🟢 Closed tickets",
|
||||
@@ -953,6 +954,7 @@
|
||||
"PAL24_PAYMENT_INSTRUCTIONS": "🏦 <b>PayPalych (SBP) payment</b>\n\n💰 Amount: {amount}\n🆔 Invoice ID: {bill_id}\n\n📱 <b>How to pay:</b>\n1. Press ‘Pay with PayPalych (SBP)’\n2. Follow the system prompts\n3. Confirm the transfer\n4. Funds will be credited automatically\n\n❓ Need help? Contact {support}",
|
||||
"PAL24_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
|
||||
"PAL24_SBP_PAY_BUTTON": "🏦 Pay with PayPalych (SBP)",
|
||||
"PAL24_SELECT_PAYMENT_METHOD": "Choose a PayPalych payment method:",
|
||||
"PAL24_TOPUP_PROMPT": "🏦 <b>PayPalych (SBP) payment</b>\n\nEnter an amount between 100 and 1,000,000 ₽.\nThe payment is processed via the PayPalych Faster Payments System.",
|
||||
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Payment methods are temporarily unavailable",
|
||||
"PAYMENT_CARD_MULENPAY": "💳 Bank card (Mulen Pay)",
|
||||
@@ -1101,6 +1103,7 @@
|
||||
"RESET_ALL_DEVICES_BUTTON": "🔄 Reset all devices",
|
||||
"RESET_DEVICE_CONFIRM_BUTTON": "✅ Reset this device",
|
||||
"RESET_TRAFFIC_BUTTON": "🔄 Reset traffic",
|
||||
"NO_SAVED_SUBSCRIPTION_ORDER": "No pending subscription order was found.",
|
||||
"RETURN_TO_SUBSCRIPTION_CHECKOUT": "⬅️ Return to subscription checkout",
|
||||
"RULES_ACCEPT": "✅ I accept the rules",
|
||||
"RULES_ACCEPTED_PROCESSING": "✅ Rules accepted! Completing registration...",
|
||||
|
||||
@@ -758,6 +758,7 @@
|
||||
"CHANNEL_SUBSCRIBE_REQUIRED_ALERT": "❌ Вы не подписались на канал!",
|
||||
"CHANNEL_SUBSCRIBE_THANKS": "✅ Спасибо за подписку",
|
||||
"CHECK_STATUS_BUTTON": "📊 Проверить статус",
|
||||
"CHECK_STATUS_NO_CHANGES": "Статус не изменился",
|
||||
"CHOOSE_ANOTHER_DEVICE": "📱 Выбрать другое устройство",
|
||||
"CLOSED_TICKETS": "🟢 Закрытые",
|
||||
"CLOSED_TICKETS_HEADER": "🟢 Закрытые тикеты",
|
||||
@@ -953,6 +954,7 @@
|
||||
"PAL24_PAYMENT_INSTRUCTIONS": "🏦 <b>Оплата через PayPalych (СБП)</b>\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 <b>Инструкция:</b>\n1. Нажмите кнопку ‘Оплатить через PayPalych (СБП)’\n2. Следуйте подсказкам платежной системы\n3. Подтвердите перевод\n4. Средства зачислятся автоматически\n\n❓ Если возникнут проблемы, обратитесь в {support}",
|
||||
"PAL24_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
|
||||
"PAL24_SBP_PAY_BUTTON": "🏦 Оплатить через PayPalych (СБП)",
|
||||
"PAL24_SELECT_PAYMENT_METHOD": "Выберите способ оплаты PayPalych:",
|
||||
"PAL24_TOPUP_PROMPT": "🏦 <b>Оплата через PayPalych (СБП)</b>\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.",
|
||||
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Способы оплаты временно недоступны",
|
||||
"PAYMENT_CARD_MULENPAY": "💳 Банковская карта (Mulen Pay)",
|
||||
@@ -1101,6 +1103,7 @@
|
||||
"RESET_ALL_DEVICES_BUTTON": "🔄 Сбросить все устройства",
|
||||
"RESET_DEVICE_CONFIRM_BUTTON": "✅ Да, сбросить это устройство",
|
||||
"RESET_TRAFFIC_BUTTON": "🔄 Сбросить трафик",
|
||||
"NO_SAVED_SUBSCRIPTION_ORDER": "❗️ Сохраненный заказ не найден.",
|
||||
"RETURN_TO_SUBSCRIPTION_CHECKOUT": "⬅️ Вернуться к оформлению подписки",
|
||||
"RULES_ACCEPT": "✅ Принимаю правила",
|
||||
"RULES_ACCEPTED_PROCESSING": "✅ Правила приняты! Завершаем регистрацию...",
|
||||
|
||||
@@ -63,7 +63,7 @@ class Pal24PaymentMixin:
|
||||
"language": language,
|
||||
}
|
||||
|
||||
normalized_payment_method = (payment_method or "SBP").upper()
|
||||
normalized_payment_method = self._normalize_payment_method(payment_method)
|
||||
|
||||
payment_module = import_module("app.services.payment_service")
|
||||
|
||||
@@ -147,6 +147,7 @@ class Pal24PaymentMixin:
|
||||
"description": description,
|
||||
"links": metadata_links,
|
||||
"raw_response": response,
|
||||
"selected_method": normalized_payment_method,
|
||||
}
|
||||
|
||||
payment = await payment_module.create_pal24_payment(
|
||||
@@ -186,6 +187,10 @@ class Pal24PaymentMixin:
|
||||
"payment_method": normalized_payment_method,
|
||||
"metadata_links": metadata_links,
|
||||
"status": payment_status,
|
||||
"sbp_url": transfer_url,
|
||||
"transfer_url": transfer_url,
|
||||
"link_page_url": link_page_url,
|
||||
"payment_url": primary_link,
|
||||
}
|
||||
|
||||
async def process_pal24_postback(
|
||||
@@ -240,7 +245,7 @@ class Pal24PaymentMixin:
|
||||
logger.info("Pal24 платеж %s уже обработан", payment.bill_id)
|
||||
return True
|
||||
|
||||
if status in {"PAID", "SUCCESS"}:
|
||||
if status in {"PAID", "SUCCESS", "OVERPAID"}:
|
||||
user = await payment_module.get_user_by_id(db, payment.user_id)
|
||||
if not user:
|
||||
logger.error(
|
||||
@@ -253,8 +258,17 @@ class Pal24PaymentMixin:
|
||||
db,
|
||||
payment,
|
||||
status=status,
|
||||
is_paid=True,
|
||||
paid_at=datetime.utcnow(),
|
||||
postback_payload=postback,
|
||||
payment_id=payment_id,
|
||||
payment_status=postback.get("Status") or status,
|
||||
payment_method=(
|
||||
postback.get("payment_method")
|
||||
or postback.get("PaymentMethod")
|
||||
or (payment.metadata_json or {}).get("selected_method")
|
||||
or getattr(payment, "payment_method", None)
|
||||
),
|
||||
)
|
||||
|
||||
if payment.transaction_id:
|
||||
@@ -407,8 +421,15 @@ class Pal24PaymentMixin:
|
||||
db,
|
||||
payment,
|
||||
status=status or "UNKNOWN",
|
||||
is_paid=False,
|
||||
postback_payload=postback,
|
||||
payment_id=payment_id,
|
||||
payment_status=postback.get("Status") or status,
|
||||
payment_method=(
|
||||
postback.get("payment_method")
|
||||
or postback.get("PaymentMethod")
|
||||
or getattr(payment, "payment_method", None)
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
"Обновили Pal24 платеж %s до статуса %s",
|
||||
@@ -446,12 +467,29 @@ class Pal24PaymentMixin:
|
||||
"bill", {}
|
||||
).get("status")
|
||||
|
||||
if remote_status and remote_status != payment.status:
|
||||
await payment_module.update_pal24_payment_status(
|
||||
db,
|
||||
payment,
|
||||
status=str(remote_status).upper(),
|
||||
)
|
||||
if remote_status:
|
||||
normalized_remote = str(remote_status).upper()
|
||||
if normalized_remote != payment.status:
|
||||
update_kwargs: Dict[str, Any] = {
|
||||
"status": normalized_remote,
|
||||
"payment_status": remote_status,
|
||||
}
|
||||
if normalized_remote in getattr(
|
||||
service, "BILL_SUCCESS_STATES", {"SUCCESS"}
|
||||
):
|
||||
update_kwargs["is_paid"] = True
|
||||
if not payment.paid_at:
|
||||
update_kwargs["paid_at"] = datetime.utcnow()
|
||||
elif normalized_remote in getattr(
|
||||
service, "BILL_FAILED_STATES", {"FAIL"}
|
||||
):
|
||||
update_kwargs["is_paid"] = False
|
||||
|
||||
await payment_module.update_pal24_payment_status(
|
||||
db,
|
||||
payment,
|
||||
**update_kwargs,
|
||||
)
|
||||
payment = await payment_module.get_pal24_payment_by_id(
|
||||
db, local_payment_id
|
||||
)
|
||||
@@ -471,3 +509,22 @@ class Pal24PaymentMixin:
|
||||
except Exception as error:
|
||||
logger.error("Ошибка получения статуса Pal24: %s", error, exc_info=True)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_payment_method(payment_method: Optional[str]) -> str:
|
||||
mapping = {
|
||||
"sbp": "sbp",
|
||||
"fast": "sbp",
|
||||
"fastpay": "sbp",
|
||||
"fast_payment": "sbp",
|
||||
"card": "card",
|
||||
"bank_card": "card",
|
||||
"bankcard": "card",
|
||||
"bank-card": "card",
|
||||
}
|
||||
|
||||
if not payment_method:
|
||||
return "sbp"
|
||||
|
||||
normalized = payment_method.strip().lower()
|
||||
return mapping.get(normalized, "sbp")
|
||||
|
||||
@@ -21,6 +21,7 @@ class SubscriptionStates(StatesGroup):
|
||||
|
||||
class BalanceStates(StatesGroup):
|
||||
waiting_for_amount = State()
|
||||
waiting_for_pal24_method = State()
|
||||
waiting_for_stars_payment = State()
|
||||
waiting_for_support_request = State()
|
||||
|
||||
|
||||
@@ -958,7 +958,7 @@ async def create_payment_link(
|
||||
option = (payload.payment_option or "").strip().lower()
|
||||
if option not in {"card", "sbp"}:
|
||||
option = "sbp"
|
||||
provider_method = "CARD" if option == "card" else "SBP"
|
||||
provider_method = "card" if option == "card" else "sbp"
|
||||
|
||||
payment_service = PaymentService()
|
||||
result = await payment_service.create_pal24_payment(
|
||||
@@ -974,7 +974,7 @@ async def create_payment_link(
|
||||
|
||||
preferred_urls: List[Optional[str]] = []
|
||||
if option == "sbp":
|
||||
preferred_urls.append(result.get("sbp_url"))
|
||||
preferred_urls.append(result.get("sbp_url") or result.get("transfer_url"))
|
||||
elif option == "card":
|
||||
preferred_urls.append(result.get("card_url"))
|
||||
preferred_urls.extend(
|
||||
@@ -998,7 +998,7 @@ async def create_payment_link(
|
||||
"bill_id": result.get("bill_id"),
|
||||
"order_id": result.get("order_id"),
|
||||
"payment_method": result.get("payment_method") or provider_method,
|
||||
"sbp_url": result.get("sbp_url"),
|
||||
"sbp_url": result.get("sbp_url") or result.get("transfer_url"),
|
||||
"card_url": result.get("card_url"),
|
||||
"link_url": result.get("link_url"),
|
||||
"link_page_url": result.get("link_page_url"),
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_create_bill_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
ttl_seconds=600,
|
||||
custom_payload={"extra": "value"},
|
||||
payer_email="user@example.com",
|
||||
payment_method="CARD",
|
||||
payment_method="card",
|
||||
)
|
||||
|
||||
assert result["bill_id"] == "BILL42"
|
||||
|
||||
@@ -101,7 +101,7 @@ async def test_create_pal24_payment_success(monkeypatch: pytest.MonkeyPatch) ->
|
||||
assert result is not None
|
||||
assert result["local_payment_id"] == 321
|
||||
assert result["bill_id"] == "BILL-1"
|
||||
assert result["payment_method"] == "CARD"
|
||||
assert result["payment_method"] == "card"
|
||||
assert result["link_url"] == "https://pal24/sbp"
|
||||
assert result["card_url"] == "https://pal24/card"
|
||||
assert stub.calls and stub.calls[0]["amount_kopeks"] == 50000
|
||||
|
||||
@@ -86,8 +86,8 @@ async def test_create_payment_link_pal24_uses_selected_option(monkeypatch):
|
||||
|
||||
assert response.payment_url == 'https://card'
|
||||
assert response.extra['selected_option'] == 'card'
|
||||
assert response.extra['payment_method'] == 'CARD'
|
||||
assert captured_calls and captured_calls[0]['payment_method'] == 'CARD'
|
||||
assert response.extra['payment_method'] == 'card'
|
||||
assert captured_calls and captured_calls[0]['payment_method'] == 'card'
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
@@ -265,7 +265,7 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
|
||||
transaction_id=777,
|
||||
bill_id='BILL99',
|
||||
order_id='ORD99',
|
||||
payment_method='SBP',
|
||||
payment_method='sbp',
|
||||
)
|
||||
|
||||
class StubPal24Service:
|
||||
@@ -297,7 +297,7 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
|
||||
assert result.extra['local_payment_id'] == 321
|
||||
assert result.extra['bill_id'] == 'BILL99'
|
||||
assert result.extra['order_id'] == 'ORD99'
|
||||
assert result.extra['payment_method'] == 'SBP'
|
||||
assert result.extra['payment_method'] == 'sbp'
|
||||
assert result.extra['payload'] == 'pal24_payload'
|
||||
assert result.extra['started_at'] == '2024-01-01T00:00:00Z'
|
||||
assert result.extra['remote_status'] == 'PAID'
|
||||
|
||||
Reference in New Issue
Block a user