Files
remnawave-bedolaga-telegram…/app/handlers/admin/promocodes.py
2025-08-31 18:36:54 +03:00

563 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)