diff --git a/app/database/crud/mulenpay.py b/app/database/crud/mulenpay.py
index 062dcaab..17648f4a 100644
--- a/app/database/crud/mulenpay.py
+++ b/app/database/crud/mulenpay.py
@@ -3,7 +3,6 @@ from datetime import datetime
from typing import Optional
from sqlalchemy import select
-from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -91,7 +90,6 @@ async def update_mulenpay_payment_status(
paid_at: Optional[datetime] = None,
callback_payload: Optional[dict] = None,
mulen_payment_id: Optional[int] = None,
- metadata: Optional[dict] = None,
) -> MulenPayPayment:
payment.status = status
if is_paid is not None:
@@ -102,8 +100,6 @@ async def update_mulenpay_payment_status(
payment.callback_payload = callback_payload
if mulen_payment_id is not None and not payment.mulen_payment_id:
payment.mulen_payment_id = mulen_payment_id
- if metadata is not None:
- payment.metadata_json = metadata
payment.updated_at = datetime.utcnow()
await db.commit()
@@ -111,19 +107,6 @@ async def update_mulenpay_payment_status(
return payment
-async def update_mulenpay_payment_metadata(
- db: AsyncSession,
- *,
- payment: MulenPayPayment,
- metadata: dict,
-) -> MulenPayPayment:
- payment.metadata_json = metadata
- payment.updated_at = datetime.utcnow()
- await db.commit()
- await db.refresh(payment)
- return payment
-
-
async def link_mulenpay_payment_to_transaction(
db: AsyncSession,
*,
diff --git a/app/database/crud/pal24.py b/app/database/crud/pal24.py
index 35cffa61..a88c46b4 100644
--- a/app/database/crud/pal24.py
+++ b/app/database/crud/pal24.py
@@ -96,7 +96,6 @@ async def update_pal24_payment_status(
balance_currency: Optional[str] = None,
payer_account: Optional[str] = None,
callback_payload: Optional[Dict[str, Any]] = None,
- metadata: Optional[Dict[str, Any]] = None,
) -> Pal24Payment:
update_values: Dict[str, Any] = {
"status": status,
@@ -122,8 +121,6 @@ async def update_pal24_payment_status(
update_values["payer_account"] = payer_account
if callback_payload is not None:
update_values["callback_payload"] = callback_payload
- if metadata is not None:
- update_values["metadata_json"] = metadata
update_values["last_status"] = status
diff --git a/app/external/yookassa_webhook.py b/app/external/yookassa_webhook.py
index 6ea1811d..0c6485b5 100644
--- a/app/external/yookassa_webhook.py
+++ b/app/external/yookassa_webhook.py
@@ -254,16 +254,16 @@ class YooKassaWebhookHandler:
logger.info(f"📊 Обработка webhook YooKassa: {webhook_data.get('event', 'unknown_event')}")
logger.debug(f"🔍 Полные данные webhook: {webhook_data}")
- event_type = webhook_data.get("event")
- if not event_type:
- logger.warning("⚠️ Webhook YooKassa без типа события")
- return web.Response(status=400, text="No event type")
-
# Извлекаем ID платежа из вебхука для предотвращения дублирования
yookassa_payment_id = webhook_data.get("object", {}).get("id")
if not yookassa_payment_id:
logger.warning("⚠️ Webhook YooKassa без ID платежа")
- return web.Response(status=400, text="No payment id")
+ return web.Response(status=400, text="No payment ID")
+
+ event_type = webhook_data.get("event")
+ if not event_type:
+ logger.warning("⚠️ Webhook YooKassa без типа события")
+ return web.Response(status=400, text="No event type")
if event_type not in YOOKASSA_ALLOWED_EVENTS:
logger.info(f"ℹ️ Игнорируем событие YooKassa: {event_type}")
@@ -274,10 +274,8 @@ class YooKassaWebhookHandler:
# Проверяем, не обрабатывается ли этот платеж уже (защита от дублирования)
from app.database.models import PaymentMethod
from app.database.crud.transaction import get_transaction_by_external_id
- existing_transaction = None
- if yookassa_payment_id and hasattr(db, "execute"):
- existing_transaction = await get_transaction_by_external_id(db, yookassa_payment_id, PaymentMethod.YOOKASSA)
-
+ existing_transaction = await get_transaction_by_external_id(db, yookassa_payment_id, PaymentMethod.YOOKASSA)
+
if existing_transaction and event_type == "payment.succeeded":
logger.info(f"ℹ️ Платеж YooKassa {yookassa_payment_id} уже был обработан. Пропускаем дублирующий вебхук.")
return web.Response(status=200, text="OK")
diff --git a/app/handlers/balance/heleket.py b/app/handlers/balance/heleket.py
index f97aa1b2..37ac4b53 100644
--- a/app/handlers/balance/heleket.py
+++ b/app/handlers/balance/heleket.py
@@ -1,10 +1,8 @@
import logging
-from datetime import datetime
from typing import Optional
from aiogram import types
from aiogram.fsm.context import FSMContext
-from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -68,11 +66,7 @@ async def start_heleket_payment(
)
await state.set_state(BalanceStates.waiting_for_amount)
- await state.update_data(
- payment_method="heleket",
- heleket_prompt_message_id=callback.message.message_id,
- heleket_prompt_chat_id=callback.message.chat.id,
- )
+ await state.update_data(payment_method="heleket")
await callback.answer()
@@ -187,52 +181,7 @@ async def process_heleket_payment_amount(
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")],
])
- state_data = await state.get_data()
- prompt_message_id = state_data.get("heleket_prompt_message_id")
- prompt_chat_id = state_data.get("heleket_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning("Не удалось удалить сообщение с суммой Heleket: %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы Heleket: %s",
- delete_error,
- )
-
- invoice_message = await message.answer(
- "\n".join(details), parse_mode="HTML", reply_markup=keyboard
- )
-
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_heleket_payment_by_id(db, result["local_payment_id"])
- if payment:
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await db.execute(
- update(payment.__class__)
- .where(payment.__class__.id == payment.id)
- .values(metadata_json=metadata, updated_at=datetime.utcnow())
- )
- await db.commit()
- except Exception as error: # pragma: no cover - diagnostics
- logger.warning("Не удалось сохранить сообщение Heleket: %s", error)
-
- await state.update_data(
- heleket_invoice_message_id=invoice_message.message_id,
- heleket_invoice_chat_id=invoice_message.chat.id,
- )
-
+ await message.answer("\n".join(details), parse_mode="HTML", reply_markup=keyboard)
await state.clear()
diff --git a/app/handlers/balance/mulenpay.py b/app/handlers/balance/mulenpay.py
index f345abeb..840ac469 100644
--- a/app/handlers/balance/mulenpay.py
+++ b/app/handlers/balance/mulenpay.py
@@ -59,11 +59,7 @@ async def start_mulenpay_payment(
)
await state.set_state(BalanceStates.waiting_for_amount)
- await state.update_data(
- payment_method="mulenpay",
- mulenpay_prompt_message_id=callback.message.message_id,
- mulenpay_prompt_chat_id=callback.message.chat.id,
- )
+ await state.update_data(payment_method="mulenpay")
await callback.answer()
@@ -97,26 +93,6 @@ async def process_mulenpay_payment_amount(
amount_rubles = amount_kopeks / 100
- state_data = await state.get_data()
- prompt_message_id = state_data.get("mulenpay_prompt_message_id")
- prompt_chat_id = state_data.get("mulenpay_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - depends on bot permissions
- logger.warning(
- "Не удалось удалить сообщение с суммой MulenPay: %s", delete_error
- )
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы MulenPay: %s",
- delete_error,
- )
-
try:
payment_service = PaymentService(message.bot)
payment_result = await payment_service.create_mulenpay_payment(
@@ -187,39 +163,12 @@ async def process_mulenpay_payment_amount(
mulenpay_name_html=mulenpay_name_html,
)
- invoice_message = await message.answer(
+ await message.answer(
message_text,
reply_markup=keyboard,
parse_mode="HTML",
)
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_mulenpay_payment_by_local_id(
- db, local_payment_id
- )
- if payment:
- payment_metadata = dict(
- getattr(payment, "metadata_json", {}) or {}
- )
- payment_metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await payment_module.update_mulenpay_payment_metadata(
- db,
- payment=payment,
- metadata=payment_metadata,
- )
- except Exception as error: # pragma: no cover - diagnostic logging only
- logger.warning("Не удалось сохранить данные сообщения MulenPay: %s", error)
-
- await state.update_data(
- mulenpay_invoice_message_id=invoice_message.message_id,
- mulenpay_invoice_chat_id=invoice_message.chat.id,
- )
-
await state.clear()
logger.info(
diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py
index 28eda7f9..0a66b20f 100644
--- a/app/handlers/balance/pal24.py
+++ b/app/handlers/balance/pal24.py
@@ -1,12 +1,10 @@
import html
import logging
-from datetime import datetime
from typing import Any, Optional
from aiogram import types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
-from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -206,36 +204,12 @@ async def _send_pal24_payment_message(
support=settings.get_support_contact_display_html(),
)
- invoice_message = await message.answer(
+ await message.answer(
message_text,
reply_markup=keyboard,
parse_mode="HTML",
)
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_pal24_payment_by_id(db, local_payment_id)
- if payment:
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await db.execute(
- update(payment.__class__)
- .where(payment.__class__.id == payment.id)
- .values(metadata_json=metadata, updated_at=datetime.utcnow())
- )
- await db.commit()
- except Exception as error: # pragma: no cover - diagnostics
- logger.warning("Не удалось сохранить сообщение PayPalych: %s", error)
-
- await state.update_data(
- pal24_invoice_message_id=invoice_message.message_id,
- pal24_invoice_chat_id=invoice_message.chat.id,
- )
-
await state.clear()
logger.info(
@@ -303,11 +277,7 @@ async def start_pal24_payment(
)
await state.set_state(BalanceStates.waiting_for_amount)
- await state.update_data(
- payment_method="pal24",
- pal24_prompt_message_id=callback.message.message_id,
- pal24_prompt_chat_id=callback.message.chat.id,
- )
+ await state.update_data(payment_method="pal24")
await callback.answer()
@@ -339,24 +309,6 @@ async def process_pal24_payment_amount(
available_methods = _get_available_pal24_methods()
- state_data = await state.get_data()
- prompt_message_id = state_data.get("pal24_prompt_message_id")
- prompt_chat_id = state_data.get("pal24_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning("Не удалось удалить сообщение с суммой PayPalych: %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы PayPalych: %s",
- delete_error,
- )
-
if len(available_methods) == 1:
await _send_pal24_payment_message(
message,
diff --git a/app/handlers/balance/platega.py b/app/handlers/balance/platega.py
index def18f65..40e8c45f 100644
--- a/app/handlers/balance/platega.py
+++ b/app/handlers/balance/platega.py
@@ -98,10 +98,6 @@ async def _prompt_amount(
)
await state.set_state(BalanceStates.waiting_for_amount)
- await state.update_data(
- platega_prompt_message_id=message.message_id,
- platega_prompt_chat_id=message.chat.id,
- )
@error_handler
@@ -304,25 +300,7 @@ async def process_platega_payment_amount(
),
)
- state_data = await state.get_data()
- prompt_message_id = state_data.get("platega_prompt_message_id")
- prompt_chat_id = state_data.get("platega_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning("Не удалось удалить сообщение с суммой Platega: %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы Platega: %s",
- delete_error,
- )
-
- invoice_message = await message.answer(
+ await message.answer(
instructions_template.format(
method=method_title,
amount=settings.format_price(amount_kopeks),
@@ -333,29 +311,6 @@ async def process_platega_payment_amount(
parse_mode="HTML",
)
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_platega_payment_by_id(db, local_payment_id)
- if payment:
- payment_metadata = dict(getattr(payment, "metadata_json", {}) or {})
- payment_metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await payment_module.update_platega_payment(
- db,
- payment=payment,
- metadata=payment_metadata,
- )
- except Exception as error: # pragma: no cover - диагностический лог
- logger.warning("Не удалось сохранить данные сообщения Platega: %s", error)
-
- await state.update_data(
- platega_invoice_message_id=invoice_message.message_id,
- platega_invoice_chat_id=invoice_message.chat.id,
- )
-
await state.clear()
diff --git a/app/handlers/balance/stars.py b/app/handlers/balance/stars.py
index a8cac0f8..0dcf0031 100644
--- a/app/handlers/balance/stars.py
+++ b/app/handlers/balance/stars.py
@@ -1,10 +1,11 @@
import logging
from aiogram import types
from aiogram.fsm.context import FSMContext
+from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
-from app.keyboards.inline import get_back_keyboard
+from app.keyboards.inline import get_back_keyboard, get_payment_methods_keyboard
from app.localization.texts import get_texts
from app.services.payment_service import PaymentService
from app.states import BalanceStates
@@ -21,11 +22,11 @@ async def start_stars_payment(
state: FSMContext
):
texts = get_texts(db_user.language)
-
+
if not settings.TELEGRAM_STARS_ENABLED:
await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True)
return
-
+
# Формируем текст сообщения в зависимости от настройки
if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS:
message_text = (
@@ -34,10 +35,10 @@ async def start_stars_payment(
)
else:
message_text = texts.TOP_UP_AMOUNT
-
+
# Создаем клавиатуру
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
@@ -45,17 +46,12 @@ async def start_stars_payment(
if quick_amount_buttons:
# Вставляем кнопки быстрого выбора перед кнопкой "Назад"
keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard
-
+
await callback.message.edit_text(
message_text,
reply_markup=keyboard
)
-
- await state.update_data(
- stars_prompt_message_id=callback.message.message_id,
- stars_prompt_chat_id=callback.message.chat.id,
- )
-
+
await state.set_state(BalanceStates.waiting_for_amount)
await state.update_data(payment_method="stars")
await callback.answer()
@@ -69,48 +65,29 @@ async def process_stars_payment_amount(
state: FSMContext
):
texts = get_texts(db_user.language)
-
+
if not settings.TELEGRAM_STARS_ENABLED:
await message.answer("⚠️ Оплата Stars временно недоступна")
return
-
+
try:
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
- stars_rate = settings.get_stars_rate()
-
+ stars_rate = settings.get_stars_rate()
+
payment_service = PaymentService(message.bot)
invoice_link = await payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}",
payload=f"balance_{db_user.id}_{amount_kopeks}"
)
-
+
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)],
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
])
-
- state_data = await state.get_data()
-
- prompt_message_id = state_data.get("stars_prompt_message_id")
- prompt_chat_id = state_data.get("stars_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning("Не удалось удалить сообщение с суммой Stars: %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы Stars: %s",
- delete_error,
- )
-
- invoice_message = await message.answer(
+
+ await message.answer(
f"⭐ Оплата через Telegram Stars\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"⭐ К оплате: {stars_amount} звезд\n"
@@ -119,14 +96,9 @@ async def process_stars_payment_amount(
reply_markup=keyboard,
parse_mode="HTML"
)
-
- await state.update_data(
- stars_invoice_message_id=invoice_message.message_id,
- stars_invoice_chat_id=invoice_message.chat.id,
- )
-
- await state.set_state(None)
-
+
+ await state.clear()
+
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
- await message.answer("⚠️ Ошибка создания платежа")
+ await message.answer("⚠️ Ошибка создания платежа")
\ No newline at end of file
diff --git a/app/handlers/balance/tribute.py b/app/handlers/balance/tribute.py
index f96538a5..e6d24015 100644
--- a/app/handlers/balance/tribute.py
+++ b/app/handlers/balance/tribute.py
@@ -13,55 +13,47 @@ logger = logging.getLogger(__name__)
@error_handler
async def start_tribute_payment(
callback: types.CallbackQuery,
- db_user: User,
+ db_user: User
):
texts = get_texts(db_user.language)
-
+
if not settings.TRIBUTE_ENABLED:
await callback.answer("❌ Оплата картой временно недоступна", show_alert=True)
return
-
+
try:
from app.services.tribute_service import TributeService
-
+
tribute_service = TributeService(callback.bot)
payment_url = await tribute_service.create_payment_link(
user_id=db_user.telegram_id,
amount_kopeks=0,
- description="Пополнение баланса VPN",
+ description="Пополнение баланса VPN"
)
-
+
if not payment_url:
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
return
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)],
- [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")],
- ]
- )
-
+
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
+ [types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)],
+ [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
+ ])
+
await callback.message.edit_text(
- f"💳 Пополнение банковской картой\n\n",
- f"• Введите любую сумму от 100₽\n",
- f"• Безопасная оплата через Tribute\n",
- f"• Мгновенное зачисление на баланс\n",
- f"• Принимаем карты Visa, MasterCard, МИР\n\n",
- f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n",
+ f"💳 Пополнение банковской картой\n\n"
+ f"• Введите любую сумму от 100₽\n"
+ f"• Безопасная оплата через Tribute\n"
+ f"• Мгновенное зачисление на баланс\n"
+ f"• Принимаем карты Visa, MasterCard, МИР\n\n"
+ f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n"
f"Нажмите кнопку для перехода к оплате:",
reply_markup=keyboard,
- parse_mode="HTML",
+ parse_mode="HTML"
)
-
- TributeService.remember_invoice_message(
- db_user.telegram_id,
- callback.message.chat.id,
- callback.message.message_id,
- )
-
+
except Exception as e:
logger.error(f"Ошибка создания Tribute платежа: {e}")
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
-
- await callback.answer()
+
+ await callback.answer()
\ No newline at end of file
diff --git a/app/handlers/balance/wata.py b/app/handlers/balance/wata.py
index fdd2327e..259d559e 100644
--- a/app/handlers/balance/wata.py
+++ b/app/handlers/balance/wata.py
@@ -1,10 +1,8 @@
import logging
-from datetime import datetime
from typing import Dict
from aiogram import types
from aiogram.fsm.context import FSMContext
-from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -58,11 +56,7 @@ async def start_wata_payment(
)
await state.set_state(BalanceStates.waiting_for_amount)
- await state.update_data(
- payment_method="wata",
- wata_prompt_message_id=callback.message.message_id,
- wata_prompt_chat_id=callback.message.chat.id,
- )
+ await state.update_data(payment_method="wata")
await callback.answer()
@@ -165,54 +159,12 @@ async def process_wata_payment_amount(
support=settings.get_support_contact_display_html(),
)
- state_data = await state.get_data()
- prompt_message_id = state_data.get("wata_prompt_message_id")
- prompt_chat_id = state_data.get("wata_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning("Не удалось удалить сообщение с суммой WATA: %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - diagnostic
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы WATA: %s",
- delete_error,
- )
-
- invoice_message = await message.answer(
+ await message.answer(
message_text,
reply_markup=keyboard,
parse_mode="HTML",
)
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_wata_payment_by_local_id(db, local_payment_id)
- if payment:
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await db.execute(
- update(payment.__class__)
- .where(payment.__class__.id == payment.id)
- .values(metadata_json=metadata, updated_at=datetime.utcnow())
- )
- await db.commit()
- except Exception as error: # pragma: no cover - diagnostics
- logger.warning("Не удалось сохранить сообщение WATA: %s", error)
-
- await state.update_data(
- wata_invoice_message_id=invoice_message.message_id,
- wata_invoice_chat_id=invoice_message.chat.id,
- )
-
await state.clear()
logger.info(
diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py
index fb6a345c..607f62bd 100644
--- a/app/handlers/balance/yookassa.py
+++ b/app/handlers/balance/yookassa.py
@@ -1,9 +1,6 @@
import logging
-from datetime import datetime
-
from aiogram import types
from aiogram.fsm.context import FSMContext
-from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -61,13 +58,9 @@ async def start_yookassa_payment(
reply_markup=keyboard,
parse_mode="HTML"
)
-
+
await state.set_state(BalanceStates.waiting_for_amount)
await state.update_data(payment_method="yookassa")
- await state.update_data(
- yookassa_prompt_message_id=callback.message.message_id,
- yookassa_prompt_chat_id=callback.message.chat.id,
- )
await callback.answer()
@@ -115,13 +108,9 @@ async def start_yookassa_sbp_payment(
reply_markup=keyboard,
parse_mode="HTML"
)
-
+
await state.set_state(BalanceStates.waiting_for_amount)
await state.update_data(payment_method="yookassa_sbp")
- await state.update_data(
- yookassa_prompt_message_id=callback.message.message_id,
- yookassa_prompt_chat_id=callback.message.chat.id,
- )
await callback.answer()
@@ -183,25 +172,7 @@ async def process_yookassa_payment_amount(
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
])
- state_data = await state.get_data()
- prompt_message_id = state_data.get("yookassa_prompt_message_id")
- prompt_chat_id = state_data.get("yookassa_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning("Не удалось удалить сообщение с суммой YooKassa: %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы YooKassa: %s",
- delete_error,
- )
-
- invoice_message = await message.answer(
+ await message.answer(
f"💳 Оплата банковской картой\n\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n"
@@ -216,34 +187,9 @@ async def process_yookassa_payment_amount(
reply_markup=keyboard,
parse_mode="HTML"
)
-
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_yookassa_payment_by_local_id(
- db, payment_result["local_payment_id"]
- )
- if payment:
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await db.execute(
- update(payment.__class__)
- .where(payment.__class__.id == payment.id)
- .values(metadata_json=metadata, updated_at=datetime.utcnow())
- )
- await db.commit()
- except Exception as error: # pragma: no cover - диагностический лог
- logger.warning("Не удалось сохранить сообщение YooKassa: %s", error)
-
- await state.update_data(
- yookassa_invoice_message_id=invoice_message.message_id,
- yookassa_invoice_chat_id=invoice_message.chat.id,
- )
-
+
await state.clear()
+
logger.info(f"Создан платеж YooKassa для пользователя {db_user.telegram_id}: "
f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}")
@@ -364,45 +310,27 @@ async def process_yookassa_sbp_payment_amount(
# Создаем клавиатуру с кнопками для оплаты по ссылке и проверки статуса
keyboard_buttons = []
-
+
# Добавляем кнопку оплаты, если доступна ссылка
if confirmation_url:
keyboard_buttons.append([types.InlineKeyboardButton(text="🔗 Перейти к оплате", url=confirmation_url)])
else:
# Если ссылка недоступна, предлагаем оплатить через ID платежа в приложении банка
keyboard_buttons.append([types.InlineKeyboardButton(text="📱 Оплатить в приложении банка", callback_data="temp_disabled")])
-
+
# Добавляем общие кнопки
keyboard_buttons.append([types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")])
keyboard_buttons.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")])
-
+
keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
-
- state_data = await state.get_data()
- prompt_message_id = state_data.get("yookassa_prompt_message_id")
- prompt_chat_id = state_data.get("yookassa_prompt_chat_id", message.chat.id)
-
- try:
- await message.delete()
- except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning("Не удалось удалить сообщение с суммой YooKassa (СБП): %s", delete_error)
-
- if prompt_message_id:
- try:
- await message.bot.delete_message(prompt_chat_id, prompt_message_id)
- except Exception as delete_error: # pragma: no cover - диагностический лог
- logger.warning(
- "Не удалось удалить сообщение с запросом суммы YooKassa (СБП): %s",
- delete_error,
- )
-
+
# Подготавливаем текст сообщения
message_text = (
f"🔗 Оплата через СБП\n\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n"
)
-
+
# Добавляем инструкции в зависимости от доступных способов оплаты
if not confirmation_url:
message_text += (
@@ -413,18 +341,18 @@ async def process_yookassa_sbp_payment_amount(
f"4. Подтвердите платеж в приложении банка\n"
f"5. Деньги поступят на баланс автоматически\n\n"
)
-
+
message_text += (
f"🔒 Оплата происходит через защищенную систему YooKassa\n"
f"✅ Принимаем СБП от всех банков-участников\n\n"
f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}"
)
-
+
# Отправляем сообщение с инструкциями и клавиатурой
# Если есть QR-код, отправляем его как медиа-сообщение
if qr_photo:
# Используем метод отправки медиа-группы или фото с описанием
- invoice_message = await message.answer_photo(
+ await message.answer_photo(
photo=qr_photo,
caption=message_text,
reply_markup=keyboard,
@@ -432,39 +360,12 @@ async def process_yookassa_sbp_payment_amount(
)
else:
# Если QR-код недоступен, отправляем обычное текстовое сообщение
- invoice_message = await message.answer(
+ await message.answer(
message_text,
reply_markup=keyboard,
parse_mode="HTML"
)
-
- try:
- from app.services import payment_service as payment_module
-
- payment = await payment_module.get_yookassa_payment_by_local_id(
- db, payment_result["local_payment_id"]
- )
- if payment:
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- metadata["invoice_message"] = {
- "chat_id": invoice_message.chat.id,
- "message_id": invoice_message.message_id,
- }
- await db.execute(
- update(payment.__class__)
- .where(payment.__class__.id == payment.id)
- .values(metadata_json=metadata, updated_at=datetime.utcnow())
- )
- await db.commit()
- except Exception as error: # pragma: no cover - диагностический лог
- logger.warning("Не удалось сохранить сообщение YooKassa (СБП): %s", error)
-
- await state.update_data(
- yookassa_invoice_message_id=invoice_message.message_id,
- yookassa_invoice_chat_id=invoice_message.chat.id,
- )
-
- await state.clear()
+
logger.info(f"Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: "
f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}")
diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py
index 48356450..4b49f51f 100644
--- a/app/handlers/stars_payments.py
+++ b/app/handlers/stars_payments.py
@@ -1,10 +1,10 @@
import logging
from decimal import Decimal, ROUND_HALF_UP
from aiogram import Dispatcher, types, F
-from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
+from app.database.models import User
from app.services.payment_service import PaymentService
from app.external.telegram_stars import TelegramStarsService
from app.database.crud.user import get_user_by_telegram_id
@@ -18,9 +18,7 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
texts = get_texts(DEFAULT_LANGUAGE)
try:
- logger.info(
- f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}"
- )
+ logger.info(f"📋 Pre-checkout query от {query.from_user.id}: {query.total_amount} XTR, payload: {query.invoice_payload}")
allowed_prefixes = ("balance_", "admin_stars_test_", "simple_sub_")
@@ -37,7 +35,6 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
try:
from app.database.database import get_db
-
async for db in get_db():
user = await get_user_by_telegram_id(db, query.from_user.id)
if not user:
@@ -80,7 +77,6 @@ async def handle_pre_checkout_query(query: types.PreCheckoutQuery):
async def handle_successful_payment(
message: types.Message,
db: AsyncSession,
- state: FSMContext,
**kwargs
):
texts = get_texts(DEFAULT_LANGUAGE)
@@ -110,27 +106,6 @@ async def handle_successful_payment(
return
payment_service = PaymentService(message.bot)
-
- state_data = await state.get_data()
- prompt_message_id = state_data.get("stars_prompt_message_id")
- prompt_chat_id = state_data.get("stars_prompt_chat_id", message.chat.id)
- invoice_message_id = state_data.get("stars_invoice_message_id")
- invoice_chat_id = state_data.get("stars_invoice_chat_id", message.chat.id)
-
- for chat_id, message_id, label in [
- (prompt_chat_id, prompt_message_id, "запрос суммы"),
- (invoice_chat_id, invoice_message_id, "инвойс Stars"),
- ]:
- if message_id:
- try:
- await message.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - зависит от прав бота
- logger.warning(
- "Не удалось удалить сообщение %s после оплаты Stars: %s",
- label,
- delete_error,
- )
-
success = await payment_service.process_stars_payment(
db=db,
user_id=user.id,
@@ -138,14 +113,7 @@ async def handle_successful_payment(
payload=payment.invoice_payload,
telegram_payment_charge_id=payment.telegram_payment_charge_id
)
-
- await state.update_data(
- stars_prompt_message_id=None,
- stars_prompt_chat_id=None,
- stars_invoice_message_id=None,
- stars_invoice_chat_id=None,
- )
-
+
if success:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount)
amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
@@ -204,15 +172,15 @@ async def handle_successful_payment(
def register_stars_handlers(dp: Dispatcher):
-
+
dp.pre_checkout_query.register(
handle_pre_checkout_query,
- F.currency == "XTR"
+ F.currency == "XTR"
)
-
+
dp.message.register(
handle_successful_payment,
F.successful_payment
)
-
+
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index c347cddf..dc474150 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -1023,6 +1023,7 @@
"PAL24_INSTRUCTION_FOLLOW": "{step}. Follow the payment page instructions",
"PAL24_PAYMENT_ERROR": "❌ Failed to create a PayPalych payment. Please try again later or contact support.",
"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_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": "🏦 PayPalych (SBP) payment\n\nEnter an amount between 100 and 1,000,000 ₽.\nThe payment is processed via the PayPalych Faster Payments System.",
@@ -1060,8 +1061,12 @@
"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": "🪙 Cryptocurrency",
+ "PAYMENT_METHOD_HELEKET_DESCRIPTION": "via Heleket",
+ "PAYMENT_METHOD_HELEKET_NAME": "🪙 Cryptocurrency (Heleket)",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via {mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 Bank card ({mulenpay_name})",
+ "PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System",
+ "PAYMENT_METHOD_PAL24_NAME": "🏦 SBP (PayPalych)",
"PAYMENT_METHOD_PLATEGA_DESCRIPTION": "via Platega (cards + SBP)",
"PAYMENT_METHOD_PLATEGA_NAME": "💳 Bank card (Platega)",
"PAYMENT_METHOD_STARS_DESCRIPTION": "fast and convenient",
@@ -1070,6 +1075,8 @@
"PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Support team",
"PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "via Tribute",
"PAYMENT_METHOD_TRIBUTE_NAME": "💳 Bank card",
+ "PAYMENT_METHOD_WATA_DESCRIPTION": "via WATA",
+ "PAYMENT_METHOD_WATA_NAME": "💳 Bank card (WATA)",
"PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "via YooKassa",
"PAYMENT_METHOD_YOOKASSA_NAME": "💳 Bank card",
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "via YooKassa Fast Payment System",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index a77f47fb..da8b89f9 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -1043,6 +1043,7 @@
"PAL24_INSTRUCTION_FOLLOW": "{step}. Следуйте подсказкам платёжной системы",
"PAL24_PAYMENT_ERROR": "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.",
"PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сумма: {amount}\n🆔 ID счета: {bill_id}\n\n📱 Инструкция:\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": "🏦 Оплата через PayPalych (СБП)\n\nВведите сумму для пополнения от 100 до 1 000 000 ₽.\nОплата проходит через систему быстрых платежей PayPalych.",
@@ -1080,8 +1081,12 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Криптовалюта",
+ "PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket",
+ "PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME": "💳 Банковская карта ({mulenpay_name})",
+ "PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей",
+ "PAYMENT_METHOD_PAL24_NAME": "🏦 СБП (PayPalych)",
"PAYMENT_METHOD_PLATEGA_DESCRIPTION": "через Platega (карты + СБП)",
"PAYMENT_METHOD_PLATEGA_NAME": "💳 Банковская карта (Platega)",
"PAYMENT_METHOD_STARS_DESCRIPTION": "быстро и удобно",
@@ -1090,6 +1095,8 @@
"PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через поддержку",
"PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute",
"PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банковская карта",
+ "PAYMENT_METHOD_WATA_DESCRIPTION": "через WATA",
+ "PAYMENT_METHOD_WATA_NAME": "💳 Банковская карта (WATA)",
"PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa",
"PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банковская карта",
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему быстрых платежей YooKassa",
diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json
index 68897290..4c8fb4b9 100644
--- a/app/localization/locales/ua.json
+++ b/app/localization/locales/ua.json
@@ -1039,11 +1039,12 @@
"PAL24_INSTRUCTION_BUTTON": "{step}. Натисніть кнопку «{button}»",
"PAL24_INSTRUCTION_COMPLETE": "{step}. Кошти зарахуються автоматично",
"PAL24_INSTRUCTION_CONFIRM": "{step}. Підтвердіть переказ",
- "PAL24_INSTRUCTION_FOLLOW": "{step}. Дотримуйтесь підказок платіжної системи",
- "PAL24_PAYMENT_ERROR": "❌ Помилка створення платежу 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_SELECT_PAYMENT_METHOD": "Оберіть спосіб оплати PayPalych:",
+ "PAL24_INSTRUCTION_FOLLOW": "{step}. Дотримуйтесь підказок платіжної системи",
+ "PAL24_PAYMENT_ERROR": "❌ Помилка створення платежу PayPalych. Спробуйте пізніше або зверніться до підтримки.",
+ "PAL24_PAYMENT_INSTRUCTIONS": "🏦 Оплата через PayPalych (СБП)\n\n💰 Сума: {amount}\n🆔 ID рахунку: {bill_id}\n\n📱 Інструкція:\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": "🏦 Оплата через PayPalych (СБП)\n\nВведіть суму для поповнення від 100 до 1 000 000 ₽.\nОплата проходить через систему швидких платежів PayPalych.",
"PAYMENTS_TEMPORARILY_UNAVAILABLE": "⚠️ Способи оплати тимчасово недоступні",
"PAYMENT_CARD_MULENPAY": "💳 Банківська картка ({mulenpay_name})",
@@ -1079,18 +1080,24 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ На даний момент автоматичні способи оплати тимчасово недоступні. Для поповнення балансу зверніться до техпідтримки.",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 Криптовалюта",
-"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}",
-"PAYMENT_METHOD_MULENPAY_NAME": "💳 Банківська картка ({mulenpay_name})",
-"PAYMENT_METHOD_PLATEGA_DESCRIPTION": "через Platega (картки + СБП)",
-"PAYMENT_METHOD_PLATEGA_NAME": "💳 Банківська картка (Platega)",
+ "PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket",
+ "PAYMENT_METHOD_HELEKET_NAME": "🪙 Криптовалюта (Heleket)",
+ "PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}",
+ "PAYMENT_METHOD_MULENPAY_NAME": "💳 Банківська картка ({mulenpay_name})",
+ "PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему швидких платежів",
+ "PAYMENT_METHOD_PAL24_NAME": "🏦 СБП (PayPalych)",
+ "PAYMENT_METHOD_PLATEGA_DESCRIPTION": "через Platega (картки + СБП)",
+ "PAYMENT_METHOD_PLATEGA_NAME": "💳 Банківська картка (Platega)",
"PAYMENT_METHOD_STARS_DESCRIPTION": "швидко та зручно",
"PAYMENT_METHOD_STARS_NAME": "⭐ Telegram Stars",
"PAYMENT_METHOD_SUPPORT_DESCRIPTION": "інші способи",
"PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через підтримку",
-"PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute",
-"PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банківська картка",
-"PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa",
-"PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банківська картка",
+ "PAYMENT_METHOD_TRIBUTE_DESCRIPTION": "через Tribute",
+ "PAYMENT_METHOD_TRIBUTE_NAME": "💳 Банківська картка",
+ "PAYMENT_METHOD_WATA_DESCRIPTION": "через WATA",
+ "PAYMENT_METHOD_WATA_NAME": "💳 Банківська картка (WATA)",
+ "PAYMENT_METHOD_YOOKASSA_DESCRIPTION": "через YooKassa",
+ "PAYMENT_METHOD_YOOKASSA_NAME": "💳 Банківська картка",
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему швидких платежів YooKassa",
"PAYMENT_METHOD_YOOKASSA_SBP_NAME": "🏦 СБП (YooKassa)",
"PAYMENT_HELEKET_MARKUP_LABEL": "Націнка провайдера",
diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json
index 3c0b906f..84210ac8 100644
--- a/app/localization/locales/zh.json
+++ b/app/localization/locales/zh.json
@@ -1042,6 +1042,7 @@
"PAL24_INSTRUCTION_FOLLOW":"{step}.按照支付系统提示操作",
"PAL24_PAYMENT_ERROR":"❌创建PayPalych付款失败。请稍后再试或联系支持。",
"PAL24_PAYMENT_INSTRUCTIONS":"🏦通过PayPalych(SBP)付款\n\n💰金额:{amount}\n🆔账单ID:{bill_id}\n\n📱说明:\n1.点击“通过PayPalych(SBP)付款”按钮\n2.按照支付系统提示操作\n3.确认转账\n4.资金将自动到账\n\n❓如果遇到问题,请联系{support}",
+"PAL24_PAY_BUTTON":"🏦通过PayPalych(SBP)付款",
"PAL24_SBP_PAY_BUTTON":"🏦通过PayPalych(SBP)付款",
"PAL24_SELECT_PAYMENT_METHOD":"请选择PayPalych支付方式:",
"PAL24_TOPUP_PROMPT":"🏦通过PayPalych(SBP)付款\n\n请输入充值金额,范围100至1000000₽。\n付款通过PayPalych快速支付系统进行。",
@@ -1079,8 +1080,12 @@
"PAYMENT_METHODS_UNAVAILABLE_ALERT":"⚠️目前自动支付方式暂时不可用。如需充值余额,请联系技术支持。",
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION":"通过CryptoBot",
"PAYMENT_METHOD_CRYPTOBOT_NAME":"🪙加密货币",
+"PAYMENT_METHOD_HELEKET_DESCRIPTION":"通过Heleket",
+"PAYMENT_METHOD_HELEKET_NAME":"🪙加密货币(Heleket)",
"PAYMENT_METHOD_MULENPAY_DESCRIPTION":"通过{mulenpay_name}",
"PAYMENT_METHOD_MULENPAY_NAME":"💳银行卡({mulenpay_name})",
+"PAYMENT_METHOD_PAL24_DESCRIPTION":"通过快速支付系统",
+"PAYMENT_METHOD_PAL24_NAME":"🏦SBP(PayPalych)",
"PAYMENT_METHOD_PLATEGA_DESCRIPTION":"通过Platega(银行卡+SBP)",
"PAYMENT_METHOD_PLATEGA_NAME":"💳银行卡(Platega)",
"PAYMENT_METHOD_STARS_DESCRIPTION":"快速便捷",
@@ -1089,6 +1094,8 @@
"PAYMENT_METHOD_SUPPORT_NAME":"🛠️通过支持",
"PAYMENT_METHOD_TRIBUTE_DESCRIPTION":"通过Tribute",
"PAYMENT_METHOD_TRIBUTE_NAME":"💳银行卡",
+"PAYMENT_METHOD_WATA_DESCRIPTION":"通过WATA",
+"PAYMENT_METHOD_WATA_NAME":"💳银行卡(WATA)",
"PAYMENT_METHOD_YOOKASSA_DESCRIPTION":"通过YooKassa",
"PAYMENT_METHOD_YOOKASSA_NAME":"💳银行卡",
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION":"通过YooKassa快速支付系统",
diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py
index 27d24a2b..c032f990 100644
--- a/app/services/payment/heleket.py
+++ b/app/services/payment/heleket.py
@@ -255,42 +255,6 @@ class HeleketPaymentMixin:
if updated_payment is None:
return None
- metadata = dict(getattr(updated_payment, "metadata_json", {}) or {})
- invoice_message = metadata.get("invoice_message") or {}
- invoice_message_removed = False
-
- if getattr(self, "bot", None) and invoice_message:
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if chat_id and message_id:
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - depends on rights
- logger.warning(
- "Не удалось удалить счёт Heleket %s: %s",
- message_id,
- delete_error,
- )
- else:
- metadata.pop("invoice_message", None)
- invoice_message_removed = True
-
- if invoice_message_removed:
- try:
- from app.database.crud import heleket as heleket_crud
-
- await heleket_crud.update_heleket_payment(
- db,
- updated_payment.uuid,
- metadata=metadata,
- )
- updated_payment.metadata_json = metadata
- except Exception as error: # pragma: no cover - diagnostics
- logger.warning(
- "Не удалось обновить метаданные Heleket после удаления счёта: %s",
- error,
- )
-
if updated_payment.transaction_id:
logger.info(
"Heleket платеж %s уже связан с транзакцией %s",
diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py
index 27dea36d..7b4ffcf3 100644
--- a/app/services/payment/mulenpay.py
+++ b/app/services/payment/mulenpay.py
@@ -182,43 +182,7 @@ class MulenPayPaymentMixin:
)
return False
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- invoice_message = metadata.get("invoice_message") or {}
-
- invoice_message_removed = False
-
- if getattr(self, "bot", None):
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if chat_id and message_id:
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning(
- "Не удалось удалить %s счёт %s: %s",
- display_name,
- message_id,
- delete_error,
- )
- else:
- metadata.pop("invoice_message", None)
- invoice_message_removed = True
-
if payment.is_paid:
- if invoice_message_removed:
- try:
- await payment_module.update_mulenpay_payment_metadata(
- db,
- payment=payment,
- metadata=metadata,
- )
- except Exception as error: # pragma: no cover - diagnostics
- logger.warning(
- "Не удалось обновить метаданные %s после удаления счёта: %s",
- display_name,
- error,
- )
-
logger.info(
"%s платеж %s уже обработан, игнорируем повторный callback",
display_name,
@@ -233,7 +197,6 @@ class MulenPayPaymentMixin:
status="success",
callback_payload=callback_data,
mulen_payment_id=mulen_payment_id_int,
- metadata=metadata,
)
if payment.transaction_id:
diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py
index 460e0e31..6dbe3dc5 100644
--- a/app/services/payment/pal24.py
+++ b/app/services/payment/pal24.py
@@ -333,41 +333,6 @@ class Pal24PaymentMixin:
payment_module = import_module("app.services.payment_service")
- metadata = dict(getattr(payment, "metadata_json", {}) or {})
- invoice_message = metadata.get("invoice_message") or {}
- invoice_message_removed = False
-
- if getattr(self, "bot", None) and invoice_message:
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if chat_id and message_id:
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - depends on rights
- logger.warning(
- "Не удалось удалить счёт PayPalych %s: %s",
- message_id,
- delete_error,
- )
- else:
- metadata.pop("invoice_message", None)
- invoice_message_removed = True
-
- if invoice_message_removed:
- try:
- await payment_module.update_pal24_payment_status(
- db,
- payment,
- status=payment.status,
- metadata=metadata,
- )
- payment.metadata_json = metadata
- except Exception as error: # pragma: no cover - diagnostics
- logger.warning(
- "Не удалось обновить метаданные PayPalych после удаления счёта: %s",
- error,
- )
-
if payment.transaction_id:
logger.info(
"Pal24 платеж %s уже привязан к транзакции (trigger=%s)",
diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py
index 2d08997a..fc7fe4d8 100644
--- a/app/services/payment/platega.py
+++ b/app/services/payment/platega.py
@@ -306,22 +306,6 @@ class PlategaPaymentMixin:
metadata = dict(getattr(payment, "metadata_json", {}) or {})
balance_already_credited = bool(metadata.get("balance_credited"))
- invoice_message = metadata.get("invoice_message") or {}
- if getattr(self, "bot", None):
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if chat_id and message_id:
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning(
- "Не удалось удалить Platega счёт %s: %s",
- message_id,
- delete_error,
- )
- else:
- metadata.pop("invoice_message", None)
-
if payment.transaction_id:
logger.info(
"Platega платеж %s уже связан с транзакцией %s",
diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py
index 3f1022d7..b7fd5942 100644
--- a/app/services/payment/stars.py
+++ b/app/services/payment/stars.py
@@ -515,6 +515,36 @@ class TelegramStarsMixin:
exc_info=True,
)
+ if getattr(self, "bot", None):
+ try:
+ keyboard = await self.build_topup_success_keyboard(user)
+
+ charge_id_short = (telegram_payment_charge_id or getattr(transaction, "external_id", ""))[:8]
+
+ await self.bot.send_message(
+ user.telegram_id,
+ (
+ "✅ Пополнение успешно!\n\n"
+ f"⭐ Звезд: {stars_amount}\n"
+ f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
+ "🦊 Способ: Telegram Stars\n"
+ f"🆔 Транзакция: {charge_id_short}...\n\n"
+ "Баланс пополнен автоматически!"
+ ),
+ parse_mode="HTML",
+ reply_markup=keyboard,
+ )
+ logger.info(
+ "✅ Отправлено уведомление пользователю %s о пополнении на %s",
+ user.telegram_id,
+ settings.format_price(amount_kopeks),
+ )
+ except Exception as error: # pragma: no cover - диагностический лог
+ logger.error(
+ "Ошибка отправки уведомления о пополнении Stars: %s",
+ error,
+ )
+
# Проверяем наличие сохраненной корзины для возврата к оформлению подписки
try:
from aiogram import types
diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py
index 7072d096..d8bc789a 100644
--- a/app/services/payment/wata.py
+++ b/app/services/payment/wata.py
@@ -415,25 +415,6 @@ class WataPaymentMixin:
if not paid_at and getattr(payment, "paid_at", None):
paid_at = payment.paid_at
existing_metadata = dict(getattr(payment, "metadata_json", {}) or {})
-
- invoice_message = existing_metadata.get("invoice_message") or {}
- invoice_message_removed = False
- if getattr(self, "bot", None) and invoice_message:
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if chat_id and message_id:
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - depends on rights
- logger.warning(
- "Не удалось удалить счёт WATA %s: %s",
- message_id,
- delete_error,
- )
- else:
- invoice_message_removed = True
- existing_metadata.pop("invoice_message", None)
-
existing_metadata["transaction"] = transaction_payload
await payment_module.update_wata_payment_status(
diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py
index 465664a6..181ab94d 100644
--- a/app/services/payment/yookassa.py
+++ b/app/services/payment/yookassa.py
@@ -383,19 +383,11 @@ class YooKassaPaymentMixin:
payment_module = import_module("app.services.payment_service")
# Проверяем, не обрабатывается ли уже этот платеж (защита от дублирования)
- get_transaction_by_external_id = getattr(
- payment_module, "get_transaction_by_external_id", None
+ existing_transaction = await payment_module.get_transaction_by_external_id( # type: ignore[attr-defined]
+ db,
+ payment.yookassa_payment_id,
+ PaymentMethod.YOOKASSA,
)
- existing_transaction = None
- if get_transaction_by_external_id:
- try:
- existing_transaction = await get_transaction_by_external_id( # type: ignore[attr-defined]
- db,
- payment.yookassa_payment_id,
- PaymentMethod.YOOKASSA,
- )
- except AttributeError:
- logger.debug("get_transaction_by_external_id недоступен, пропускаем проверку дубликатов")
if existing_transaction:
# Если транзакция уже существует, просто завершаем обработку
@@ -445,22 +437,6 @@ class YooKassaPaymentMixin:
except Exception as parse_error:
logger.error(f"Ошибка парсинга метаданных платежа: {parse_error}")
- invoice_message = payment_metadata.get("invoice_message") or {}
- if getattr(self, "bot", None):
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if chat_id and message_id:
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as delete_error: # pragma: no cover - depends on bot rights
- logger.warning(
- "Не удалось удалить сообщение YooKassa %s: %s",
- message_id,
- delete_error,
- )
- else:
- payment_metadata.pop("invoice_message", None)
-
processing_completed = bool(payment_metadata.get("processing_completed"))
transaction = None
@@ -496,20 +472,11 @@ class YooKassaPaymentMixin:
)
if transaction is None:
- get_transaction_by_external_id = getattr(
- payment_module, "get_transaction_by_external_id", None
+ existing_transaction = await payment_module.get_transaction_by_external_id( # type: ignore[attr-defined]
+ db,
+ payment.yookassa_payment_id,
+ PaymentMethod.YOOKASSA,
)
- existing_transaction = None
-
- if get_transaction_by_external_id:
- try:
- existing_transaction = await get_transaction_by_external_id( # type: ignore[attr-defined]
- db,
- payment.yookassa_payment_id,
- PaymentMethod.YOOKASSA,
- )
- except AttributeError:
- logger.debug("get_transaction_by_external_id недоступен, пропускаем проверку дубликатов")
if existing_transaction:
# Если транзакция уже существует, пропускаем обработку
diff --git a/app/services/payment_service.py b/app/services/payment_service.py
index 949f7b23..9f417d73 100644
--- a/app/services/payment_service.py
+++ b/app/services/payment_service.py
@@ -112,11 +112,6 @@ async def update_mulenpay_payment_status(*args, **kwargs):
return await mulenpay_crud.update_mulenpay_payment_status(*args, **kwargs)
-async def update_mulenpay_payment_metadata(*args, **kwargs):
- mulenpay_crud = import_module("app.database.crud.mulenpay")
- return await mulenpay_crud.update_mulenpay_payment_metadata(*args, **kwargs)
-
-
async def link_mulenpay_payment_to_transaction(*args, **kwargs):
mulenpay_crud = import_module("app.database.crud.mulenpay")
return await mulenpay_crud.link_mulenpay_payment_to_transaction(*args, **kwargs)
diff --git a/app/services/tribute_service.py b/app/services/tribute_service.py
index 3a6d8c07..bef09a91 100644
--- a/app/services/tribute_service.py
+++ b/app/services/tribute_service.py
@@ -23,30 +23,10 @@ logger = logging.getLogger(__name__)
class TributeService:
- _invoice_messages: Dict[int, Dict[str, int]] = {}
def __init__(self, bot: Bot):
self.bot = bot
self.tribute_api = TributeAPI()
-
- @classmethod
- def remember_invoice_message(cls, user_id: int, chat_id: int, message_id: int) -> None:
- cls._invoice_messages[user_id] = {"chat_id": chat_id, "message_id": message_id}
-
- async def _cleanup_invoice_message(self, user_id: int) -> None:
- invoice_message = self._invoice_messages.pop(user_id, None)
- if not invoice_message or not getattr(self, "bot", None):
- return
-
- chat_id = invoice_message.get("chat_id")
- message_id = invoice_message.get("message_id")
- if not chat_id or not message_id:
- return
-
- try:
- await self.bot.delete_message(chat_id, message_id)
- except Exception as error: # pragma: no cover - depends on bot rights
- logger.warning("Не удалось удалить Tribute счёт %s: %s", message_id, error)
async def create_payment_link(
self,
@@ -194,8 +174,7 @@ class TributeService:
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о Tribute пополнении: {e}")
-
- await self._cleanup_invoice_message(user_telegram_id)
+
await self._send_success_notification(user_telegram_id, amount_kopeks)
logger.info(f"🎉 Успешно обработан Tribute платеж: {amount_kopeks/100}₽ для пользователя {user_telegram_id}")
diff --git a/tests/external/test_yookassa_webhook.py b/tests/external/test_yookassa_webhook.py
index 3ae1f6ff..03b98927 100644
--- a/tests/external/test_yookassa_webhook.py
+++ b/tests/external/test_yookassa_webhook.py
@@ -134,9 +134,9 @@ async def test_handle_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
status = response.status
text = await response.text()
- assert status == 400
- assert text == "No payment id"
- process_mock.assert_not_awaited()
+ assert status == 200
+ assert text == "OK"
+ process_mock.assert_awaited_once()
@pytest.mark.asyncio
@@ -160,9 +160,9 @@ async def test_handle_webhook_trusts_cf_connecting_ip(monkeypatch: pytest.Monkey
status = response.status
text = await response.text()
- assert status == 400
- assert text == "No payment id"
- process_mock.assert_not_awaited()
+ assert status == 200
+ assert text == "OK"
+ process_mock.assert_awaited_once()
@pytest.mark.asyncio
@@ -184,9 +184,9 @@ async def test_handle_webhook_with_optional_signature(monkeypatch: pytest.Monkey
status = response.status
text = await response.text()
- assert status == 400
- assert text == "No payment id"
- process_mock.assert_not_awaited()
+ assert status == 200
+ assert text == "OK"
+ process_mock.assert_awaited_once()
@pytest.mark.asyncio