Merge pull request #1957 from BEDOLAGA-DEV/bedolaga/remove-unnecessary-chat-messages-after-balance-refill

Clean up Telegram Stars top-up messages
This commit is contained in:
Egor
2025-11-21 03:24:32 +03:00
committed by GitHub
4 changed files with 118 additions and 53 deletions

View File

@@ -1,7 +1,6 @@
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
@@ -22,11 +21,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 = (
@@ -35,10 +34,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
@@ -46,14 +45,17 @@ 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.set_state(BalanceStates.waiting_for_amount)
await state.update_data(payment_method="stars")
await state.update_data(
payment_method="stars",
prompt_message_id=callback.message.message_id,
)
await callback.answer()
@@ -65,40 +67,75 @@ 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")]
])
await message.answer(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
f"К оплате: {stars_amount} звезд\n"
f"📊 Курс: {stars_rate}₽ за звезду\n\n"
prompt_message_id = (await state.get_data()).get("prompt_message_id")
if prompt_message_id:
try:
await message.bot.delete_message(
chat_id=message.chat.id,
message_id=prompt_message_id,
)
except Exception:
logger.warning(
"Не удалось удалить сообщение с вводом суммы Stars",
exc_info=True,
)
try:
await message.delete()
except Exception:
logger.warning(
"Не удалось удалить сообщение пользователя с суммой",
exc_info=True,
)
invoice_message = await message.answer(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n",
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n",
f"К оплате: {stars_amount} звезд\n",
f"📊 Курс: {stars_rate}₽ за звезду\n\n",
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard,
parse_mode="HTML"
)
try:
from app.utils.stars_message_cleanup import register_stars_payment_messages
await register_stars_payment_messages(
user_id=message.from_user.id,
message_ids=[invoice_message.message_id],
)
except Exception:
logger.warning(
"Не удалось зарегистрировать сообщения Stars для очистки",
exc_info=True,
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
await message.answer("⚠️ Ошибка создания платежа")
await message.answer("⚠️ Ошибка создания платежа")

View File

@@ -10,6 +10,7 @@ from app.external.telegram_stars import TelegramStarsService
from app.database.crud.user import get_user_by_telegram_id
from app.localization.loader import DEFAULT_LANGUAGE
from app.localization.texts import get_texts
from app.utils.stars_message_cleanup import consume_stars_payment_messages
logger = logging.getLogger(__name__)
@@ -113,8 +114,30 @@ async def handle_successful_payment(
payload=payment.invoice_payload,
telegram_payment_charge_id=payment.telegram_payment_charge_id
)
if success:
cleanup_message_ids = set()
try:
cleanup_message_ids = await consume_stars_payment_messages(user_id)
except Exception:
logger.warning(
"Не удалось получить сообщения Stars для удаления",
exc_info=True,
)
for message_id in cleanup_message_ids:
try:
await message.bot.delete_message(
chat_id=message.chat.id,
message_id=message_id,
)
except Exception:
logger.warning(
"Не удалось удалить промежуточное сообщение Stars",
exc_info=True,
)
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount)
amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
amount_text = settings.format_price(amount_kopeks).replace("", "")

View File

@@ -515,36 +515,6 @@ 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,
(
"✅ <b>Пополнение успешно!</b>\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

View File

@@ -0,0 +1,35 @@
"""Вспомогательные функции для очистки служебных сообщений Telegram Stars."""
from __future__ import annotations
import asyncio
from typing import Iterable, Set
__all__ = [
"register_stars_payment_messages",
"consume_stars_payment_messages",
]
_messages_lock = asyncio.Lock()
_messages_to_cleanup: dict[int, set[int]] = {}
async def register_stars_payment_messages(user_id: int, message_ids: Iterable[int]) -> None:
"""Сохраняет идентификаторы сообщений для последующего удаления.
Args:
user_id: Telegram ID пользователя.
message_ids: Сообщения, которые нужно удалить после успешной оплаты.
"""
async with _messages_lock:
existing = _messages_to_cleanup.setdefault(user_id, set())
existing.update(message_ids)
async def consume_stars_payment_messages(user_id: int) -> Set[int]:
"""Возвращает и удаляет накопленные сообщения для пользователя."""
async with _messages_lock:
return _messages_to_cleanup.pop(user_id, set())