mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
563 lines
19 KiB
Python
563 lines
19 KiB
Python
import logging
|
||
from datetime import datetime, timedelta
|
||
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.states import AdminStates
|
||
from app.database.models import User, PromoCodeType
|
||
from app.keyboards.admin import (
|
||
get_admin_promocodes_keyboard, get_promocode_type_keyboard,
|
||
get_admin_pagination_keyboard, get_confirmation_keyboard
|
||
)
|
||
from app.localization.texts import get_texts
|
||
from app.database.crud.promocode import (
|
||
get_promocodes_list, get_promocodes_count, create_promocode,
|
||
get_promocode_statistics, get_promocode_by_code, update_promocode,
|
||
delete_promocode
|
||
)
|
||
from app.utils.decorators import admin_required, error_handler
|
||
from app.utils.formatters import format_datetime
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promocodes_menu(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
total_codes = await get_promocodes_count(db)
|
||
active_codes = await get_promocodes_count(db, is_active=True)
|
||
|
||
text = f"""
|
||
🎫 <b>Управление промокодами</b>
|
||
|
||
📊 <b>Статистика:</b>
|
||
- Всего промокодов: {total_codes}
|
||
- Активных: {active_codes}
|
||
- Неактивных: {total_codes - active_codes}
|
||
|
||
Выберите действие:
|
||
"""
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=get_admin_promocodes_keyboard(db_user.language)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promocodes_list(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession,
|
||
page: int = 1
|
||
):
|
||
limit = 10
|
||
offset = (page - 1) * limit
|
||
|
||
promocodes = await get_promocodes_list(db, offset=offset, limit=limit)
|
||
total_count = await get_promocodes_count(db)
|
||
total_pages = (total_count + limit - 1) // limit
|
||
|
||
if not promocodes:
|
||
await callback.message.edit_text(
|
||
"🎫 Промокоды не найдены",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_promocodes")]
|
||
])
|
||
)
|
||
await callback.answer()
|
||
return
|
||
|
||
text = f"🎫 <b>Список промокодов</b> (стр. {page}/{total_pages})\n\n"
|
||
|
||
for promo in promocodes:
|
||
status_emoji = "✅" if promo.is_active else "❌"
|
||
type_emoji = {"balance": "💰", "subscription_days": "📅", "trial_subscription": "🎁"}.get(promo.type, "🎫")
|
||
|
||
text += f"{status_emoji} {type_emoji} <code>{promo.code}</code>\n"
|
||
text += f"📊 Использований: {promo.current_uses}/{promo.max_uses}\n"
|
||
|
||
if promo.type == PromoCodeType.BALANCE.value:
|
||
text += f"💰 Бонус: {settings.format_price(promo.balance_bonus_kopeks)}\n"
|
||
elif promo.type == PromoCodeType.SUBSCRIPTION_DAYS.value:
|
||
text += f"📅 Дней: {promo.subscription_days}\n"
|
||
|
||
if promo.valid_until:
|
||
text += f"⏰ До: {format_datetime(promo.valid_until)}\n"
|
||
|
||
text += f"🔧 Управление: /promo_{promo.id}\n\n"
|
||
|
||
keyboard = []
|
||
|
||
if total_pages > 1:
|
||
pagination_row = get_admin_pagination_keyboard(
|
||
page, total_pages, "admin_promo_list", "admin_promocodes", db_user.language
|
||
).inline_keyboard[0]
|
||
keyboard.append(pagination_row)
|
||
|
||
keyboard.extend([
|
||
[types.InlineKeyboardButton(text="➕ Создать", callback_data="admin_promo_create")],
|
||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_promocodes")]
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promocode_management(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
|
||
promo = await db.get(PromoCode, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
status_emoji = "✅" if promo.is_active else "❌"
|
||
type_emoji = {"balance": "💰", "subscription_days": "📅", "trial_subscription": "🎁"}.get(promo.type, "🎫")
|
||
|
||
text = f"""
|
||
🎫 <b>Управление промокодом</b>
|
||
|
||
{type_emoji} <b>Код:</b> <code>{promo.code}</code>
|
||
{status_emoji} <b>Статус:</b> {'Активен' if promo.is_active else 'Неактивен'}
|
||
📊 <b>Использований:</b> {promo.current_uses}/{promo.max_uses}
|
||
"""
|
||
|
||
if promo.type == PromoCodeType.BALANCE.value:
|
||
text += f"💰 <b>Бонус:</b> {settings.format_price(promo.balance_bonus_kopeks)}\n"
|
||
elif promo.type == PromoCodeType.SUBSCRIPTION_DAYS.value:
|
||
text += f"📅 <b>Дней:</b> {promo.subscription_days}\n"
|
||
|
||
if promo.valid_until:
|
||
text += f"⏰ <b>Действует до:</b> {format_datetime(promo.valid_until)}\n"
|
||
|
||
text += f"📅 <b>Создан:</b> {format_datetime(promo.created_at)}\n"
|
||
|
||
keyboard = [
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="✏️ Редактировать",
|
||
callback_data=f"promo_edit_{promo.id}"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text="🔄 Переключить статус",
|
||
callback_data=f"promo_toggle_{promo.id}"
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="📊 Статистика",
|
||
callback_data=f"promo_stats_{promo.id}"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text="🗑️ Удалить",
|
||
callback_data=f"promo_delete_{promo.id}"
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(text="⬅️ К списку", callback_data="admin_promo_list")
|
||
]
|
||
]
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_promocode_status(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
|
||
promo = await db.get(PromoCode, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
new_status = not promo.is_active
|
||
await update_promocode(db, promo, is_active=new_status)
|
||
|
||
status_text = "активирован" if new_status else "деактивирован"
|
||
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
|
||
|
||
await show_promocode_management(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def confirm_delete_promocode(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
|
||
promo = await db.get(PromoCode, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
text = f"""
|
||
⚠️ <b>Подтверждение удаления</b>
|
||
|
||
Вы действительно хотите удалить промокод <code>{promo.code}</code>?
|
||
|
||
<b>Внимание:</b> Это действие нельзя отменить!
|
||
"""
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="✅ Да, удалить",
|
||
callback_data=f"promo_delete_confirm_{promo.id}"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data=f"promo_manage_{promo.id}"
|
||
)
|
||
]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def delete_promocode_confirmed(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
|
||
promo = await db.get(PromoCode, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
code = promo.code
|
||
success = await delete_promocode(db, promo)
|
||
|
||
if success:
|
||
await callback.answer(f"✅ Промокод {code} удален", show_alert=True)
|
||
await show_promocodes_list(callback, db_user, db)
|
||
else:
|
||
await callback.answer("❌ Ошибка удаления промокода", show_alert=True)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promocode_stats(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
|
||
promo = await db.get(PromoCode, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
stats = await get_promocode_statistics(db, promo_id)
|
||
|
||
text = f"""
|
||
📊 <b>Статистика промокода</b> <code>{promo.code}</code>
|
||
|
||
📈 <b>Общая статистика:</b>
|
||
- Всего использований: {stats['total_uses']}
|
||
- Использований сегодня: {stats['today_uses']}
|
||
- Осталось использований: {promo.max_uses - promo.current_uses}
|
||
|
||
📅 <b>Последние использования:</b>
|
||
"""
|
||
|
||
for use in stats['recent_uses'][:5]:
|
||
use_date = format_datetime(use.used_at)
|
||
text += f"- {use_date} (ID: {use.user_id})\n"
|
||
|
||
if not stats['recent_uses']:
|
||
text += "- Пока не было использований\n"
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="⬅️ Назад",
|
||
callback_data=f"promo_manage_{promo.id}"
|
||
)
|
||
]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_promocode_creation(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
await callback.message.edit_text(
|
||
"🎫 <b>Создание промокода</b>\n\n"
|
||
"Выберите тип промокода:",
|
||
reply_markup=get_promocode_type_keyboard(db_user.language)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def select_promocode_type(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
promo_type = callback.data.split('_')[-1]
|
||
|
||
type_names = {
|
||
"balance": "💰 Пополнение баланса",
|
||
"days": "📅 Дни подписки",
|
||
"trial": "🎁 Тестовая подписка"
|
||
}
|
||
|
||
await state.update_data(promocode_type=promo_type)
|
||
|
||
await callback.message.edit_text(
|
||
f"🎫 <b>Создание промокода</b>\n\n"
|
||
f"Тип: {type_names.get(promo_type, promo_type)}\n\n"
|
||
f"Введите код промокода (только латинские буквы и цифры):",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_promocodes")]
|
||
])
|
||
)
|
||
|
||
await state.set_state(AdminStates.creating_promocode)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_code(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
code = message.text.strip().upper()
|
||
|
||
if not code.isalnum() or len(code) < 3 or len(code) > 20:
|
||
await message.answer("❌ Код должен содержать только латинские буквы и цифры (3-20 символов)")
|
||
return
|
||
|
||
from app.database.crud.promocode import get_promocode_by_code
|
||
existing = await get_promocode_by_code(db, code)
|
||
if existing:
|
||
await message.answer("❌ Промокод с таким кодом уже существует")
|
||
return
|
||
|
||
await state.update_data(promocode_code=code)
|
||
|
||
data = await state.get_data()
|
||
promo_type = data.get('promocode_type')
|
||
|
||
if promo_type == "balance":
|
||
await message.answer(
|
||
f"💰 <b>Промокод:</b> <code>{code}</code>\n\n"
|
||
f"Введите сумму пополнения баланса (в рублях):"
|
||
)
|
||
await state.set_state(AdminStates.setting_promocode_value)
|
||
elif promo_type == "days":
|
||
await message.answer(
|
||
f"📅 <b>Промокод:</b> <code>{code}</code>\n\n"
|
||
f"Введите количество дней подписки:"
|
||
)
|
||
await state.set_state(AdminStates.setting_promocode_value)
|
||
elif promo_type == "trial":
|
||
await message.answer(
|
||
f"🎁 <b>Промокод:</b> <code>{code}</code>\n\n"
|
||
f"Введите количество дней тестовой подписки:"
|
||
)
|
||
await state.set_state(AdminStates.setting_promocode_value)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_value(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
try:
|
||
value = int(message.text.strip())
|
||
|
||
data = await state.get_data()
|
||
promo_type = data.get('promocode_type')
|
||
|
||
if promo_type == "balance" and (value < 1 or value > 10000):
|
||
await message.answer("❌ Сумма должна быть от 1 до 10,000 рублей")
|
||
return
|
||
elif promo_type in ["days", "trial"] and (value < 1 or value > 3650):
|
||
await message.answer("❌ Количество дней должно быть от 1 до 3650")
|
||
return
|
||
|
||
await state.update_data(promocode_value=value)
|
||
|
||
await message.answer(
|
||
f"📊 Введите количество использований промокода (или 0 для безлимита):"
|
||
)
|
||
await state.set_state(AdminStates.setting_promocode_uses)
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число")
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_uses(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
try:
|
||
max_uses = int(message.text.strip())
|
||
|
||
if max_uses < 0 or max_uses > 100000:
|
||
await message.answer("❌ Количество использований должно быть от 0 до 100,000")
|
||
return
|
||
|
||
if max_uses == 0:
|
||
max_uses = 999999
|
||
|
||
await state.update_data(promocode_max_uses=max_uses)
|
||
|
||
await message.answer(
|
||
f"⏰ Введите срок действия промокода в днях (или 0 для бессрочного):"
|
||
)
|
||
await state.set_state(AdminStates.setting_promocode_expiry)
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число")
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_expiry(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
try:
|
||
expiry_days = int(message.text.strip())
|
||
|
||
if expiry_days < 0 or expiry_days > 3650:
|
||
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
code = data.get('promocode_code')
|
||
promo_type = data.get('promocode_type')
|
||
value = data.get('promocode_value', 0)
|
||
max_uses = data.get('promocode_max_uses', 1)
|
||
|
||
valid_until = None
|
||
if expiry_days > 0:
|
||
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
|
||
|
||
type_map = {
|
||
"balance": PromoCodeType.BALANCE,
|
||
"days": PromoCodeType.SUBSCRIPTION_DAYS,
|
||
"trial": PromoCodeType.TRIAL_SUBSCRIPTION
|
||
}
|
||
|
||
promocode = await create_promocode(
|
||
db=db,
|
||
code=code,
|
||
type=type_map[promo_type],
|
||
balance_bonus_kopeks=value * 100 if promo_type == "balance" else 0,
|
||
subscription_days=value if promo_type in ["days", "trial"] else 0,
|
||
max_uses=max_uses,
|
||
valid_until=valid_until,
|
||
created_by=db_user.id
|
||
)
|
||
|
||
type_names = {
|
||
"balance": "Пополнение баланса",
|
||
"days": "Дни подписки",
|
||
"trial": "Тестовая подписка"
|
||
}
|
||
|
||
summary_text = f"""
|
||
✅ <b>Промокод создан!</b>
|
||
|
||
🎫 <b>Код:</b> <code>{promocode.code}</code>
|
||
📝 <b>Тип:</b> {type_names.get(promo_type)}
|
||
"""
|
||
|
||
if promo_type == "balance":
|
||
summary_text += f"💰 <b>Сумма:</b> {settings.format_price(promocode.balance_bonus_kopeks)}\n"
|
||
elif promo_type in ["days", "trial"]:
|
||
summary_text += f"📅 <b>Дней:</b> {promocode.subscription_days}\n"
|
||
|
||
summary_text += f"📊 <b>Использований:</b> {promocode.max_uses}\n"
|
||
|
||
if promocode.valid_until:
|
||
summary_text += f"⏰ <b>Действует до:</b> {format_datetime(promocode.valid_until)}\n"
|
||
|
||
await message.answer(
|
||
summary_text,
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="🎫 К промокодам", callback_data="admin_promocodes")]
|
||
])
|
||
)
|
||
|
||
await state.clear()
|
||
logger.info(f"Создан промокод {code} администратором {db_user.telegram_id}")
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число дней")
|
||
|
||
|
||
def register_handlers(dp: Dispatcher):
|
||
dp.callback_query.register(show_promocodes_menu, F.data == "admin_promocodes")
|
||
dp.callback_query.register(show_promocodes_list, F.data == "admin_promo_list")
|
||
dp.callback_query.register(start_promocode_creation, F.data == "admin_promo_create")
|
||
dp.callback_query.register(select_promocode_type, F.data.startswith("promo_type_"))
|
||
|
||
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
|
||
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
|
||
dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_"))
|
||
dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_"))
|
||
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
|
||
|
||
dp.message.register(process_promocode_code, AdminStates.creating_promocode)
|
||
dp.message.register(process_promocode_value, AdminStates.setting_promocode_value)
|
||
dp.message.register(process_promocode_uses, AdminStates.setting_promocode_uses)
|
||
dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry)
|