- Changed the reference from `created_at` to `activated_at` in the promo activations handler to accurately reflect the activation date. - Updated the localization files for both English and Russian to remove HTML tags from the promo code toggle success message, enhancing clarity.
1612 lines
67 KiB
Python
1612 lines
67 KiB
Python
import logging
|
||
import random
|
||
import string
|
||
from aiogram import Router, F, types, Bot
|
||
from aiogram.filters import StateFilter
|
||
from aiogram.fsm.context import FSMContext
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Optional
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from config.settings import Settings
|
||
|
||
from db.dal import promo_code_dal
|
||
|
||
from bot.states.admin_states import AdminStates
|
||
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
|
||
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
||
from bot.middlewares.i18n import JsonI18n
|
||
|
||
router = Router(name="admin_promo_codes_router")
|
||
|
||
|
||
async def create_promo_prompt_handler(callback: types.CallbackQuery,
|
||
state: FSMContext, i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error preparing promo creation.",
|
||
show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
# Step 1: Ask for promo code
|
||
prompt_text = _(
|
||
"admin_promo_step1_code",
|
||
default="🎟 <b>Создание промокода</b>\n\n<b>Шаг 1 из 4:</b> Код промокода\n\nВведите код промокода (3-30 символов, только буквы и цифры):"
|
||
)
|
||
|
||
try:
|
||
await callback.message.edit_text(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
except Exception as e:
|
||
logging.warning(
|
||
f"Could not edit message for promo prompt: {e}. Sending new.")
|
||
await callback.message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
await state.set_state(AdminStates.waiting_for_promo_code)
|
||
|
||
|
||
# Step 1: Process promo code
|
||
@router.message(AdminStates.waiting_for_promo_code, F.text)
|
||
async def process_promo_code_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
code_str = message.text.strip().upper()
|
||
if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
|
||
await message.answer(_(
|
||
"admin_promo_invalid_code_format",
|
||
default="❌ Код промокода должен содержать 3-30 символов (только буквы и цифры)"
|
||
))
|
||
return
|
||
|
||
# Check if code already exists
|
||
existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str)
|
||
if existing_promo:
|
||
await message.answer(_(
|
||
"admin_promo_code_already_exists",
|
||
default="❌ Промокод с таким кодом уже существует"
|
||
))
|
||
return
|
||
|
||
await state.update_data(promo_code=code_str)
|
||
|
||
# Step 2: Ask for bonus days
|
||
prompt_text = _(
|
||
"admin_promo_step2_bonus_days",
|
||
default="🎟 <b>Создание промокода</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКод: <b>{code}</b>\n\nВведите количество бонусных дней (1-365):",
|
||
code=code_str
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_promo_bonus_days)
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error processing promo code: {e}")
|
||
await message.answer(_(
|
||
"admin_promo_processing_error",
|
||
default="❌ Ошибка обработки промокода"
|
||
))
|
||
|
||
# Step 2: Process bonus days
|
||
@router.message(AdminStates.waiting_for_promo_bonus_days, F.text)
|
||
async def process_promo_bonus_days_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
bonus_days = int(message.text.strip())
|
||
if bonus_days <= 0 or bonus_days > 365:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_bonus_days",
|
||
default="❌ Бонусные дни должны быть от 1 до 365"
|
||
))
|
||
return
|
||
|
||
data = await state.get_data()
|
||
await state.update_data(bonus_days=bonus_days)
|
||
|
||
# Step 3: Ask for max activations
|
||
prompt_text = _(
|
||
"admin_promo_step3_max_activations",
|
||
default="🎟 <b>Создание промокода</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКод: <b>{code}</b>\nБонусные дни: <b>{bonus_days}</b>\n\nВведите максимальное количество активаций (1-10000):",
|
||
code=data['promo_code'],
|
||
bonus_days=bonus_days
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_promo_max_activations)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_bonus_days",
|
||
default="❌ Бонусные дни должны быть от 1 до 365"
|
||
))
|
||
|
||
# Step 3: Process max activations
|
||
@router.message(AdminStates.waiting_for_promo_max_activations, F.text)
|
||
async def process_promo_max_activations_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
max_activations = int(message.text.strip())
|
||
if max_activations <= 0 or max_activations > 10000:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_max_activations",
|
||
default="❌ Максимальные активации должны быть от 1 до 10000"
|
||
))
|
||
return
|
||
|
||
data = await state.get_data()
|
||
await state.update_data(max_activations=max_activations)
|
||
|
||
# Step 4: Ask for validity days (with buttons)
|
||
keyboard = InlineKeyboardBuilder()
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_unlimited_validity", default="♾️ Неограниченно"),
|
||
callback_data="promo_unlimited_validity"
|
||
)
|
||
)
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_set_validity", default="⏰ Установить срок"),
|
||
callback_data="promo_set_validity"
|
||
)
|
||
)
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_panel_back_button", default="⬅️ Назад"),
|
||
callback_data="admin_panel_back"
|
||
)
|
||
)
|
||
|
||
prompt_text = _(
|
||
"admin_promo_step4_validity",
|
||
default="🎟 <b>Создание промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nБонусные дни: <b>{bonus_days}</b>\nМакс. активации: <b>{max_activations}</b>\n\nВыберите срок действия промокода:",
|
||
code=data['promo_code'],
|
||
bonus_days=data['bonus_days'],
|
||
max_activations=max_activations
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=keyboard.as_markup(),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_promo_validity_days)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_max_activations",
|
||
default="❌ Максимальные активации должны быть от 1 до 10000"
|
||
))
|
||
|
||
# Handle validity choice buttons
|
||
@router.callback_query(F.data == "promo_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
|
||
async def process_promo_unlimited_validity(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
await state.update_data(validity_days=None)
|
||
await create_promo_code_final(callback, state, i18n_data, settings, session)
|
||
|
||
@router.callback_query(F.data == "promo_set_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
|
||
async def process_promo_set_validity(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await callback.answer("Language service error.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
prompt_text = _(
|
||
"admin_promo_enter_validity_days",
|
||
default="⏰ Введите количество дней действия промокода (1-365):"
|
||
)
|
||
|
||
try:
|
||
await callback.message.edit_text(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
except Exception as e:
|
||
await callback.message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
await callback.answer()
|
||
|
||
# Step 4: Process validity days (when user chooses to set)
|
||
@router.message(AdminStates.waiting_for_promo_validity_days, F.text)
|
||
async def process_promo_validity_days_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
validity_days = int(message.text.strip())
|
||
if validity_days <= 0 or validity_days > 365:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_validity_days",
|
||
default="❌ Дни действия должны быть от 1 до 365"
|
||
))
|
||
return
|
||
|
||
await state.update_data(validity_days=validity_days)
|
||
|
||
# Create a fake callback for consistency
|
||
fake_callback = types.CallbackQuery(
|
||
id="fake",
|
||
from_user=message.from_user,
|
||
chat_instance="fake",
|
||
message=message,
|
||
data="fake"
|
||
)
|
||
|
||
await create_promo_code_final(fake_callback, state, i18n_data, settings, session)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_validity_days",
|
||
default="❌ Дни действия должны быть от 1 до 365"
|
||
))
|
||
|
||
# Description handlers removed - not needed for this model
|
||
|
||
async def create_promo_code_final(callback_or_message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
"""Final step to create the promo code"""
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
if hasattr(callback_or_message, 'answer'):
|
||
await callback_or_message.answer("Language service error.", show_alert=True)
|
||
else:
|
||
await callback_or_message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
promo_code = data['promo_code']
|
||
bonus_days = data['bonus_days']
|
||
max_activations = data['max_activations']
|
||
validity_days = data.get('validity_days')
|
||
|
||
# Create validity date
|
||
valid_until_date: Optional[datetime] = None
|
||
valid_until_str_display = _("admin_promo_valid_indefinitely", default="Неограниченно")
|
||
|
||
if validity_days:
|
||
valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
|
||
valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
|
||
|
||
# Show creating message
|
||
creating_text = _(
|
||
"admin_promo_creating",
|
||
default="⏳ Создаю промокод..."
|
||
)
|
||
|
||
if hasattr(callback_or_message, 'message'):
|
||
try:
|
||
await callback_or_message.message.edit_text(creating_text)
|
||
except:
|
||
await callback_or_message.message.answer(creating_text)
|
||
if hasattr(callback_or_message, 'answer'):
|
||
await callback_or_message.answer()
|
||
else:
|
||
await callback_or_message.answer(creating_text)
|
||
|
||
# Create promo code
|
||
try:
|
||
promo_data = {
|
||
"code": promo_code,
|
||
"bonus_days": bonus_days,
|
||
"max_activations": max_activations,
|
||
"current_activations": 0,
|
||
"is_active": True,
|
||
"valid_until": valid_until_date,
|
||
"created_by_admin_id": callback_or_message.from_user.id if hasattr(callback_or_message, 'from_user') and callback_or_message.from_user else 0
|
||
}
|
||
|
||
await promo_code_dal.create_promo_code(session, promo_data)
|
||
await session.commit()
|
||
|
||
# Send success message
|
||
success_text = _(
|
||
"admin_promo_created_success",
|
||
default="✅ Промокод создан!\n\n🎟 Код: <b>{code}</b>\n🎁 Бонусные дни: <b>{days}</b>\n🔢 Максимальные активации: <b>{max_act}</b>\n⏰ Действует до: <b>{validity}</b>",
|
||
code=promo_code,
|
||
days=bonus_days,
|
||
max_act=max_activations,
|
||
validity=valid_until_str_display
|
||
)
|
||
|
||
if hasattr(callback_or_message, 'message'):
|
||
target_message = callback_or_message.message
|
||
else:
|
||
target_message = callback_or_message
|
||
|
||
await target_message.answer(
|
||
success_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error creating promo code: {e}")
|
||
await session.rollback()
|
||
error_text = _(
|
||
"admin_promo_creation_failed",
|
||
default="❌ Ошибка создания промокода: {error}",
|
||
error=str(e)
|
||
)
|
||
|
||
if hasattr(callback_or_message, 'message'):
|
||
target_message = callback_or_message.message
|
||
else:
|
||
target_message = callback_or_message
|
||
|
||
await target_message.answer(
|
||
error_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
# Legacy handler removed - using step-by-step creation now
|
||
|
||
|
||
async def view_promo_codes_handler(callback: types.CallbackQuery,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error displaying promo codes.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
promo_models = await promo_code_dal.get_all_active_promo_codes(session,
|
||
limit=20,
|
||
offset=0)
|
||
|
||
if not promo_models:
|
||
await callback.message.edit_text(
|
||
_("admin_no_active_promos"),
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||
await callback.answer()
|
||
return
|
||
|
||
response_text_parts = [f"<b>{_('admin_active_promos_list_header')}</b>\n"]
|
||
for promo in promo_models:
|
||
valid_until_display_text = _("admin_promo_valid_indefinitely")
|
||
if promo.valid_until:
|
||
|
||
valid_until_display_text = promo.valid_until.strftime('%Y-%m-%d')
|
||
|
||
response_text_parts.append(
|
||
_("admin_promo_list_item",
|
||
code=promo.code,
|
||
bonus=promo.bonus_days,
|
||
current=promo.current_activations,
|
||
max=promo.max_activations,
|
||
valid_until=valid_until_display_text))
|
||
|
||
final_text = "\n".join(response_text_parts)
|
||
try:
|
||
await callback.message.edit_text(
|
||
final_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
except Exception as e:
|
||
logging.warning(
|
||
f"Failed to edit message for promo list: {e}. Sending new.")
|
||
if callback.message:
|
||
await callback.message.answer(
|
||
final_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(
|
||
current_lang, i18n),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
|
||
|
||
# New unified promo management system
|
||
@router.callback_query(F.data == "admin_action:promo_management")
|
||
async def promo_management_handler(callback: types.CallbackQuery,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
"""Show list of all promo codes for management"""
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error displaying promo codes.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
# Get ALL promo codes (including inactive)
|
||
promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0)
|
||
if not promo_models:
|
||
await callback.message.edit_text(
|
||
_("admin_promo_management_empty", default="📭 Промокоды отсутствуют"),
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||
await callback.answer()
|
||
return
|
||
|
||
kb = InlineKeyboardBuilder()
|
||
for promo in promo_models:
|
||
# Show promo code with status indicator
|
||
status_emoji = "✅" if promo.is_active else "🚫"
|
||
if promo.valid_until and promo.valid_until < datetime.now(timezone.utc):
|
||
status_emoji = "⏰" # Expired
|
||
elif promo.current_activations >= promo.max_activations:
|
||
status_emoji = "🔄" # Used up
|
||
|
||
button_text = f"{status_emoji} {promo.code} ({promo.current_activations}/{promo.max_activations})"
|
||
kb.row(
|
||
InlineKeyboardButton(
|
||
text=button_text,
|
||
callback_data=f"promo_detail:{promo.promo_code_id}")
|
||
)
|
||
|
||
kb.row(
|
||
InlineKeyboardButton(text=_("back_to_admin_panel_button", default="⬅️ Назад"),
|
||
callback_data="admin_action:main"))
|
||
|
||
await callback.message.edit_text(
|
||
_("admin_promo_management_title", default="🎟 <b>Управление промокодами</b>\n\nВыберите промокод для детального просмотра:"),
|
||
reply_markup=kb.as_markup(),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
|
||
|
||
@router.callback_query(F.data.startswith("promo_detail:"))
|
||
async def promo_detail_handler(callback: types.CallbackQuery,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
"""Show detailed promo code information with management options"""
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error displaying promo details.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
promo_id = int(callback.data.split(":")[1])
|
||
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
|
||
if not promo:
|
||
await callback.answer(_("admin_promo_not_found", default="Промокод не найден"), show_alert=True)
|
||
return
|
||
|
||
# Determine status
|
||
status = _("admin_promo_status_active", default="✅ Активен")
|
||
if not promo.is_active:
|
||
status = _("admin_promo_status_inactive", default="🚫 Неактивен")
|
||
elif promo.valid_until and promo.valid_until < datetime.now(timezone.utc):
|
||
status = _("admin_promo_status_expired", default="⏰ Истек")
|
||
elif promo.current_activations >= promo.max_activations:
|
||
status = _("admin_promo_status_used_up", default="🔄 Исчерпан")
|
||
|
||
# Format validity
|
||
validity = _("admin_promo_valid_indefinitely", default="Неограниченно")
|
||
if promo.valid_until:
|
||
validity = promo.valid_until.strftime('%Y-%m-%d %H:%M')
|
||
|
||
# Format created date
|
||
created_date = promo.created_at.strftime('%Y-%m-%d %H:%M') if promo.created_at else "N/A"
|
||
creator = f"Admin {promo.created_by_admin_id}" if promo.created_by_admin_id else "N/A"
|
||
|
||
# Build card text
|
||
card_text = _(
|
||
"admin_promo_card_title",
|
||
default="🎟 <b>Промокод: {code}</b>",
|
||
code=promo.code
|
||
) + "\n\n"
|
||
|
||
card_text += _(
|
||
"admin_promo_card_bonus_days",
|
||
default="🎁 Бонусные дни: <b>{days}</b>",
|
||
days=promo.bonus_days
|
||
) + "\n"
|
||
|
||
card_text += _(
|
||
"admin_promo_card_activations",
|
||
default="🔢 Активации: <b>{current}/{max}</b>",
|
||
current=promo.current_activations,
|
||
max=promo.max_activations
|
||
) + "\n"
|
||
|
||
card_text += _(
|
||
"admin_promo_card_validity",
|
||
default="⏰ Действует до: <b>{validity}</b>",
|
||
validity=validity
|
||
) + "\n"
|
||
|
||
card_text += _(
|
||
"admin_promo_card_status",
|
||
default="📊 Статус: <b>{status}</b>",
|
||
status=status
|
||
) + "\n"
|
||
|
||
card_text += _(
|
||
"admin_promo_card_created",
|
||
default="📅 Создан: <b>{created}</b>",
|
||
created=created_date
|
||
) + "\n"
|
||
|
||
card_text += _(
|
||
"admin_promo_card_created_by",
|
||
default="👤 Создал: <b>{creator}</b>",
|
||
creator=creator
|
||
)
|
||
|
||
# Build keyboard
|
||
kb = InlineKeyboardBuilder()
|
||
|
||
# Row 1: Edit and Toggle status
|
||
kb.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_edit_button", default="✏️ Редактировать"),
|
||
callback_data=f"promo_edit:{promo_id}"),
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_toggle_status_button", default="🔄 Вкл/Выкл"),
|
||
callback_data=f"promo_toggle:{promo_id}")
|
||
)
|
||
|
||
# Row 2: View activations and Delete
|
||
kb.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_view_activations_button", default="📋 Активации"),
|
||
callback_data=f"promo_activations:{promo_id}"),
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_delete_button", default="🗑 Удалить"),
|
||
callback_data=f"promo_delete:{promo_id}")
|
||
)
|
||
|
||
# Row 3: Back to list
|
||
kb.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_back_to_list_button", default="⬅️ К списку"),
|
||
callback_data="admin_action:promo_management")
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
card_text,
|
||
reply_markup=kb.as_markup(),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
|
||
|
||
# Legacy manage_promo_codes_handler - keeping for compatibility
|
||
@router.callback_query(F.data == "admin_action:manage_promos")
|
||
async def manage_promo_codes_handler(callback: types.CallbackQuery,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
# Redirect to new unified handler
|
||
await promo_management_handler(callback, i18n_data, settings, session)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("promo_edit:"))
|
||
async def promo_edit_select_handler(callback: types.CallbackQuery, state: FSMContext,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
promo_id = int(callback.data.split(":")[1])
|
||
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not promo or not callback.message:
|
||
await callback.answer("Error", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
await state.update_data(
|
||
edit_promo_id=promo_id,
|
||
original_code=promo.code,
|
||
original_bonus_days=promo.bonus_days,
|
||
original_max_activations=promo.max_activations,
|
||
original_valid_until=promo.valid_until
|
||
)
|
||
|
||
# Step 1: Edit code
|
||
prompt_text = _(
|
||
"admin_promo_edit_step1_code",
|
||
default="✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 1 из 4:</b> Код промокода\n\nТекущий код: <b>{current_code}</b>\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:",
|
||
current_code=promo.code
|
||
)
|
||
|
||
await state.set_state(AdminStates.waiting_for_promo_edit_code)
|
||
await callback.message.edit_text(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
|
||
|
||
# Step 1: Process edited code
|
||
@router.message(AdminStates.waiting_for_promo_edit_code, F.text)
|
||
async def process_promo_edit_code_handler(message: types.Message, state: FSMContext,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
original_code = data.get("original_code")
|
||
|
||
try:
|
||
code_str = message.text.strip().upper()
|
||
if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
|
||
await message.answer(_(
|
||
"admin_promo_invalid_code_format",
|
||
default="❌ Код промокода должен содержать 3-30 символов (только буквы и цифры)"
|
||
))
|
||
return
|
||
|
||
# Check if code already exists (except current promo)
|
||
if code_str != original_code:
|
||
existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str)
|
||
if existing_promo:
|
||
await message.answer(_(
|
||
"admin_promo_code_already_exists",
|
||
default="❌ Промокод с таким кодом уже существует"
|
||
))
|
||
return
|
||
|
||
await state.update_data(new_code=code_str)
|
||
|
||
# Step 2: Ask for bonus days
|
||
prompt_text = _(
|
||
"admin_promo_edit_step2_bonus_days",
|
||
default="✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКод: <b>{code}</b>\nТекущие дни: <b>{current_days}</b>\n\nВведите количество бонусных дней (1-365):",
|
||
code=code_str,
|
||
current_days=data.get("original_bonus_days")
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_promo_edit_bonus_days)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_code_format",
|
||
default="❌ Код промокода должен содержать 3-30 символов (только буквы и цифры)"
|
||
))
|
||
|
||
|
||
# Step 2: Process edited bonus days
|
||
@router.message(AdminStates.waiting_for_promo_edit_bonus_days, F.text)
|
||
async def process_promo_edit_bonus_days_handler(message: types.Message, state: FSMContext,
|
||
i18n_data: dict, settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
|
||
try:
|
||
bonus_days = int(message.text.strip())
|
||
if not (1 <= bonus_days <= 365):
|
||
await message.answer(_(
|
||
"admin_promo_invalid_bonus_days",
|
||
default="❌ Количество дней должно быть от 1 до 365"
|
||
))
|
||
return
|
||
|
||
await state.update_data(new_bonus_days=bonus_days)
|
||
|
||
# Step 3: Ask for max activations
|
||
prompt_text = _(
|
||
"admin_promo_edit_step3_max_activations",
|
||
default="✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nТекущие активации: <b>{current_max}</b>\n\nВведите максимальное количество активаций (1-10000):",
|
||
code=data.get("new_code"),
|
||
days=bonus_days,
|
||
current_max=data.get("original_max_activations")
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_promo_edit_max_activations)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_bonus_days",
|
||
default="❌ Количество дней должно быть от 1 до 365"
|
||
))
|
||
|
||
|
||
# Step 3: Process edited max activations
|
||
@router.message(AdminStates.waiting_for_promo_edit_max_activations, F.text)
|
||
async def process_promo_edit_max_activations_handler(message: types.Message, state: FSMContext,
|
||
i18n_data: dict, settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
|
||
try:
|
||
max_activations = int(message.text.strip())
|
||
if not (1 <= max_activations <= 10000):
|
||
await message.answer(_(
|
||
"admin_promo_invalid_max_activations",
|
||
default="❌ Количество активаций должно быть от 1 до 10000"
|
||
))
|
||
return
|
||
|
||
await state.update_data(new_max_activations=max_activations)
|
||
|
||
# Step 4: Ask for validity days (with buttons)
|
||
current_valid_until = data.get("original_valid_until")
|
||
current_validity_str = _("admin_promo_valid_indefinitely", default="Неограниченно")
|
||
if current_valid_until:
|
||
current_validity_str = current_valid_until.strftime('%Y-%m-%d')
|
||
|
||
keyboard = InlineKeyboardBuilder()
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_edit_unlimited_validity", default="♾️ Неограниченно"),
|
||
callback_data="promo_edit_unlimited_validity"
|
||
)
|
||
)
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_edit_set_validity", default="⏰ Установить срок"),
|
||
callback_data="promo_edit_set_validity"
|
||
)
|
||
)
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_panel_back_button", default="⬅️ Назад"),
|
||
callback_data="admin_panel_back"
|
||
)
|
||
)
|
||
|
||
prompt_text = _(
|
||
"admin_promo_edit_step4_validity",
|
||
default="✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nАктивации: <b>{max_act}</b>\nТекущий срок: <b>{current_validity}</b>\n\nВыберите срок действия промокода:",
|
||
code=data.get("new_code"),
|
||
days=data.get("new_bonus_days"),
|
||
max_act=max_activations,
|
||
current_validity=current_validity_str
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=keyboard.as_markup(),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_promo_edit_validity_days)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_max_activations",
|
||
default="❌ Количество активаций должно быть от 1 до 10000"
|
||
))
|
||
|
||
|
||
# Handle edit validity choice buttons
|
||
@router.callback_query(F.data == "promo_edit_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_edit_validity_days))
|
||
async def process_promo_edit_unlimited_validity(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await callback.answer("Language service error.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
promo_id = data.get("edit_promo_id")
|
||
|
||
# Update promo code with unlimited validity
|
||
update_data = {
|
||
"code": data.get("new_code"),
|
||
"bonus_days": data.get("new_bonus_days"),
|
||
"max_activations": data.get("new_max_activations"),
|
||
"valid_until": None, # Unlimited
|
||
}
|
||
|
||
# Show updating message
|
||
try:
|
||
await callback.message.edit_text(
|
||
_("admin_promo_creating", default="⏳ Обновляю промокод..."),
|
||
)
|
||
except:
|
||
await callback.message.answer(
|
||
_("admin_promo_creating", default="⏳ Обновляю промокод..."),
|
||
)
|
||
await callback.answer()
|
||
|
||
updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
|
||
if updated:
|
||
await session.commit()
|
||
|
||
success_text = _(
|
||
"admin_promo_updated_success",
|
||
default="✅ Промокод обновлен!\n\n🎟 Код: <b>{code}</b>\n🎁 Бонусные дни: <b>{days}</b>\n🔢 Максимальные активации: <b>{max_act}</b>\n⏰ Действует до: <b>{validity}</b>",
|
||
code=data.get("new_code"),
|
||
days=data.get("new_bonus_days"),
|
||
max_act=data.get("new_max_activations"),
|
||
validity=_("admin_promo_valid_indefinitely", default="Неограниченно")
|
||
)
|
||
|
||
await callback.message.answer(
|
||
success_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
else:
|
||
await session.rollback()
|
||
await callback.message.answer(_(
|
||
"admin_promo_update_failed",
|
||
default="❌ Ошибка обновления промокода"
|
||
))
|
||
|
||
await state.clear()
|
||
|
||
@router.callback_query(F.data == "promo_edit_set_validity", StateFilter(AdminStates.waiting_for_promo_edit_validity_days))
|
||
async def process_promo_edit_set_validity(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await callback.answer("Language service error.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
prompt_text = _(
|
||
"admin_promo_edit_enter_validity_days",
|
||
default="⏰ Введите количество дней действия промокода (1-365):"
|
||
)
|
||
|
||
try:
|
||
await callback.message.edit_text(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
except Exception as e:
|
||
await callback.message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
await callback.answer()
|
||
|
||
# Step 4: Process edited validity and finalize
|
||
@router.message(AdminStates.waiting_for_promo_edit_validity_days, F.text)
|
||
async def process_promo_edit_validity_handler(message: types.Message, state: FSMContext,
|
||
i18n_data: dict, settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
promo_id = data.get("edit_promo_id")
|
||
|
||
try:
|
||
validity_days = int(message.text.strip())
|
||
if not (1 <= validity_days <= 365):
|
||
await message.answer(_(
|
||
"admin_promo_invalid_validity_days",
|
||
default="❌ Дни действия должны быть от 1 до 365"
|
||
))
|
||
return
|
||
|
||
# Calculate validity date
|
||
valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
|
||
valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
|
||
|
||
# Update promo code
|
||
update_data = {
|
||
"code": data.get("new_code"),
|
||
"bonus_days": data.get("new_bonus_days"),
|
||
"max_activations": data.get("new_max_activations"),
|
||
"valid_until": valid_until_date,
|
||
}
|
||
|
||
updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
|
||
if updated:
|
||
await session.commit()
|
||
|
||
success_text = _(
|
||
"admin_promo_updated_success",
|
||
default="✅ Промокод обновлен!\n\n🎟 Код: <b>{code}</b>\n🎁 Бонусные дни: <b>{days}</b>\n🔢 Максимальные активации: <b>{max_act}</b>\n⏰ Действует до: <b>{validity}</b>",
|
||
code=data.get("new_code"),
|
||
days=data.get("new_bonus_days"),
|
||
max_act=data.get("new_max_activations"),
|
||
validity=valid_until_str_display
|
||
)
|
||
|
||
await message.answer(
|
||
success_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
else:
|
||
await session.rollback()
|
||
await message.answer(_(
|
||
"admin_promo_update_failed",
|
||
default="❌ Ошибка обновления промокода"
|
||
))
|
||
|
||
await state.clear()
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_promo_invalid_validity_days",
|
||
default="❌ Дни действия должны быть от 1 до 365"
|
||
))
|
||
|
||
|
||
@router.callback_query(F.data.startswith("promo_toggle:"))
|
||
async def promo_toggle_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||
settings: Settings, session: AsyncSession):
|
||
"""Toggle promo code active status"""
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
promo_id = int(callback.data.split(":")[1])
|
||
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
|
||
if not promo:
|
||
await callback.answer(_("admin_promo_not_found", default="Промокод не найден"), show_alert=True)
|
||
return
|
||
|
||
# Toggle status
|
||
new_status = not promo.is_active
|
||
update_data = {"is_active": new_status}
|
||
|
||
updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
|
||
if updated:
|
||
await session.commit()
|
||
|
||
status_text = _("admin_promo_status_activated", default="активирован") if new_status else _("admin_promo_status_deactivated", default="деактивирован")
|
||
success_text = _(
|
||
"admin_promo_toggle_success",
|
||
default="✅ Промокод <b>{code}</b> {status}",
|
||
code=promo.code,
|
||
status=status_text
|
||
)
|
||
|
||
await callback.answer(success_text, show_alert=True)
|
||
|
||
# Refresh the detail view
|
||
await promo_detail_handler(callback, i18n_data, settings, session)
|
||
else:
|
||
await session.rollback()
|
||
await callback.answer(_(
|
||
"admin_promo_toggle_failed",
|
||
default="❌ Ошибка изменения статуса промокода"
|
||
), show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("promo_activations:"))
|
||
async def promo_activations_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||
settings: Settings, session: AsyncSession):
|
||
"""Show promo code activations"""
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
promo_id = int(callback.data.split(":")[1])
|
||
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
|
||
if not promo:
|
||
await callback.answer(_("admin_promo_not_found", default="Промокод не найден"), show_alert=True)
|
||
return
|
||
|
||
# Get activations for this promo code
|
||
activations = await promo_code_dal.get_promo_activations_by_code_id(session, promo_id)
|
||
|
||
if not activations:
|
||
text = _(
|
||
"admin_promo_no_activations",
|
||
default="📋 <b>Активации промокода: {code}</b>\n\n❌ Активаций не найдено",
|
||
code=promo.code
|
||
)
|
||
else:
|
||
text_parts = [_(
|
||
"admin_promo_activations_header",
|
||
default="📋 <b>Активации промокода: {code}</b>\n\n",
|
||
code=promo.code
|
||
)]
|
||
|
||
for activation in activations:
|
||
activation_date = activation.activated_at.strftime('%Y-%m-%d %H:%M') if activation.activated_at else "N/A"
|
||
text_parts.append(_(
|
||
"admin_promo_activation_item",
|
||
default="👤 User ID: <b>{user_id}</b>\n📅 Дата: <b>{date}</b>\n",
|
||
user_id=activation.user_id,
|
||
date=activation_date
|
||
))
|
||
|
||
text = "".join(text_parts)
|
||
|
||
# Build keyboard with back button
|
||
kb = InlineKeyboardBuilder()
|
||
kb.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_promo_back_to_detail_button", default="⬅️ К промокоду"),
|
||
callback_data=f"promo_detail:{promo_id}")
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=kb.as_markup(),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
|
||
|
||
@router.callback_query(F.data.startswith("promo_delete:"))
|
||
async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||
settings: Settings, session: AsyncSession):
|
||
promo_id = int(callback.data.split(":")[1])
|
||
promo = await promo_code_dal.delete_promo_code(session, promo_id)
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
if promo:
|
||
await session.commit()
|
||
await callback.message.edit_text(
|
||
_("admin_promo_deleted_success", code=promo.code),
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
else:
|
||
await session.rollback()
|
||
await callback.message.edit_text(
|
||
_("admin_promo_not_found"),
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||
await callback.answer()
|
||
|
||
|
||
@router.callback_query(
|
||
F.data == "admin_action:main",
|
||
StateFilter(
|
||
AdminStates.waiting_for_promo_details,
|
||
AdminStates.waiting_for_promo_code,
|
||
AdminStates.waiting_for_promo_bonus_days,
|
||
AdminStates.waiting_for_promo_max_activations,
|
||
AdminStates.waiting_for_promo_validity_days,
|
||
AdminStates.waiting_for_promo_edit_details,
|
||
AdminStates.waiting_for_bulk_promo_quantity,
|
||
AdminStates.waiting_for_bulk_promo_bonus_days,
|
||
AdminStates.waiting_for_bulk_promo_max_activations,
|
||
AdminStates.waiting_for_bulk_promo_validity_days,
|
||
),
|
||
)
|
||
async def cancel_promo_creation_state_to_menu(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
settings: Settings,
|
||
i18n_data: dict,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error cancelling.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
await callback.message.edit_text(_("admin_action_cancelled_default"),
|
||
reply_markup=get_admin_panel_keyboard(
|
||
i18n, current_lang, settings))
|
||
except Exception:
|
||
await callback.message.answer(_("admin_action_cancelled_default"),
|
||
reply_markup=get_admin_panel_keyboard(
|
||
i18n, current_lang, settings))
|
||
|
||
await callback.answer(_("admin_action_cancelled_default_alert"))
|
||
await state.clear()
|
||
|
||
|
||
async def create_bulk_promo_prompt_handler(callback: types.CallbackQuery,
|
||
state: FSMContext, i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n or not callback.message:
|
||
await callback.answer("Error preparing bulk promo creation.",
|
||
show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
# Step 1: Ask for quantity
|
||
prompt_text = _(
|
||
"admin_bulk_promo_step1_quantity",
|
||
default="📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 1 из 4:</b> Количество промокодов\n\nВведите количество промокодов для создания (1-1000):"
|
||
)
|
||
|
||
try:
|
||
await callback.message.edit_text(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
except Exception as e:
|
||
logging.warning(
|
||
f"Could not edit message for bulk promo prompt: {e}. Sending new.")
|
||
await callback.message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML")
|
||
await callback.answer()
|
||
await state.set_state(AdminStates.waiting_for_bulk_promo_quantity)
|
||
|
||
|
||
def generate_unique_promo_code(length: int = 8) -> str:
|
||
"""Generate a unique promotional code"""
|
||
characters = string.ascii_uppercase + string.digits
|
||
# Exclude confusing characters
|
||
characters = characters.replace('0', '').replace('O', '').replace('1', '').replace('I', '').replace('L', '')
|
||
return ''.join(random.choice(characters) for _ in range(length))
|
||
|
||
|
||
# Step 1: Process quantity
|
||
@router.message(AdminStates.waiting_for_bulk_promo_quantity, F.text)
|
||
async def process_bulk_promo_quantity_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
quantity = int(message.text.strip())
|
||
if quantity <= 0 or quantity > 1000:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_quantity",
|
||
default="❌ Количество должно быть от 1 до 1000"
|
||
))
|
||
return
|
||
|
||
await state.update_data(quantity=quantity)
|
||
|
||
# Step 2: Ask for bonus days
|
||
prompt_text = _(
|
||
"admin_bulk_promo_step2_bonus_days",
|
||
default="📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКоличество: <b>{quantity}</b>\n\nВведите количество бонусных дней (1-365):",
|
||
quantity=quantity
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_bulk_promo_bonus_days)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_quantity",
|
||
default="❌ Количество должно быть от 1 до 1000"
|
||
))
|
||
|
||
# Step 2: Process bonus days
|
||
@router.message(AdminStates.waiting_for_bulk_promo_bonus_days, F.text)
|
||
async def process_bulk_promo_bonus_days_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
bonus_days = int(message.text.strip())
|
||
if bonus_days <= 0 or bonus_days > 365:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_bonus_days",
|
||
default="❌ Бонусные дни должны быть от 1 до 365"
|
||
))
|
||
return
|
||
|
||
data = await state.get_data()
|
||
await state.update_data(bonus_days=bonus_days)
|
||
|
||
# Step 3: Ask for max activations
|
||
prompt_text = _(
|
||
"admin_bulk_promo_step3_max_activations",
|
||
default="📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКоличество: <b>{quantity}</b>\nБонусные дни: <b>{bonus_days}</b>\n\nВведите максимальное количество активаций для каждого промокода (1-10000):",
|
||
quantity=data['quantity'],
|
||
bonus_days=bonus_days
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_bulk_promo_max_activations)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_bonus_days",
|
||
default="❌ Бонусные дни должны быть от 1 до 365"
|
||
))
|
||
|
||
# Step 3: Process max activations
|
||
@router.message(AdminStates.waiting_for_bulk_promo_max_activations, F.text)
|
||
async def process_bulk_promo_max_activations_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
max_activations = int(message.text.strip())
|
||
if max_activations <= 0 or max_activations > 10000:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_max_activations",
|
||
default="❌ Максимальные активации должны быть от 1 до 10000"
|
||
))
|
||
return
|
||
|
||
data = await state.get_data()
|
||
await state.update_data(max_activations=max_activations)
|
||
|
||
# Step 4: Ask for validity days (optional)
|
||
keyboard = InlineKeyboardBuilder()
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_bulk_promo_unlimited_validity", default="♾️ Неограниченно"),
|
||
callback_data="bulk_promo_unlimited_validity"
|
||
)
|
||
)
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_bulk_promo_set_validity", default="⏰ Установить срок"),
|
||
callback_data="bulk_promo_set_validity"
|
||
)
|
||
)
|
||
keyboard.row(
|
||
InlineKeyboardButton(
|
||
text=_("admin_panel_back_button", default="⬅️ Назад"),
|
||
callback_data="admin_panel_back"
|
||
)
|
||
)
|
||
|
||
prompt_text = _(
|
||
"admin_bulk_promo_step4_validity",
|
||
default="📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКоличество: <b>{quantity}</b>\nБонусные дни: <b>{bonus_days}</b>\nМакс. активации: <b>{max_activations}</b>\n\nВыберите срок действия промокодов:",
|
||
quantity=data['quantity'],
|
||
bonus_days=data['bonus_days'],
|
||
max_activations=max_activations
|
||
)
|
||
|
||
await message.answer(
|
||
prompt_text,
|
||
reply_markup=keyboard.as_markup(),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(AdminStates.waiting_for_bulk_promo_validity_days)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_max_activations",
|
||
default="❌ Максимальные активации должны быть от 1 до 10000"
|
||
))
|
||
|
||
# Handle validity choice buttons
|
||
@router.callback_query(F.data == "bulk_promo_unlimited_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
|
||
async def process_bulk_promo_unlimited_validity(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
await state.update_data(validity_days=None)
|
||
await create_bulk_promo_codes_final(callback, state, i18n_data, settings, session)
|
||
|
||
@router.callback_query(F.data == "bulk_promo_set_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
|
||
async def process_bulk_promo_set_validity(callback: types.CallbackQuery,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await callback.answer("Language service error.", show_alert=True)
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
prompt_text = _(
|
||
"admin_bulk_promo_enter_validity_days",
|
||
default="⏰ Введите количество дней действия промокодов (1-365):"
|
||
)
|
||
|
||
try:
|
||
await callback.message.edit_text(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
except Exception as e:
|
||
await callback.message.answer(
|
||
prompt_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
await callback.answer()
|
||
|
||
# Step 4: Process validity days (when user chooses to set)
|
||
@router.message(AdminStates.waiting_for_bulk_promo_validity_days, F.text)
|
||
async def process_bulk_promo_validity_days_handler(message: types.Message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
await message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
try:
|
||
validity_days = int(message.text.strip())
|
||
if validity_days <= 0 or validity_days > 365:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_validity_days",
|
||
default="❌ Дни действия должны быть от 1 до 365"
|
||
))
|
||
return
|
||
|
||
await state.update_data(validity_days=validity_days)
|
||
|
||
# Create a fake callback for consistency
|
||
fake_callback = types.CallbackQuery(
|
||
id="fake",
|
||
from_user=message.from_user,
|
||
chat_instance="fake",
|
||
message=message,
|
||
data="fake"
|
||
)
|
||
|
||
await create_bulk_promo_codes_final(fake_callback, state, i18n_data, settings, session)
|
||
|
||
except ValueError:
|
||
await message.answer(_(
|
||
"admin_bulk_promo_invalid_validity_days",
|
||
default="❌ Дни действия должны быть от 1 до 365"
|
||
))
|
||
|
||
async def create_bulk_promo_codes_final(callback_or_message,
|
||
state: FSMContext,
|
||
i18n_data: dict,
|
||
settings: Settings,
|
||
session: AsyncSession):
|
||
"""Final step to create all promo codes"""
|
||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||
if not i18n:
|
||
if hasattr(callback_or_message, 'answer'):
|
||
await callback_or_message.answer("Language service error.", show_alert=True)
|
||
else:
|
||
await callback_or_message.reply("Language service error.")
|
||
return
|
||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||
|
||
data = await state.get_data()
|
||
quantity = data['quantity']
|
||
bonus_days = data['bonus_days']
|
||
max_activations = data['max_activations']
|
||
validity_days = data.get('validity_days')
|
||
|
||
# Create validity date
|
||
valid_until_date: Optional[datetime] = None
|
||
valid_until_str_display = _("admin_promo_valid_indefinitely", default="Неограниченно")
|
||
|
||
if validity_days:
|
||
valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
|
||
valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
|
||
|
||
# Show creating message
|
||
creating_text = _(
|
||
"admin_bulk_promo_creating",
|
||
default="⏳ Создаю {quantity} промокодов...",
|
||
quantity=quantity
|
||
)
|
||
|
||
if hasattr(callback_or_message, 'message'):
|
||
try:
|
||
await callback_or_message.message.edit_text(creating_text)
|
||
except:
|
||
await callback_or_message.message.answer(creating_text)
|
||
if hasattr(callback_or_message, 'answer'):
|
||
await callback_or_message.answer()
|
||
else:
|
||
await callback_or_message.answer(creating_text)
|
||
|
||
# Generate and create promo codes
|
||
created_codes = []
|
||
try:
|
||
for i in range(quantity):
|
||
# Generate unique code
|
||
promo_code = generate_unique_promo_code()
|
||
|
||
# Ensure uniqueness
|
||
existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
|
||
retries = 0
|
||
while existing_promo and retries < 10:
|
||
promo_code = generate_unique_promo_code()
|
||
existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
|
||
retries += 1
|
||
|
||
if retries >= 10:
|
||
raise Exception("Не удалось создать уникальный промокод")
|
||
|
||
# Create promo code
|
||
promo_data = {
|
||
"code": promo_code,
|
||
"bonus_days": bonus_days,
|
||
"max_activations": max_activations,
|
||
"current_activations": 0,
|
||
"is_active": True,
|
||
"valid_until": valid_until_date,
|
||
"created_by_admin_id": callback_or_message.from_user.id if hasattr(callback_or_message, 'from_user') and callback_or_message.from_user else 0
|
||
}
|
||
|
||
await promo_code_dal.create_promo_code(session, promo_data)
|
||
created_codes.append(promo_code)
|
||
|
||
await session.commit()
|
||
|
||
# Send success message
|
||
success_text = _(
|
||
"admin_bulk_promo_created_success",
|
||
default="✅ Создано {count} промокодов на {days} дней!\n\nДействуют до: {validity}\nМакс. активации: {max_act}",
|
||
count=quantity,
|
||
days=bonus_days,
|
||
validity=valid_until_str_display,
|
||
max_act=max_activations
|
||
)
|
||
|
||
if hasattr(callback_or_message, 'message'):
|
||
target_message = callback_or_message.message
|
||
else:
|
||
target_message = callback_or_message
|
||
|
||
await target_message.answer(
|
||
success_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
# Always send codes as file
|
||
codes_file_content = "\n".join(created_codes)
|
||
codes_file = types.BufferedInputFile(
|
||
codes_file_content.encode('utf-8'),
|
||
filename=f"bulk_promo_codes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
||
)
|
||
|
||
await target_message.answer_document(
|
||
codes_file,
|
||
caption=_(
|
||
"admin_bulk_promo_codes_file",
|
||
default="📄 Все созданные промокоды в файле ({count} шт.)",
|
||
count=quantity
|
||
)
|
||
)
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error creating bulk promo codes: {e}")
|
||
await session.rollback()
|
||
error_text = _(
|
||
"admin_bulk_promo_creation_failed",
|
||
default="❌ Ошибка создания промокодов: {error}",
|
||
error=str(e)
|
||
)
|
||
|
||
if hasattr(callback_or_message, 'message'):
|
||
target_message = callback_or_message.message
|
||
else:
|
||
target_message = callback_or_message
|
||
|
||
await target_message.answer(
|
||
error_text,
|
||
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
# Old bulk promo handler removed - now using step-by-step process
|
||
|
||
|
||
|