mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 03:11:47 +00:00
Добавлена полная поддержка DISCOUNT типа в админке:
- Тип "💸 Одноразовая скидка" в селекторе
- Флоу создания: код → процент (1-100) → макс использований → срок промокода (дни) → срок скидки (часы)
- Валидация процента скидки (1-100)
- Валидация срока действия скидки (0-8760 часов)
- Отображение в списках и странице управления
- Новый стейт setting_discount_hours для ввода срока скидки
1296 lines
46 KiB
Python
1296 lines
46 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 PromoCode, PromoCodeUse, PromoCodeType, User
|
||
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, get_promocode_by_id
|
||
)
|
||
from app.database.crud.promo_group import get_promo_group_by_id, get_promo_groups_with_counts
|
||
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"
|
||
keyboard = []
|
||
|
||
for promo in promocodes:
|
||
status_emoji = "✅" if promo.is_active else "❌"
|
||
type_emoji = {
|
||
"balance": "💰",
|
||
"subscription_days": "📅",
|
||
"trial_subscription": "🎁",
|
||
"promo_group": "🏷️",
|
||
"discount": "💸"
|
||
}.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"
|
||
elif promo.type == PromoCodeType.PROMO_GROUP.value:
|
||
if promo.promo_group:
|
||
text += f"🏷️ Промогруппа: {promo.promo_group.name}\n"
|
||
elif promo.type == PromoCodeType.DISCOUNT.value:
|
||
discount_hours = promo.subscription_days
|
||
if discount_hours > 0:
|
||
text += f"💸 Скидка: {promo.balance_bonus_kopeks}% ({discount_hours} ч.)\n"
|
||
else:
|
||
text += f"💸 Скидка: {promo.balance_bonus_kopeks}% (до покупки)\n"
|
||
|
||
if promo.valid_until:
|
||
text += f"⏰ До: {format_datetime(promo.valid_until)}\n"
|
||
|
||
keyboard.append([
|
||
types.InlineKeyboardButton(
|
||
text=f"🎫 {promo.code}",
|
||
callback_data=f"promo_manage_{promo.id}"
|
||
)
|
||
])
|
||
|
||
text += "\n"
|
||
|
||
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_promocodes_list_page(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
"""Обработчик пагинации списка промокодов."""
|
||
try:
|
||
page = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
page = 1
|
||
await show_promocodes_list(callback, db_user, db, page=page)
|
||
|
||
|
||
@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 get_promocode_by_id(db, 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": "🎁",
|
||
"promo_group": "🏷️",
|
||
"discount": "💸"
|
||
}.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"
|
||
elif promo.type == PromoCodeType.PROMO_GROUP.value:
|
||
if promo.promo_group:
|
||
text += f"🏷️ <b>Промогруппа:</b> {promo.promo_group.name} (приоритет: {promo.promo_group.priority})\n"
|
||
elif promo.promo_group_id:
|
||
text += f"🏷️ <b>Промогруппа ID:</b> {promo.promo_group_id} (не найдена)\n"
|
||
elif promo.type == PromoCodeType.DISCOUNT.value:
|
||
discount_hours = promo.subscription_days
|
||
if discount_hours > 0:
|
||
text += f"💸 <b>Скидка:</b> {promo.balance_bonus_kopeks}% (срок: {discount_hours} ч.)\n"
|
||
else:
|
||
text += f"💸 <b>Скидка:</b> {promo.balance_bonus_kopeks}% (до первой покупки)\n"
|
||
|
||
if promo.valid_until:
|
||
text += f"⏰ <b>Действует до:</b> {format_datetime(promo.valid_until)}\n"
|
||
|
||
first_purchase_only = getattr(promo, 'first_purchase_only', False)
|
||
first_purchase_emoji = "✅" if first_purchase_only else "❌"
|
||
text += f"🆕 <b>Только первая покупка:</b> {first_purchase_emoji}\n"
|
||
|
||
text += f"📅 <b>Создан:</b> {format_datetime(promo.created_at)}\n"
|
||
|
||
first_purchase_btn_text = "🆕 Первая покупка: ✅" if first_purchase_only else "🆕 Первая покупка: ❌"
|
||
|
||
keyboard = [
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="✏️ Редактировать",
|
||
callback_data=f"promo_edit_{promo.id}"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text="🔄 Переключить статус",
|
||
callback_data=f"promo_toggle_{promo.id}"
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=first_purchase_btn_text,
|
||
callback_data=f"promo_toggle_first_{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 show_promocode_edit_menu(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
promo = await get_promocode_by_id(db, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
text = f"""
|
||
✏️ <b>Редактирование промокода</b> <code>{promo.code}</code>
|
||
|
||
💰 <b>Текущие параметры:</b>
|
||
"""
|
||
|
||
if promo.type == PromoCodeType.BALANCE.value:
|
||
text += f"• Бонус: {settings.format_price(promo.balance_bonus_kopeks)}\n"
|
||
elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]:
|
||
text += f"• Дней: {promo.subscription_days}\n"
|
||
|
||
text += f"• Использований: {promo.current_uses}/{promo.max_uses}\n"
|
||
|
||
if promo.valid_until:
|
||
text += f"• До: {format_datetime(promo.valid_until)}\n"
|
||
else:
|
||
text += f"• Срок: бессрочно\n"
|
||
|
||
text += f"\nВыберите параметр для изменения:"
|
||
|
||
keyboard = [
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="📅 Дата окончания",
|
||
callback_data=f"promo_edit_date_{promo.id}"
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="📊 Количество использований",
|
||
callback_data=f"promo_edit_uses_{promo.id}"
|
||
)
|
||
]
|
||
]
|
||
|
||
if promo.type == PromoCodeType.BALANCE.value:
|
||
keyboard.insert(1, [
|
||
types.InlineKeyboardButton(
|
||
text="💰 Сумма бонуса",
|
||
callback_data=f"promo_edit_amount_{promo.id}"
|
||
)
|
||
])
|
||
elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]:
|
||
keyboard.insert(1, [
|
||
types.InlineKeyboardButton(
|
||
text="📅 Количество дней",
|
||
callback_data=f"promo_edit_days_{promo.id}"
|
||
)
|
||
])
|
||
|
||
keyboard.extend([
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text="⬅️ Назад",
|
||
callback_data=f"promo_manage_{promo.id}"
|
||
)
|
||
]
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_promocode_date(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
await state.update_data(
|
||
editing_promo_id=promo_id,
|
||
edit_action="date"
|
||
)
|
||
|
||
text = f"""
|
||
📅 <b>Изменение даты окончания промокода</b>
|
||
|
||
Введите количество дней до окончания (от текущего момента):
|
||
• Введите <b>0</b> для бессрочного промокода
|
||
• Введите положительное число для установки срока
|
||
|
||
<i>Например: 30 (промокод будет действовать 30 дней)</i>
|
||
|
||
ID промокода: {promo_id}
|
||
"""
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await state.set_state(AdminStates.setting_promocode_expiry)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_promocode_amount(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
await state.update_data(
|
||
editing_promo_id=promo_id,
|
||
edit_action="amount"
|
||
)
|
||
|
||
text = f"""
|
||
💰 <b>Изменение суммы бонуса промокода</b>
|
||
|
||
Введите новую сумму в рублях:
|
||
<i>Например: 500</i>
|
||
|
||
ID промокода: {promo_id}
|
||
"""
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await state.set_state(AdminStates.setting_promocode_value)
|
||
await callback.answer()
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_promocode_days(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
# ИСПРАВЛЕНИЕ: берем последний элемент как ID
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
await state.update_data(
|
||
editing_promo_id=promo_id,
|
||
edit_action="days"
|
||
)
|
||
|
||
text = f"""
|
||
📅 <b>Изменение количества дней подписки</b>
|
||
|
||
Введите новое количество дней:
|
||
<i>Например: 30</i>
|
||
|
||
ID промокода: {promo_id}
|
||
"""
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await state.set_state(AdminStates.setting_promocode_value)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_promocode_uses(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext
|
||
):
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
await state.update_data(
|
||
editing_promo_id=promo_id,
|
||
edit_action="uses"
|
||
)
|
||
|
||
text = f"""
|
||
📊 <b>Изменение максимального количества использований</b>
|
||
|
||
Введите новое количество использований:
|
||
• Введите <b>0</b> для безлимитных использований
|
||
• Введите положительное число для ограничения
|
||
|
||
<i>Например: 100</i>
|
||
|
||
ID промокода: {promo_id}
|
||
"""
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await state.set_state(AdminStates.setting_promocode_uses)
|
||
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": "🎁 Тестовая подписка",
|
||
"group": "🏷️ Промогруппа",
|
||
"discount": "💸 Одноразовая скидка"
|
||
}
|
||
|
||
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
|
||
|
||
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)
|
||
elif promo_type == "discount":
|
||
await message.answer(
|
||
f"💸 <b>Промокод:</b> <code>{code}</code>\n\n"
|
||
f"Введите процент скидки (1-100):"
|
||
)
|
||
await state.set_state(AdminStates.setting_promocode_value)
|
||
elif promo_type == "group":
|
||
# Show promo group selection
|
||
groups_with_counts = await get_promo_groups_with_counts(db, limit=50)
|
||
|
||
if not groups_with_counts:
|
||
await message.answer(
|
||
"❌ Промогруппы не найдены. Создайте хотя бы одну промогруппу.",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_promocodes")]
|
||
])
|
||
)
|
||
await state.clear()
|
||
return
|
||
|
||
keyboard = []
|
||
text = f"🏷️ <b>Промокод:</b> <code>{code}</code>\n\nВыберите промогруппу для назначения:\n\n"
|
||
|
||
for promo_group, user_count in groups_with_counts:
|
||
text += f"• {promo_group.name} (приоритет: {promo_group.priority}, пользователей: {user_count})\n"
|
||
keyboard.append([
|
||
types.InlineKeyboardButton(
|
||
text=f"{promo_group.name} (↑{promo_group.priority})",
|
||
callback_data=f"promo_select_group_{promo_group.id}"
|
||
)
|
||
])
|
||
|
||
keyboard.append([
|
||
types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_promocodes")
|
||
])
|
||
|
||
await message.answer(
|
||
text,
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
)
|
||
await state.set_state(AdminStates.selecting_promo_group)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promo_group_selection(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
"""Handle promo group selection for promocode"""
|
||
try:
|
||
promo_group_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промогруппы", show_alert=True)
|
||
return
|
||
|
||
promo_group = await get_promo_group_by_id(db, promo_group_id)
|
||
if not promo_group:
|
||
await callback.answer("❌ Промогруппа не найдена", show_alert=True)
|
||
return
|
||
|
||
await state.update_data(
|
||
promo_group_id=promo_group_id,
|
||
promo_group_name=promo_group.name
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
f"🏷️ <b>Промокод для промогруппы</b>\n\n"
|
||
f"Промогруппа: {promo_group.name}\n"
|
||
f"Приоритет: {promo_group.priority}\n\n"
|
||
f"📊 Введите количество использований промокода (или 0 для безлимита):"
|
||
)
|
||
|
||
await state.set_state(AdminStates.setting_promocode_uses)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_value(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
data = await state.get_data()
|
||
|
||
if data.get('editing_promo_id'):
|
||
await handle_edit_value(message, db_user, state, db)
|
||
return
|
||
|
||
try:
|
||
value = int(message.text.strip())
|
||
|
||
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
|
||
elif promo_type == "discount" and (value < 1 or value > 100):
|
||
await message.answer("❌ Процент скидки должен быть от 1 до 100")
|
||
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("❌ Введите корректное число")
|
||
|
||
|
||
async def handle_edit_value(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
data = await state.get_data()
|
||
promo_id = data.get('editing_promo_id')
|
||
edit_action = data.get('edit_action')
|
||
|
||
promo = await get_promocode_by_id(db, promo_id)
|
||
if not promo:
|
||
await message.answer("❌ Промокод не найден")
|
||
await state.clear()
|
||
return
|
||
|
||
try:
|
||
value = int(message.text.strip())
|
||
|
||
if edit_action == "amount":
|
||
if value < 1 or value > 10000:
|
||
await message.answer("❌ Сумма должна быть от 1 до 10,000 рублей")
|
||
return
|
||
|
||
await update_promocode(db, promo, balance_bonus_kopeks=value * 100)
|
||
await message.answer(
|
||
f"✅ Сумма бонуса изменена на {value}₽",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
|
||
])
|
||
)
|
||
|
||
elif edit_action == "days":
|
||
if value < 1 or value > 3650:
|
||
await message.answer("❌ Количество дней должно быть от 1 до 3650")
|
||
return
|
||
|
||
await update_promocode(db, promo, subscription_days=value)
|
||
await message.answer(
|
||
f"✅ Количество дней изменено на {value}",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
|
||
])
|
||
)
|
||
|
||
await state.clear()
|
||
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: {edit_action} = {value}")
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число")
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_uses(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
data = await state.get_data()
|
||
|
||
if data.get('editing_promo_id'):
|
||
await handle_edit_uses(message, db_user, state, db)
|
||
return
|
||
|
||
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("❌ Введите корректное число")
|
||
|
||
|
||
async def handle_edit_uses(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
data = await state.get_data()
|
||
promo_id = data.get('editing_promo_id')
|
||
|
||
promo = await get_promocode_by_id(db, promo_id)
|
||
if not promo:
|
||
await message.answer("❌ Промокод не найден")
|
||
await state.clear()
|
||
return
|
||
|
||
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
|
||
|
||
if max_uses < promo.current_uses:
|
||
await message.answer(
|
||
f"❌ Новый лимит ({max_uses}) не может быть меньше текущих использований ({promo.current_uses})"
|
||
)
|
||
return
|
||
|
||
await update_promocode(db, promo, max_uses=max_uses)
|
||
|
||
uses_text = "безлимитное" if max_uses == 999999 else str(max_uses)
|
||
await message.answer(
|
||
f"✅ Максимальное количество использований изменено на {uses_text}",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
|
||
])
|
||
)
|
||
|
||
await state.clear()
|
||
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: max_uses = {max_uses}")
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число")
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_promocode_expiry(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
data = await state.get_data()
|
||
|
||
if data.get('editing_promo_id'):
|
||
await handle_edit_expiry(message, db_user, state, db)
|
||
return
|
||
|
||
try:
|
||
expiry_days = int(message.text.strip())
|
||
|
||
if expiry_days < 0 or expiry_days > 3650:
|
||
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
|
||
return
|
||
|
||
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)
|
||
promo_group_id = data.get('promo_group_id')
|
||
promo_group_name = data.get('promo_group_name')
|
||
|
||
# Для DISCOUNT типа нужно дополнительно спросить срок действия скидки в часах
|
||
if promo_type == "discount":
|
||
await state.update_data(promocode_expiry_days=expiry_days)
|
||
await message.answer(
|
||
f"⏰ <b>Промокод:</b> <code>{code}</code>\n\n"
|
||
f"Введите срок действия скидки в часах (0-8760):\n"
|
||
f"0 = бессрочно до первой покупки"
|
||
)
|
||
await state.set_state(AdminStates.setting_discount_hours)
|
||
return
|
||
|
||
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,
|
||
"group": PromoCodeType.PROMO_GROUP
|
||
}
|
||
|
||
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,
|
||
promo_group_id=promo_group_id if promo_type == "group" else None
|
||
)
|
||
|
||
type_names = {
|
||
"balance": "Пополнение баланса",
|
||
"days": "Дни подписки",
|
||
"trial": "Тестовая подписка",
|
||
"group": "Промогруппа"
|
||
}
|
||
|
||
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"
|
||
elif promo_type == "group" and promo_group_name:
|
||
summary_text += f"🏷️ <b>Промогруппа:</b> {promo_group_name}\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("❌ Введите корректное число дней")
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_discount_hours(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
"""Обработчик ввода срока действия скидки в часах для DISCOUNT промокода."""
|
||
data = await state.get_data()
|
||
|
||
try:
|
||
discount_hours = int(message.text.strip())
|
||
|
||
if discount_hours < 0 or discount_hours > 8760:
|
||
await message.answer("❌ Срок действия скидки должен быть от 0 до 8760 часов")
|
||
return
|
||
|
||
code = data.get('promocode_code')
|
||
value = data.get('promocode_value', 0) # Процент скидки
|
||
max_uses = data.get('promocode_max_uses', 1)
|
||
expiry_days = data.get('promocode_expiry_days', 0)
|
||
|
||
valid_until = None
|
||
if expiry_days > 0:
|
||
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
|
||
|
||
# Создаем DISCOUNT промокод
|
||
# balance_bonus_kopeks = процент скидки (НЕ копейки!)
|
||
# subscription_days = срок действия скидки в часах (НЕ дни!)
|
||
promocode = await create_promocode(
|
||
db=db,
|
||
code=code,
|
||
type=PromoCodeType.DISCOUNT,
|
||
balance_bonus_kopeks=value, # Процент (1-100)
|
||
subscription_days=discount_hours, # Часы (0-8760)
|
||
max_uses=max_uses,
|
||
valid_until=valid_until,
|
||
created_by=db_user.id,
|
||
promo_group_id=None
|
||
)
|
||
|
||
summary_text = f"""
|
||
✅ <b>Промокод создан!</b>
|
||
|
||
🎫 <b>Код:</b> <code>{promocode.code}</code>
|
||
📝 <b>Тип:</b> Одноразовая скидка
|
||
💸 <b>Скидка:</b> {promocode.balance_bonus_kopeks}%
|
||
"""
|
||
|
||
if discount_hours > 0:
|
||
summary_text += f"⏰ <b>Срок скидки:</b> {discount_hours} ч.\n"
|
||
else:
|
||
summary_text += f"⏰ <b>Срок скидки:</b> до первой покупки\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"Создан DISCOUNT промокод {code} ({value}%, {discount_hours}ч) администратором {db_user.telegram_id}")
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число часов")
|
||
|
||
|
||
async def handle_edit_expiry(
|
||
message: types.Message,
|
||
db_user: User,
|
||
state: FSMContext,
|
||
db: AsyncSession
|
||
):
|
||
data = await state.get_data()
|
||
promo_id = data.get('editing_promo_id')
|
||
|
||
promo = await get_promocode_by_id(db, promo_id)
|
||
if not promo:
|
||
await message.answer("❌ Промокод не найден")
|
||
await state.clear()
|
||
return
|
||
|
||
try:
|
||
expiry_days = int(message.text.strip())
|
||
|
||
if expiry_days < 0 or expiry_days > 3650:
|
||
await message.answer("❌ Срок действия должен быть от 0 до 3650 дней")
|
||
return
|
||
|
||
valid_until = None
|
||
if expiry_days > 0:
|
||
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
|
||
|
||
await update_promocode(db, promo, valid_until=valid_until)
|
||
|
||
if valid_until:
|
||
expiry_text = f"до {format_datetime(valid_until)}"
|
||
else:
|
||
expiry_text = "бессрочно"
|
||
|
||
await message.answer(
|
||
f"✅ Срок действия промокода изменен: {expiry_text}",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")]
|
||
])
|
||
)
|
||
|
||
await state.clear()
|
||
logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: expiry = {expiry_days} дней")
|
||
|
||
except ValueError:
|
||
await message.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 get_promocode_by_id(db, 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 toggle_promocode_first_purchase(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
"""Переключает режим 'только для первой покупки'."""
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
|
||
promo = await get_promocode_by_id(db, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
new_status = not getattr(promo, 'first_purchase_only', False)
|
||
await update_promocode(db, promo, first_purchase_only=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
|
||
):
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
promo = await get_promocode_by_id(db, promo_id)
|
||
if not promo:
|
||
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||
return
|
||
|
||
text = f"""
|
||
⚠️ <b>Подтверждение удаления</b>
|
||
|
||
Вы действительно хотите удалить промокод <code>{promo.code}</code>?
|
||
|
||
📊 <b>Информация о промокоде:</b>
|
||
• Использований: {promo.current_uses}/{promo.max_uses}
|
||
• Статус: {'Активен' if promo.is_active else 'Неактивен'}
|
||
|
||
<b>⚠️ Внимание:</b> Это действие нельзя отменить!
|
||
|
||
ID: {promo_id}
|
||
"""
|
||
|
||
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
|
||
):
|
||
try:
|
||
promo_id = int(callback.data.split('_')[-1])
|
||
except (ValueError, IndexError):
|
||
await callback.answer("❌ Ошибка получения ID промокода", show_alert=True)
|
||
return
|
||
|
||
promo = await get_promocode_by_id(db, 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 get_promocode_by_id(db, 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>
|
||
"""
|
||
|
||
if stats['recent_uses']:
|
||
for use in stats['recent_uses'][:5]:
|
||
use_date = format_datetime(use.used_at)
|
||
|
||
if hasattr(use, 'user_username') and use.user_username:
|
||
user_display = f"@{use.user_username}"
|
||
elif hasattr(use, 'user_full_name') and use.user_full_name:
|
||
user_display = use.user_full_name
|
||
elif hasattr(use, 'user_telegram_id'):
|
||
user_display = f"ID{use.user_telegram_id}"
|
||
else:
|
||
user_display = f"ID{use.user_id}"
|
||
|
||
text += f"- {use_date} | {user_display}\n"
|
||
else:
|
||
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 show_general_promocode_stats(
|
||
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}
|
||
|
||
Для детальной статистики выберите конкретный промокод из списка.
|
||
"""
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||
[
|
||
types.InlineKeyboardButton(text="🎫 К промокодам", callback_data="admin_promo_list")
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_promocodes")
|
||
]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
await callback.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(show_promocodes_list_page, F.data.startswith("admin_promo_list_page_"))
|
||
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(process_promo_group_selection, F.data.startswith("promo_select_group_"))
|
||
|
||
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
|
||
dp.callback_query.register(toggle_promocode_first_purchase, F.data.startswith("promo_toggle_first_"))
|
||
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
|
||
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
|
||
|
||
dp.callback_query.register(start_edit_promocode_date, F.data.startswith("promo_edit_date_"))
|
||
dp.callback_query.register(start_edit_promocode_amount, F.data.startswith("promo_edit_amount_"))
|
||
dp.callback_query.register(start_edit_promocode_days, F.data.startswith("promo_edit_days_"))
|
||
dp.callback_query.register(start_edit_promocode_uses, F.data.startswith("promo_edit_uses_"))
|
||
dp.callback_query.register(show_general_promocode_stats, F.data == "admin_promo_general_stats")
|
||
|
||
dp.callback_query.register(
|
||
show_promocode_edit_menu,
|
||
F.data.regexp(r"^promo_edit_\d+$")
|
||
)
|
||
|
||
dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_"))
|
||
dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_"))
|
||
|
||
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)
|
||
dp.message.register(process_discount_hours, AdminStates.setting_discount_hours)
|
||
|