Files
marzban_tg_bot/handlers/payment.py
2026-01-11 07:07:32 +03:00

220 lines
8.2 KiB
Python

from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, LabeledPrice, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from datetime import datetime
import logging
from config import CONFIG, PLANS
from database import db
from marzban import marzban
from keyboards import main_keyboard
router = Router()
logger = logging.getLogger(__name__)
async def grant_subscription(user_id: int, plan_id: str, promo_code: str, amount: int, bonus_days: int = 0):
total_days = 0
data_limit = 0
plan_name = "VIP Sub"
marzban_days = 0
if plan_id:
plan = PLANS[plan_id]
total_days = plan['days'] + bonus_days
marzban_days = total_days
data_limit = plan['data_limit']
plan_name = plan['name']
else:
# VIP case without plan
total_days = 365 * 99 # For DB (99 years)
marzban_days = 0 # For Marzban (Unlimited/None)
data_limit = 0 # Unlimited
plan_name = "VIP"
user = await db.get_user(user_id)
tg_username = user['username']
note = f"@{tg_username}" if tg_username else ""
sub_until = user['subscription_until']
if isinstance(sub_until, str):
try:
sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f')
except: pass
# Marzban
try:
marzban_username = user['marzban_username']
resp = None
# Если есть подписка и она активна (и не бесконечна, хотя тут не важно)
is_sub_active = sub_until and sub_until > datetime.now()
if is_sub_active:
logger.info(f"Attempting to modify existing user: {marzban_username}")
resp = await marzban.modify_user(
marzban_username,
data_limit,
marzban_days,
note
)
if isinstance(resp, dict) and resp.get('detail') == 'User not found':
logger.info(f"User {marzban_username} missing in Marzban, re-creating...")
resp = await marzban.create_user(
marzban_username,
data_limit,
marzban_days,
note
)
else:
logger.info(f"Creating/Reactivating user: {marzban_username}")
resp = await marzban.create_user(
marzban_username,
data_limit,
marzban_days,
note
)
except Exception as e:
logger.error(f"Marzban error in grant_subscription: {e}")
# DB
await db.update_subscription(user_id, total_days, data_limit)
await db.add_payment(
user_id,
plan_id or "vip", # Store 'vip' as plan name/id
amount,
promo_code
)
if promo_code:
await db.decrement_promo_usage(promo_code)
# Return dummy plan dict for display
return {'name': plan_name, 'days': total_days, 'data_limit': data_limit}, total_days
@router.callback_query(F.data == "pay_now")
async def process_payment(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
plan_id = data.get('selected_plan')
is_vip = data.get('is_unlimited_promo')
if not plan_id and not is_vip:
await callback.answer("❌ Сессия истекла. Пожалуйста, выберите тариф снова.", show_alert=True)
return
# Если VIP без плана, ставим дефолтные значения
plan_name = "VIP"
plan_days = 3650
plan_limit = 0
base_price = 0
if plan_id:
plan = PLANS[plan_id]
plan_name = plan['name']
plan_days = plan['days']
plan_limit = plan['data_limit']
base_price = plan['price']
final_price = int(data.get('final_price', base_price))
promo_code = data.get('promo_code')
bonus_days = data.get('bonus_days', 0)
if final_price <= 0:
sticky_msg = ""
if promo_code:
p_data = await db.get_promo_code(promo_code)
# Use explicit key access with fallback logic if needed, but keys exist
if p_data and p_data['is_sticky']:
user = await db.get_user(callback.from_user.id)
u_disc = user['personal_discount'] if user and user['personal_discount'] else 0
if p_data['discount'] > u_disc:
await db.set_user_discount(callback.from_user.id, p_data['discount'])
sticky_msg = "\n🔐 Скидка закреплена навсегда!"
plan, date_days = await grant_subscription(callback.from_user.id, plan_id, promo_code, 0, bonus_days)
await callback.message.edit_text(
f"✅ Подписка активирована бесплатно!\n\n"
f"План: {plan['name']}\n"
f"Срок: {date_days} дней\n"
f"Трафик: {plan['data_limit'] if plan['data_limit'] > 0 else ''} ГБ\n"
f"{sticky_msg}\n"
f"Настройте подключение в меню: 📊 Моя подписка",
reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"])
)
await state.clear()
else:
# Создаем инвойс для Telegram Stars
limit_str = f"{plan['data_limit']} ГБ" if plan['data_limit'] > 0 else "Безлимит"
await callback.message.answer_invoice(
title=f"Подписка VPN - {plan['name']}",
description=f"Трафик: {limit_str} на {plan['days']} дней",
payload=f"{plan_id}:{data.get('promo_code', '')}",
currency="XTR", # Telegram Stars
prices=[LabeledPrice(label=plan['name'], amount=final_price)],
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", pay=True)]
])
)
@router.pre_checkout_query()
async def checkout_process(pre_checkout_query):
await pre_checkout_query.answer(ok=True)
@router.message(F.successful_payment)
async def successful_payment(message: Message):
payment = message.successful_payment
plan_id, promo_code = payment.invoice_payload.split(":")
# We can reuse grant_subscription helper
promo_code = promo_code if promo_code else None
bonus_days = 0
sticky_text = ""
if promo_code:
# Fetch actual promo details
promo_data = await db.get_promo_code(promo_code)
if promo_data:
bonus_days = promo_data['bonus_days']
# STICKY LOGIC
# Access by key (sqlite3.Row has no .get method)
is_sticky = False
try:
is_sticky = promo_data['is_sticky']
except IndexError:
pass # Column missing?
if is_sticky:
user = await db.get_user(message.from_user.id)
current_discount = user['personal_discount'] if user and user['personal_discount'] else 0
new_discount = promo_data['discount']
if new_discount > current_discount:
await db.set_user_discount(message.from_user.id, new_discount)
sticky_text = f"\n🔐 <b>Скидка {new_discount}% закреплена за вами НАВСЕГДА!</b>"
plan, date_days = await grant_subscription(
message.from_user.id,
plan_id,
promo_code,
payment.total_amount,
bonus_days
)
limit_str = f"{plan['data_limit']} ГБ" if plan['data_limit'] > 0 else "Безлимит"
await message.answer(
f"✅ Оплата успешна!\n\n"
f"Ваша подписка активирована на {date_days} дней.\n"
f"Трафик: {limit_str}\n"
f"{sticky_text}\n"
f"Получите конфигурацию через меню: 📊 Моя подписка",
reply_markup=main_keyboard(message.from_user.id in CONFIG["ADMIN_IDS"]),
parse_mode="HTML"
)