diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py
index f48b1148..fa1a877c 100644
--- a/handlers/admin/coupons/coupons_handler.py
+++ b/handlers/admin/coupons/coupons_handler.py
@@ -1,15 +1,36 @@
+from datetime import datetime
from typing import Any
+import html
+import pytz
from aiogram import F, Router
+from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
-from aiogram.types import CallbackQuery, Message, InlineQuery, InlineQueryResultArticle, InputTextMessageContent
-from aiogram.enums import ParseMode
+from aiogram.types import (
+ CallbackQuery,
+ InlineQuery,
+ InlineQueryResultArticle,
+ InputTextMessageContent,
+ Message,
+)
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from config import USERNAME_BOT, INLINE_MODE
-from database import create_coupon, delete_coupon, get_all_coupons
+from config import INLINE_MODE, USERNAME_BOT
+from database import (
+ add_connection,
+ check_connection_exists,
+ create_coupon,
+ create_coupon_usage,
+ delete_coupon,
+ get_all_coupons,
+ get_keys,
+ update_key_expiry,
+)
from filters.admin import IsAdminFilter
+from handlers.buttons import BACK
+from handlers.keys.key_utils import renew_key_in_cluster
+from handlers.profile import process_callback_view_profile
from logger import logger
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
@@ -20,16 +41,17 @@ router = Router()
class AdminCouponsState(StatesGroup):
- waiting_for_coupon_data = State()
+ waiting_for_coupon_type = State()
+ waiting_for_balance_data = State()
+ waiting_for_days_data = State()
+ waiting_for_key_selection = State()
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons"),
IsAdminFilter(),
)
-async def handle_coupons(
- callback_query: CallbackQuery,
-):
+async def handle_coupons(callback_query: CallbackQuery):
await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb())
@@ -38,26 +60,52 @@ async def handle_coupons(
IsAdminFilter(),
)
async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext):
+ text = "🎫 Выберите тип купона:"
+ kb = InlineKeyboardBuilder()
+ kb.button(text="💰 Баланс", callback_data="coupon_type_balance")
+ kb.button(text="⏳ Время", callback_data="coupon_type_days")
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
+ kb.adjust(1)
+
+ await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
+ await state.set_state(AdminCouponsState.waiting_for_coupon_type)
+
+
+@router.callback_query(F.data == "coupon_type_balance")
+async def handle_balance_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
text = (
"🎫 Введите данные для создания купона в формате:\n\n"
"📝 код 💰 сумма 🔢 лимит\n\n"
"Пример: 'COUPON1 50 5' 👈\n\n"
)
+ kb = InlineKeyboardBuilder()
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
- await callback_query.message.edit_text(
- text=text,
- reply_markup=build_admin_back_kb("coupons"),
+ await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
+ await state.set_state(AdminCouponsState.waiting_for_balance_data)
+
+
+@router.callback_query(F.data == "coupon_type_days")
+async def handle_days_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
+ text = (
+ "🎫 Введите данные для создания купона в формате:\n\n"
+ "📝 код ⏳ дни 🔢 лимит\n\n"
+ "Пример: 'DAYS10 10 50' 👈\n\n"
)
- await state.set_state(AdminCouponsState.waiting_for_coupon_data)
+ kb = InlineKeyboardBuilder()
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
+
+ await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
+ await state.set_state(AdminCouponsState.waiting_for_days_data)
-@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
-async def handle_coupon_data_input(message: Message, state: FSMContext, session: Any):
+@router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter())
+async def handle_balance_coupon_input(message: Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
kb = InlineKeyboardBuilder()
- kb.button(text="Назад", callback_data=AdminPanelCallback(action="coupons").pack())
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
if len(parts) != 3:
text = (
@@ -70,15 +118,17 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session:
try:
coupon_code = parts[0]
- coupon_amount = float(parts[1])
+ coupon_amount = int(parts[1])
usage_limit = int(parts[2])
+ if coupon_amount <= 0:
+ raise ValueError("Сумма должна быть больше 0")
except ValueError:
text = "⚠️ Проверьте правильность введенных данных!\n💱 Сумма должна быть числом, а лимит — целым числом."
await message.answer(text=text, reply_markup=kb.as_markup())
return
try:
- await create_coupon(coupon_code, coupon_amount, usage_limit, session)
+ await create_coupon(coupon_code, coupon_amount, usage_limit, session, days=None)
coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
text = (
@@ -91,7 +141,60 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session:
kb = InlineKeyboardBuilder()
if INLINE_MODE:
kb.button(text="📤 Поделиться", switch_inline_query=f"coupon_{coupon_code}")
- kb.button(text="Назад", callback_data=AdminPanelCallback(action="coupons").pack())
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
+ kb.adjust(1)
+
+ await message.answer(text=text, reply_markup=kb.as_markup())
+ await state.clear()
+
+ except Exception as e:
+ logger.error(f"Ошибка при создании купона: {e}")
+ await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
+
+
+@router.message(AdminCouponsState.waiting_for_days_data, IsAdminFilter())
+async def handle_days_coupon_input(message: Message, state: FSMContext, session: Any):
+ text = message.text.strip()
+ parts = text.split()
+
+ kb = InlineKeyboardBuilder()
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
+
+ if len(parts) != 3:
+ text = (
+ "❌ Некорректный формат! 📝 Пожалуйста, введите данные в формате:\n"
+ "🏷️ код ⏳ дни 🔢 лимит\n"
+ "Пример: 'DAYS10 10 50' 👈"
+ )
+ await message.answer(text=text, reply_markup=kb.as_markup())
+ return
+
+ try:
+ coupon_code = parts[0]
+ days = int(parts[1])
+ usage_limit = int(parts[2])
+ if days <= 0:
+ raise ValueError("Количество дней должно быть больше 0")
+ except ValueError:
+ text = "⚠️ Проверьте правильность введенных данных!\n💱 Дни должны быть числом, а лимит — целым числом."
+ await message.answer(text=text, reply_markup=kb.as_markup())
+ return
+
+ try:
+ await create_coupon(coupon_code, 0, usage_limit, session, days=days)
+
+ coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
+ text = (
+ f"✅ Купон с кодом {coupon_code} успешно создан!\n"
+ f"⏳ Дней: {days}\n"
+ f"🔢 Лимит использования: {usage_limit} раз\n"
+ f"🔗 Ссылка: {coupon_link}\n"
+ )
+
+ kb = InlineKeyboardBuilder()
+ if INLINE_MODE:
+ kb.button(text="📤 Поделиться", switch_inline_query=f"coupon_{coupon_code}")
+ kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
kb.adjust(1)
await message.answer(text=text, reply_markup=kb.as_markup())
@@ -124,12 +227,13 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
coupon_list = "📜 Список всех купонов:\n\n"
for coupon in coupons:
+ value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}"
coupon_list += (
f"🏷️ Код: {coupon['code']}\n"
- f"💰 Сумма: {coupon['amount']} рублей\n"
+ f"{value_text}\n"
f"🔢 Лимит использования: {coupon['usage_limit']} раз\n"
f"✅ Использовано: {coupon['usage_count']} раз\n"
- f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n"
+ f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n\n"
)
await callback_query.message.edit_text(text=coupon_list, reply_markup=kb)
except Exception as e:
@@ -167,12 +271,13 @@ async def update_coupons_list(message, session: Any, page: int = 1):
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
coupon_list = "📜 Список всех купонов:\n\n"
for coupon in coupons:
+ value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ Дней: {coupon['days']}"
coupon_list += (
f"🏷️ Код: {coupon['code']}\n"
- f"💰 Сумма: {coupon['amount']} рублей\n"
+ f"{value_text}\n"
f"🔢 Лимит использования: {coupon['usage_limit']} раз\n"
f"✅ Использовано: {coupon['usage_count']} раз\n"
- f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n"
+ f"🔗 Ссылка: https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}\n\n"
)
await message.edit_text(text=coupon_list, reply_markup=kb)
@@ -198,10 +303,10 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
return
title = f"Купон {coupon['code']}"
- description = f"Получи {coupon['amount']} рублей!"
+ description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {coupon['days']} дней!"
message_text = (
f"🎫 Купон: {coupon['code']}\n"
- f"💰 Бонус: {coupon['amount']} рублей\n"
+ f"{'💰 Бонус: ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ Продление: ' + str(coupon['days']) + ' дней'}\n"
f"👇 Нажми, чтобы активировать!"
)
@@ -224,3 +329,156 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
cache_time=86400,
is_personal=True
)
+
+
+@router.message(F.text.regexp(r"^/start coupons_(.+)$"))
+async def handle_coupon_activation(message: Message, state: FSMContext, session: Any, admin: bool = False):
+ coupon_code = message.text.split("coupons_")[1]
+
+ coupons = await get_all_coupons(session, page=1, per_page=10)
+ coupon = next((c for c in coupons["coupons"] if c["code"] == coupon_code), None)
+
+ if not coupon:
+ await message.answer("❌ Купон не найден.")
+ return
+
+ if coupon["usage_count"] >= coupon["usage_limit"] or coupon["is_used"]:
+ await message.answer("❌ Лимит активаций купона исчерпан.")
+ return
+
+ usage = await session.fetchrow(
+ "SELECT * FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2",
+ coupon["id"],
+ message.from_user.id
+ )
+ if usage:
+ await message.answer("❌ Вы уже активировали этот купон.")
+ return
+
+ connection_exists = await check_connection_exists(message.from_user.id)
+ if not connection_exists:
+ await add_connection(tg_id=message.from_user.id, session=session)
+
+ if coupon["amount"] > 0:
+ await session.execute(
+ "UPDATE connections SET balance = balance + $1 WHERE tg_id = $2",
+ coupon["amount"],
+ message.from_user.id
+ )
+ await session.execute(
+ "UPDATE coupons SET usage_count = usage_count + 1, is_used = $1 WHERE id = $2",
+ coupon["usage_count"] + 1 >= coupon["usage_limit"],
+ coupon["id"]
+ )
+ await create_coupon_usage(coupon["id"], message.from_user.id, session)
+ await message.answer(f"✅ Купон активирован, на баланс начислено {coupon['amount']} рублей.")
+ await process_callback_view_profile(message, state, admin)
+ return
+
+ if coupon["days"] is not None and coupon["days"] > 0:
+ keys = await get_keys(message.from_user.id, session)
+ active_keys = [k for k in keys if not k["is_frozen"]]
+
+ if not active_keys:
+ await message.answer("❌ У вас нет активных подписок для продления.")
+ return
+
+ builder = InlineKeyboardBuilder()
+ moscow_tz = pytz.timezone("Europe/Moscow")
+ response_message = "🔑 Выберите подписку для продления:\n\n
" + + for key in active_keys: + alias = key.get("alias") + email = key["email"] + client_id = key["client_id"] + expiry_time = key.get("expiry_time") + + key_display = html.escape(alias.strip() if alias else email) + expiry_date = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz).strftime("до %d.%m.%y, %H:%M") + response_message += f"• {key_display} ({expiry_date})\n" + builder.button(text=key_display, callback_data=f"extend_key|{client_id}|{coupon['id']}") + + response_message += "" + builder.button(text="Отмена", callback_data="cancel_coupon_activation") + builder.adjust(1) + + await message.answer(response_message, reply_markup=builder.as_markup()) + await state.set_state(AdminCouponsState.waiting_for_key_selection) + await state.update_data(coupon_id=coupon["id"]) + return + + await message.answer("❌ Купон недействителен (нет суммы или дней).") + + +@router.callback_query(F.data.startswith("extend_key|")) +async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool = False): + parts = callback_query.data.split("|") + client_id = parts[1] + coupon_id = int(parts[2]) + + coupon = await session.fetchrow("SELECT * FROM coupons WHERE id = $1", coupon_id) + if not coupon or coupon["usage_count"] >= coupon["usage_limit"]: + await callback_query.message.edit_text("❌ Купон недействителен или лимит исчерпан.") + await state.clear() + return + + usage = await session.fetchrow( + "SELECT * FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2", + coupon_id, + callback_query.from_user.id + ) + if usage: + await callback_query.message.edit_text("❌ Вы уже активировали этот купон.") + await state.clear() + return + + key = await session.fetchrow( + "SELECT * FROM keys WHERE tg_id = $1 AND client_id = $2", + callback_query.from_user.id, + client_id + ) + if not key or key["is_frozen"]: + await callback_query.message.edit_text("❌ Выбранная подписка не найдена или заморожена.") + await state.clear() + return + + now_ms = int(datetime.now().timestamp() * 1000) + current_expiry = key["expiry_time"] + new_expiry = max(now_ms, current_expiry) + (coupon["days"] * 86400 * 1000) + + try: + await renew_key_in_cluster( + cluster_id=key["server_id"], + email=key["email"], + client_id=client_id, + new_expiry_time=new_expiry, + total_gb=0 + ) + await update_key_expiry(client_id, new_expiry, session) + + await session.execute( + "UPDATE coupons SET usage_count = usage_count + 1, is_used = $1 WHERE id = $2", + coupon["usage_count"] + 1 >= coupon["usage_limit"], + coupon["id"] + ) + await create_coupon_usage(coupon["id"], callback_query.from_user.id, session) + + alias = key.get("alias") or key["email"] + expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime("%d.%m.%y, %H:%M") + text = f"✅ Купон активирован, подписка {alias} продлена на {coupon['days']}⏳ дней до {expiry_date}📆." + + await callback_query.message.answer(text) + await process_callback_view_profile(callback_query.message, state, admin) + await state.clear() + + except Exception as e: + logger.error(f"Ошибка при продлении ключа: {e}") + await callback_query.message.edit_text("❌ Произошла ошибка при продлении подписки.") + await state.clear() + + +@router.callback_query(F.data == "cancel_coupon_activation") +async def cancel_coupon_activation(callback_query: CallbackQuery, state: FSMContext, admin: bool = False): + await callback_query.message.answer("⚠️ Активация купона отменена.") + await process_callback_view_profile(callback_query.message, state, admin) + await state.clear()