Files
remnawave-bedolaga-telegram…/app/handlers/stars_payments.py
T
2026-01-14 04:11:44 +03:00

469 lines
19 KiB
Python

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.services.payment_service import PaymentService
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
logger = logging.getLogger(__name__)
async def _handle_wheel_spin_payment(
message: types.Message,
db: AsyncSession,
user,
stars_amount: int,
payload: str,
texts,
):
"""Обработка Stars платежа для колеса удачи."""
from app.services.wheel_service import wheel_service
from app.database.crud.wheel import get_or_create_wheel_config, get_wheel_prizes
try:
config = await get_or_create_wheel_config(db)
if not config.is_enabled:
await message.answer(
"❌ Колесо удачи временно недоступно. Звезды будут возвращены.",
)
return False
# Выполняем спин напрямую (оплата уже прошла через Stars)
prizes = await get_or_create_wheel_config(db)
prizes = await get_wheel_prizes(db, config.id, active_only=True)
if not prizes:
await message.answer(
"❌ Призы не настроены. Обратитесь в поддержку.",
)
return False
# Рассчитываем стоимость в копейках для статистики
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
payment_value_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
# Рассчитываем вероятности и выбираем приз
prizes_with_probs = wheel_service.calculate_prize_probabilities(config, prizes, payment_value_kopeks)
selected_prize = wheel_service._select_prize(prizes_with_probs)
# Применяем приз
generated_promocode = await wheel_service._apply_prize(db, user, selected_prize, config)
# Создаем запись спина
from app.database.crud.wheel import create_wheel_spin
from app.database.models import WheelSpinPaymentType
promocode_id = None
if generated_promocode:
result = await db.execute(
f"SELECT id FROM promocodes WHERE code = '{generated_promocode}'"
)
row = result.fetchone()
if row:
promocode_id = row[0]
logger.info(
f"🎰 Creating wheel spin: user.id={user.id}, user.telegram_id={user.telegram_id}, "
f"prize={selected_prize.display_name}"
)
spin = await create_wheel_spin(
db=db,
user_id=user.id,
prize_id=selected_prize.id,
payment_type=WheelSpinPaymentType.TELEGRAM_STARS.value,
payment_amount=stars_amount,
payment_value_kopeks=payment_value_kopeks,
prize_type=selected_prize.prize_type,
prize_value=selected_prize.prize_value,
prize_display_name=selected_prize.display_name,
prize_value_kopeks=selected_prize.prize_value_kopeks,
generated_promocode_id=promocode_id,
is_applied=True,
)
logger.info(f"🎰 Wheel spin created: spin.id={spin.id}, spin.user_id={spin.user_id}")
# Ensure all changes are committed (subscription days, traffic GB, etc.)
await db.commit()
# Отправляем результат
prize_message = wheel_service._get_prize_message(selected_prize, generated_promocode)
emoji = selected_prize.emoji or "🎁"
await message.answer(
f"🎰 <b>Колесо удачи!</b>\n\n"
f"{emoji} <b>{selected_prize.display_name}</b>\n\n"
f"{prize_message}\n\n"
f"⭐ Потрачено: {stars_amount} Stars",
parse_mode="HTML",
)
logger.info(
f"🎰 Wheel spin via Stars: user={user.id}, prize={selected_prize.display_name}, "
f"stars={stars_amount}"
)
return True
except Exception as e:
logger.error(f"Ошибка обработки wheel spin payment: {e}", exc_info=True)
await message.answer(
"❌ Произошла ошибка при обработке спина. Обратитесь в поддержку.",
)
return False
async def _handle_trial_payment(
message: types.Message,
db: AsyncSession,
user,
stars_amount: int,
payload: str,
texts,
):
"""Обработка Stars платежа для платного триала."""
from app.database.crud.subscription import activate_pending_trial_subscription
from app.services.subscription_service import SubscriptionService
from app.services.admin_notification_service import AdminNotificationService
from app.database.crud.transaction import create_transaction
from app.database.models import TransactionType, PaymentMethod
try:
# Парсим payload: trial_{subscription_id}
parts = payload.split("_")
if len(parts) < 2:
logger.error(f"Невалидный trial payload: {payload}")
await message.answer(
"❌ Ошибка: неверный формат платежа. Обратитесь в поддержку.",
)
return False
try:
subscription_id = int(parts[1])
except ValueError:
logger.error(f"Невалидный subscription_id в trial payload: {payload}")
await message.answer(
"❌ Ошибка: неверный ID подписки. Обратитесь в поддержку.",
)
return False
# Рассчитываем стоимость в копейках
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
# Создаём транзакцию
transaction = await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=amount_kopeks,
description=f"Оплата пробной подписки через Telegram Stars ({stars_amount} ⭐)",
payment_method=PaymentMethod.TELEGRAM_STARS,
external_id=f"trial_stars_{subscription_id}",
is_completed=True,
)
# Активируем pending триальную подписку
subscription = await activate_pending_trial_subscription(
db=db,
subscription_id=subscription_id,
user_id=user.id,
)
if not subscription:
logger.error(f"Не удалось активировать триальную подписку {subscription_id} для пользователя {user.id}")
# Возвращаем деньги на баланс
from app.database.crud.user import add_user_balance
await add_user_balance(
db,
user,
amount_kopeks,
"Возврат за неудачную активацию триала",
transaction_type=TransactionType.REFUND,
)
await message.answer(
"❌ Не удалось активировать пробную подписку. Средства возвращены на баланс.",
)
return False
# Создаем пользователя в RemnaWave
subscription_service = SubscriptionService()
try:
await subscription_service.create_remnawave_user(db, subscription)
except Exception as rw_error:
logger.error(f"Ошибка создания пользователя RemnaWave для триала: {rw_error}")
# Не откатываем подписку, просто логируем - RemnaWave может быть временно недоступен
await db.commit()
await db.refresh(user)
# Отправляем уведомление админам
try:
admin_notification_service = AdminNotificationService(message.bot)
await admin_notification_service.send_trial_activation_notification(
user=user,
subscription=subscription,
paid_amount=amount_kopeks,
payment_method="Telegram Stars",
)
except Exception as admin_error:
logger.warning(f"Ошибка отправки уведомления админам о триале: {admin_error}")
# Отправляем сообщение пользователю
await message.answer(
f"🎉 <b>Пробная подписка активирована!</b>\n\n"
f"⭐ Потрачено: {stars_amount} Stars\n"
f"📅 Период: {settings.TRIAL_DURATION_DAYS} дней\n"
f"📱 Устройств: {subscription.device_limit}\n\n"
f"Используйте меню для подключения к VPN.",
parse_mode="HTML",
)
logger.info(
f"✅ Платный триал активирован через Stars: user={user.id}, "
f"subscription={subscription.id}, stars={stars_amount}"
)
return True
except Exception as e:
logger.error(f"Ошибка обработки trial payment: {e}", exc_info=True)
await message.answer(
"❌ Произошла ошибка при активации пробной подписки. Обратитесь в поддержку.",
)
return False
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}"
)
allowed_prefixes = ("balance_", "admin_stars_test_", "simple_sub_", "wheel_spin_", "trial_")
if not query.invoice_payload or not query.invoice_payload.startswith(allowed_prefixes):
logger.warning(f"Невалидный payload: {query.invoice_payload}")
await query.answer(
ok=False,
error_message=texts.t(
"STARS_PRECHECK_INVALID_PAYLOAD",
"Ошибка валидации платежа. Попробуйте еще раз.",
),
)
return
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:
logger.warning(f"Пользователь {query.from_user.id} не найден в БД")
await query.answer(
ok=False,
error_message=texts.t(
"STARS_PRECHECK_USER_NOT_FOUND",
"Пользователь не найден. Обратитесь в поддержку.",
),
)
return
texts = get_texts(user.language or DEFAULT_LANGUAGE)
break
except Exception as db_error:
logger.error(f"Ошибка подключения к БД в pre_checkout_query: {db_error}")
await query.answer(
ok=False,
error_message=texts.t(
"STARS_PRECHECK_TECHNICAL_ERROR",
"Техническая ошибка. Попробуйте позже.",
),
)
return
await query.answer(ok=True)
logger.info(f"✅ Pre-checkout одобрен для пользователя {query.from_user.id}")
except Exception as e:
logger.error(f"Ошибка в pre_checkout_query: {e}", exc_info=True)
await query.answer(
ok=False,
error_message=texts.t(
"STARS_PRECHECK_TECHNICAL_ERROR",
"Техническая ошибка. Попробуйте позже.",
),
)
async def handle_successful_payment(
message: types.Message,
db: AsyncSession,
state: FSMContext,
**kwargs
):
texts = get_texts(DEFAULT_LANGUAGE)
try:
payment = message.successful_payment
user_id = message.from_user.id
logger.info(
f"💳 Успешный Stars платеж от {user_id}: "
f"{payment.total_amount} XTR, "
f"payload: {payment.invoice_payload}, "
f"charge_id: {payment.telegram_payment_charge_id}"
)
user = await get_user_by_telegram_id(db, user_id)
texts = get_texts(user.language if user and user.language else DEFAULT_LANGUAGE)
if not user:
logger.error(f"Пользователь {user_id} не найден при обработке Stars платежа")
await message.answer(
texts.t(
"STARS_PAYMENT_USER_NOT_FOUND",
"❌ Ошибка: пользователь не найден. Обратитесь в поддержку.",
)
)
return
# Обработка оплаты спина колеса удачи
if payment.invoice_payload and payment.invoice_payload.startswith("wheel_spin_"):
await _handle_wheel_spin_payment(
message=message,
db=db,
user=user,
stars_amount=payment.total_amount,
payload=payment.invoice_payload,
texts=texts,
)
return
# Обработка оплаты платного триала
if payment.invoice_payload and payment.invoice_payload.startswith("trial_"):
await _handle_trial_payment(
message=message,
db=db,
user=user,
stars_amount=payment.total_amount,
payload=payment.invoice_payload,
texts=texts,
)
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,
stars_amount=payment.total_amount,
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))
amount_text = settings.format_price(amount_kopeks).replace("", "")
keyboard = await payment_service.build_topup_success_keyboard(user)
transaction_id_short = payment.telegram_payment_charge_id[:8]
await message.answer(
texts.t(
"STARS_PAYMENT_SUCCESS",
"🎉 <b>Платеж успешно обработан!</b>\n\n"
"⭐ Потрачено звезд: {stars_spent}\n"
"💰 Зачислено на баланс: {amount}\n"
"🆔 ID транзакции: {transaction_id}...\n\n"
"⚠️ <b>Важно:</b> Пополнение баланса не активирует подписку автоматически. "
"Обязательно активируйте подписку отдельно!\n\n"
"🔄 При наличии сохранённой корзины подписки и включенной автопокупке, "
"подписка будет приобретена автоматически после пополнения баланса.\n\n"
"Спасибо за пополнение! 🚀",
).format(
stars_spent=payment.total_amount,
amount=amount_text,
transaction_id=transaction_id_short,
),
parse_mode="HTML",
reply_markup=keyboard,
)
logger.info(
"✅ Stars платеж успешно обработан: пользователь %s, %s звезд → %s",
user.id,
payment.total_amount,
settings.format_price(amount_kopeks),
)
else:
logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}")
await message.answer(
texts.t(
"STARS_PAYMENT_ENROLLMENT_ERROR",
"❌ Произошла ошибка при зачислении средств. "
"Обратитесь в поддержку, платеж будет проверен вручную.",
)
)
except Exception as e:
logger.error(f"Ошибка в successful_payment: {e}", exc_info=True)
await message.answer(
texts.t(
"STARS_PAYMENT_PROCESSING_ERROR",
"❌ Техническая ошибка при обработке платежа. "
"Обратитесь в поддержку для решения проблемы.",
)
)
def register_stars_handlers(dp: Dispatcher):
dp.pre_checkout_query.register(
handle_pre_checkout_query,
F.currency == "XTR"
)
dp.message.register(
handle_successful_payment,
F.successful_payment
)
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")