Merge pull request #63 from machka-pasla/dev

Dev
This commit is contained in:
Machka Pasla
2025-08-06 01:24:06 +03:00
committed by GitHub
4 changed files with 293 additions and 356 deletions
+230 -317
View File
@@ -1,4 +1,6 @@
import logging import logging
import csv
import io
from aiogram import Router, F, types from aiogram import Router, F, types
from aiogram.filters import StateFilter from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -17,10 +19,45 @@ from bot.middlewares.i18n import JsonI18n
router = Router(name="promo_manage_router") router = Router(name="promo_manage_router")
async def view_promo_codes_handler(callback: types.CallbackQuery, async def get_promo_detail_text_and_keyboard(promo_id: int, session: AsyncSession, i18n: JsonI18n, current_lang: str):
i18n_data: dict, settings: Settings, _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
session: AsyncSession): promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
"""View all active promo codes""" if not promo:
return None, None
status = _("admin_promo_status_active") if promo.is_active else _("admin_promo_status_inactive")
if promo.valid_until and promo.valid_until < datetime.now(timezone.utc):
status = _("admin_promo_status_expired")
elif promo.current_activations >= promo.max_activations:
status = _("admin_promo_status_used_up")
validity = _("admin_promo_valid_indefinitely")
if promo.valid_until:
validity = promo.valid_until.strftime("%d.%m.%Y %H:%M")
created = promo.created_at.strftime("%d.%m.%Y %H:%M") if promo.created_at else "N/A"
text = "\n".join([
_("admin_promo_card_title", code=promo.code),
_("admin_promo_card_bonus_days", days=promo.bonus_days),
_("admin_promo_card_activations", current=promo.current_activations, max=promo.max_activations),
_("admin_promo_card_validity", validity=validity),
_("admin_promo_card_status", status=status),
_("admin_promo_card_created", created=created),
_("admin_promo_card_created_by", creator=promo.created_by_admin_id)
])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=_("admin_promo_edit_button"), callback_data=f"promo_edit_select:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_toggle_status_button"), callback_data=f"promo_toggle:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_view_activations_button"), callback_data=f"promo_activations:{promo_id}:0"))
builder.row(InlineKeyboardButton(text=_("admin_promo_delete_button"), callback_data=f"promo_delete:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_back_to_list_button"), callback_data="admin_action:promo_management"))
return text, builder.as_markup()
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) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message: if not i18n or not callback.message:
@@ -28,57 +65,19 @@ async def view_promo_codes_handler(callback: types.CallbackQuery,
return return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try: promo_models = await promo_code_dal.get_all_active_promo_codes(session, limit=20, offset=0)
promo_models = await promo_code_dal.get_all_active_promo_codes(session, text = f"{_('admin_active_promos_list_header')}\n\n{_('admin_no_active_promos')}" if not promo_models else "\n".join(
limit=20, [_("admin_active_promos_list_header"), ""] + [
offset=0) f"🎟 <code>{p.code}</code> | 🎁 {p.bonus_days}д | 📊 {p.current_activations}/{p.max_activations} | ⏰ {p.valid_until.strftime('%d.%m.%Y') if p.valid_until else _('admin_promo_valid_indefinitely')}"
for p in promo_models
if not promo_models: ]
text = _(
"admin_no_promo_codes",
default="📋 <b>Активные промокоды</b>\n\n❌ Нет активных промокодов"
)
else:
lines = [_("admin_active_promo_codes_title",
default="📋 <b>Активные промокоды</b>\n")]
for promo in promo_models:
validity_str = _("admin_promo_unlimited", default="Без ограничений")
if promo.valid_until:
validity_str = promo.valid_until.strftime("%d.%m.%Y")
lines.append(
f"🎟 <code>{promo.code}</code> | "
f"🎁 {promo.bonus_days}д | "
f"📊 {promo.current_activations}/{promo.max_activations} | "
f"{validity_str}"
) )
text = "\n".join(lines) await callback.message.edit_text(text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), parse_mode="HTML")
try:
await callback.message.edit_text(
text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
parse_mode="HTML"
)
except Exception:
await callback.message.answer(
text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
parse_mode="HTML"
)
await callback.answer() await callback.answer()
except Exception as e:
logging.error(f"Error viewing promo codes: {e}")
await callback.answer(_("error_occurred_try_again"), show_alert=True)
async def promo_management_handler(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
async def promo_management_handler(callback: types.CallbackQuery,
i18n_data: dict, settings: Settings,
session: AsyncSession):
"""Main promo management interface"""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message: if not i18n or not callback.message:
@@ -86,337 +85,251 @@ async def promo_management_handler(callback: types.CallbackQuery,
return return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try:
promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0) promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0)
if not promo_models: if not promo_models:
text = _( await callback.message.edit_text(_("admin_promo_management_empty"), reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), parse_mode="HTML")
"admin_no_promo_codes_to_manage",
default="🛠 <b>Управление промокодами</b>\n\n❌ Нет промокодов для управления"
)
await callback.message.edit_text(
text,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
parse_mode="HTML"
)
await callback.answer() await callback.answer()
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
text_lines = [_("admin_promo_management_title", default="🛠 <b>Управление промокодами</b>\n")]
for promo in promo_models: for promo in promo_models:
status_emoji = "" if promo.is_active else "" builder.row(InlineKeyboardButton(text=f"📝 {promo.code}", callback_data=f"promo_detail:{promo.promo_code_id}"))
validity_str = _("admin_promo_unlimited", default="") builder.row(InlineKeyboardButton(text=_("back_to_admin_panel_button"), callback_data="admin_action:main"))
if promo.valid_until:
validity_str = promo.valid_until.strftime("%d.%m")
text_lines.append( await callback.message.edit_text(_("admin_promo_management_title"), reply_markup=builder.as_markup(), parse_mode="HTML")
f"{status_emoji} <code>{promo.code}</code> | "
f"🎁{promo.bonus_days}д | "
f"📊{promo.current_activations}/{promo.max_activations} | "
f"{validity_str}"
)
builder.row(
InlineKeyboardButton(
text=f"📝 {promo.code}",
callback_data=f"promo_detail:{promo.promo_code_id}"
)
)
builder.row(
InlineKeyboardButton(
text=_("admin_back_to_panel", default="🔙 В админ панель"),
callback_data="admin_action:main"
)
)
text = "\n".join(text_lines)
try:
await callback.message.edit_text(
text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
except Exception:
await callback.message.answer(
text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
await callback.answer() await callback.answer()
except Exception as e:
logging.error(f"Error in promo management: {e}")
await callback.answer(_("error_occurred_try_again"), show_alert=True)
@router.callback_query(F.data.startswith("promo_detail:")) @router.callback_query(F.data.startswith("promo_detail:"))
async def promo_detail_handler(callback: types.CallbackQuery, async def promo_detail_handler(callback: types.CallbackQuery, i18n_data: dict, session: AsyncSession):
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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message: current_lang = i18n_data.get("current_language")
if not i18n or not callback.message or not current_lang:
await callback.answer("Error processing request.", show_alert=True) await callback.answer("Error processing request.", show_alert=True)
return return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try: try:
promo_id = int(callback.data.split(":")[1]) promo_id = int(callback.data.split(":")[1])
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id) text, keyboard = await get_promo_detail_text_and_keyboard(promo_id, session, i18n, current_lang)
if text:
if not promo: await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
await callback.answer(_("admin_promo_not_found", default="❌ Промокод не найден"), show_alert=True)
return
# Format promo details
status_text = _("admin_promo_active", default="✅ Активен") if promo.is_active else _("admin_promo_inactive", default="❌ Неактивен")
validity_text = _("admin_promo_unlimited", default="Без ограничений")
if promo.valid_until:
if promo.valid_until > datetime.now(timezone.utc):
validity_text = promo.valid_until.strftime("%d.%m.%Y %H:%M")
else: else:
validity_text = f"{promo.valid_until.strftime('%d.%m.%Y %H:%M')} (истек)" await callback.answer(i18n.gettext(current_lang, "admin_promo_not_found"), show_alert=True)
except (ValueError, IndexError):
created_at = promo.created_at.strftime("%d.%m.%Y %H:%M") if promo.created_at else "N/A" await callback.answer(i18n.gettext(current_lang, "admin_promo_not_found"), show_alert=True)
text = _(
"admin_promo_detail_info",
default="🎟 <b>Детали промокода</b>\n\n"
"📝 Код: <code>{code}</code>\n"
"🎁 Бонусные дни: <b>{bonus_days}</b>\n"
"📊 Использований: <b>{current}/{max}</b>\n"
"📋 Статус: {status}\n"
"⏰ Действует до: <b>{validity}</b>\n"
"👤 Создал: <code>{admin_id}</code>\n"
"🕐 Создан: <b>{created}</b>",
code=promo.code,
bonus_days=promo.bonus_days,
current=promo.current_activations,
max=promo.max_activations,
status=status_text,
validity=validity_text,
admin_id=promo.created_by_admin_id,
created=created_at
)
# Create management buttons
builder = InlineKeyboardBuilder()
# Edit button
builder.row(
InlineKeyboardButton(
text=_("admin_promo_edit", default="✏️ Редактировать"),
callback_data=f"promo_edit:{promo_id}"
)
)
# Toggle active/inactive
toggle_text = _("admin_promo_deactivate", default="❌ Деактивировать") if promo.is_active else _("admin_promo_activate", default="✅ Активировать")
builder.row(
InlineKeyboardButton(
text=toggle_text,
callback_data=f"promo_toggle:{promo_id}"
)
)
# View activations
builder.row(
InlineKeyboardButton(
text=_("admin_promo_view_activations", default="📋 Активации"),
callback_data=f"promo_activations:{promo_id}"
)
)
# Delete button
builder.row(
InlineKeyboardButton(
text=_("admin_promo_delete", default="🗑 Удалить"),
callback_data=f"promo_delete:{promo_id}"
)
)
# Back button
builder.row(
InlineKeyboardButton(
text=_("admin_back_to_management", default="🔙 К управлению"),
callback_data="admin_action:promo_management"
)
)
try:
await callback.message.edit_text(
text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
except Exception:
await callback.message.answer(
text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
await callback.answer() await callback.answer()
except ValueError:
await callback.answer(_("admin_invalid_promo_id", default="❌ Неверный ID промокода"), show_alert=True)
except Exception as e:
logging.error(f"Error in promo detail: {e}")
await callback.answer(_("error_occurred_try_again"), show_alert=True)
@router.callback_query(F.data.startswith("promo_toggle:")) @router.callback_query(F.data.startswith("promo_toggle:"))
async def promo_toggle_handler(callback: types.CallbackQuery, i18n_data: dict, async def promo_toggle_handler(callback: types.CallbackQuery, i18n_data: dict, session: AsyncSession):
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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: current_lang = i18n_data.get("current_language")
await callback.answer("Language service error.", show_alert=True) if not i18n or not callback.message or not current_lang:
return return await callback.answer("Language service error.", show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try: try:
promo_id = int(callback.data.split(":")[1]) promo_id = int(callback.data.split(":")[1])
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id) promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
if not promo: if not promo:
await callback.answer(_("admin_promo_not_found", default="❌ Промокод не найден"), show_alert=True) return await callback.answer(_("admin_promo_not_found"), show_alert=True)
return
new_status = not promo.is_active new_status = not promo.is_active
update_data = {"is_active": new_status} if await promo_code_dal.update_promo_code(session, promo_id, {"is_active": new_status}):
updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
if updated:
await session.commit() await session.commit()
status_text = _("admin_promo_activated", default="активирован") if new_status else _("admin_promo_deactivated", default="деактивирован") status_text = _("admin_promo_status_activated") if new_status else _("admin_promo_status_deactivated")
await callback.answer( await callback.answer(_("admin_promo_toggle_success", code=promo.code, status=status_text))
_("admin_promo_status_changed", default="✅ Промокод {status}", status=status_text)
) text, keyboard = await get_promo_detail_text_and_keyboard(promo_id, session, i18n, current_lang)
# Refresh the detail view if text:
callback.data = f"promo_detail:{promo_id}" await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
await promo_detail_handler(callback, i18n_data, settings, session)
else: else:
await callback.answer(_("error_occurred_try_again"), show_alert=True) await callback.answer(_("error_occurred_try_again"), show_alert=True)
except (ValueError, IndexError):
except ValueError: await callback.answer(_("admin_promo_not_found"), show_alert=True)
await callback.answer(_("admin_invalid_promo_id", default="❌ Неверный ID промокода"), show_alert=True)
except Exception as e:
logging.error(f"Error toggling promo: {e}")
await callback.answer(_("error_occurred_try_again"), show_alert=True)
@router.callback_query(F.data.startswith("promo_activations:")) @router.callback_query(F.data.startswith("promo_activations:"))
async def promo_activations_handler(callback: types.CallbackQuery, i18n_data: dict, async def promo_activations_handler(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
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") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message: current_lang = i18n_data.get("current_language")
await callback.answer("Error processing request.", show_alert=True) if not i18n or not callback.message or not current_lang:
return return await callback.answer("Error processing request.", show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try:
parts = callback.data.split(":")
promo_id = int(parts[1])
page = int(parts[2])
page_size = settings.LOGS_PAGE_SIZE
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
if not promo:
return await callback.answer(_("admin_promo_not_found"), show_alert=True)
total_activations = await promo_code_dal.count_promo_activations_by_code_id(session, promo_id)
activations = await promo_code_dal.get_promo_activations_by_code_id(session, promo_id, limit=page_size, offset=page * page_size)
builder = InlineKeyboardBuilder()
if not activations:
text = _("admin_promo_no_activations", code=promo.code)
else:
text = _("admin_promo_activations_header", code=promo.code) + "\n\n"
text += "\n".join([_("admin_promo_activation_item", user_id=a.user_id, date=a.activated_at.strftime("%d.%m.%Y %H:%M")) for a in activations])
nav_buttons = []
if page > 0:
nav_buttons.append(InlineKeyboardButton(text="⬅️", callback_data=f"promo_activations:{promo_id}:{page-1}"))
if (page + 1) * page_size < total_activations:
nav_buttons.append(InlineKeyboardButton(text="➡️", callback_data=f"promo_activations:{promo_id}:{page+1}"))
if nav_buttons:
builder.row(*nav_buttons)
builder.row(InlineKeyboardButton(text=_("admin_promo_export_csv_button"), callback_data=f"promo_export:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_back_to_detail_button"), callback_data=f"promo_detail:{promo_id}"))
await callback.message.edit_text(text, reply_markup=builder.as_markup(), parse_mode="HTML")
except (ValueError, IndexError):
await callback.answer(_("admin_promo_not_found"), show_alert=True)
await callback.answer()
@router.callback_query(F.data.startswith("promo_export:"))
async def promo_export_activations_handler(callback: types.CallbackQuery, i18n_data: dict, session: AsyncSession):
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang = i18n_data.get("current_language")
if not i18n or not callback.message or not current_lang:
return await callback.answer("Error processing request.", show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try: try:
promo_id = int(callback.data.split(":")[1]) promo_id = int(callback.data.split(":")[1])
promo = await promo_code_dal.get_promo_code_by_id(session, promo_id) promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
if not promo: if not promo:
await callback.answer(_("admin_promo_not_found", default="❌ Промокод не найден"), show_alert=True) return await callback.answer(_("admin_promo_not_found"), show_alert=True)
return
activations = await promo_code_dal.get_promo_activations_by_code_id(session, promo_id) activations = await promo_code_dal.get_promo_activations_by_code_id(session, promo_id)
text_lines = [
_("admin_promo_activations_title",
default="📋 <b>Активации промокода {code}</b>\n",
code=promo.code)
]
if not activations: if not activations:
text_lines.append(_("admin_no_activations", default="❌ Нет активаций")) return await callback.answer(_("admin_promo_no_activations", code=promo.code), show_alert=True)
else:
for activation in activations[:10]: # Show last 10
activated_at = activation.activated_at.strftime("%d.%m.%Y %H:%M") if activation.activated_at else "N/A"
text_lines.append(
f"👤 <code>{activation.user_id}</code> | 🕐 {activated_at}"
)
if len(activations) > 10: output = io.StringIO()
text_lines.append(f"\n... и еще {len(activations) - 10}") writer = csv.writer(output)
writer.writerow(["User ID", "Activation Date"])
for act in activations:
writer.writerow([act.user_id, act.activated_at.strftime("%Y-%m-%d %H:%M:%S")])
text = "\n".join(text_lines) output.seek(0)
file = types.BufferedInputFile(output.getvalue().encode('utf-8'), filename=f"promo_{promo.code}_activations.csv")
await callback.message.answer_document(file, caption=_("admin_promo_export_caption", code=promo.code))
builder = InlineKeyboardBuilder() except (ValueError, IndexError):
builder.row( await callback.answer(_("admin_promo_not_found"), show_alert=True)
InlineKeyboardButton(
text=_("admin_back_to_promo", default="🔙 К промокоду"),
callback_data=f"promo_detail:{promo_id}"
)
)
try:
await callback.message.edit_text(
text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
except Exception:
await callback.message.answer(
text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
await callback.answer() await callback.answer()
except ValueError:
await callback.answer(_("admin_invalid_promo_id", default="❌ Неверный ID промокода"), show_alert=True)
except Exception as e:
logging.error(f"Error viewing activations: {e}")
await callback.answer(_("error_occurred_try_again"), show_alert=True)
@router.callback_query(F.data.startswith("promo_delete:")) @router.callback_query(F.data.startswith("promo_delete:"))
async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict, async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict, session: AsyncSession):
settings: Settings, session: AsyncSession):
"""Delete promo code"""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n: current_lang = i18n_data.get("current_language")
await callback.answer("Language service error.", show_alert=True) if not i18n or not callback.message or not current_lang:
return return await callback.answer("Language service error.", show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try: try:
promo_id = int(callback.data.split(":")[1]) promo_id = int(callback.data.split(":")[1])
promo = await promo_code_dal.delete_promo_code(session, promo_id) promo = await promo_code_dal.delete_promo_code(session, promo_id)
if promo: if promo:
await session.commit() await session.commit()
await callback.answer( await callback.answer(_("admin_promo_deleted_success", code=promo.code), show_alert=True)
_("admin_promo_deleted", default="✅ Промокод удален", code=promo.code) await promo_management_handler(callback, i18n_data, {}, session) # Settings not needed here
)
# Go back to management
callback.data = "admin_action:promo_management"
await promo_management_handler(callback, i18n_data, settings, session)
else: else:
await callback.answer(_("admin_promo_not_found", default="❌ Промокод не найден"), show_alert=True) await callback.answer(_("admin_promo_not_found"), show_alert=True)
except (ValueError, IndexError):
except ValueError: await callback.answer(_("admin_promo_not_found"), show_alert=True)
await callback.answer(_("admin_invalid_promo_id", default="❌ Неверный ID промокода"), show_alert=True)
except Exception as e:
logging.error(f"Error deleting promo: {e}")
await callback.answer(_("error_occurred_try_again"), show_alert=True)
# Legacy handlers that redirect to new system # --- Promo Edit Handlers ---
async def manage_promo_codes_handler(callback: types.CallbackQuery, @router.callback_query(F.data.startswith("promo_edit_select:"))
i18n_data: dict, settings: Settings, async def promo_edit_select_handler(callback: types.CallbackQuery, i18n_data: dict):
session: AsyncSession): i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
"""Redirect to new unified handler""" current_lang = i18n_data.get("current_language")
if not i18n or not callback.message or not current_lang:
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
promo_id = int(callback.data.split(":")[1])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=_("admin_promo_edit_bonus_days"), callback_data=f"promo_edit_field:bonus_days:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_edit_max_activations"), callback_data=f"promo_edit_field:max_activations:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_edit_validity"), callback_data=f"promo_edit_field:valid_until:{promo_id}"))
builder.row(InlineKeyboardButton(text=_("admin_promo_back_to_detail_button"), callback_data=f"promo_detail:{promo_id}"))
await callback.message.edit_text(_("admin_promo_edit_select_field"), reply_markup=builder.as_markup())
await callback.answer()
@router.callback_query(F.data.startswith("promo_edit_field:"))
async def promo_edit_field_handler(callback: types.CallbackQuery, state: FSMContext, i18n_data: dict):
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang = i18n_data.get("current_language")
if not i18n or not callback.message or not current_lang: return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
_, field, promo_id_str = callback.data.split(":")
await state.update_data(promo_id=int(promo_id_str), field_to_edit=field)
prompts = {
"bonus_days": "admin_promo_prompt_bonus_days",
"max_activations": "admin_promo_prompt_max_activations",
"valid_until": "admin_promo_prompt_validity_days"
}
await state.set_state(AdminStates.waiting_for_promo_edit_details)
await callback.message.edit_text(_(prompts.get(field, "error_occurred_try_again")))
await callback.answer()
@router.message(StateFilter(AdminStates.waiting_for_promo_edit_details))
async def process_promo_edit_details(message: types.Message, state: FSMContext, session: AsyncSession, i18n_data: dict):
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
current_lang = i18n_data.get("current_language")
if not i18n or not message or not current_lang: return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
data = await state.get_data()
promo_id = data.get("promo_id")
field = data.get("field_to_edit")
try:
value = message.text
update_data = {}
if field == "bonus_days":
update_data["bonus_days"] = int(value)
elif field == "max_activations":
update_data["max_activations"] = int(value)
elif field == "valid_until":
if value.lower() in ['0', 'вечно', 'бессрочно', 'indefinite']:
update_data["valid_until"] = None
else:
days = int(value)
update_data["valid_until"] = datetime.now(timezone.utc) + timedelta(days=days)
if await promo_code_dal.update_promo_code(session, promo_id, update_data):
await session.commit()
await message.answer(_("admin_promo_edit_success"))
# Reset state and show updated details
await state.clear()
text, keyboard = await get_promo_detail_text_and_keyboard(promo_id, session, i18n, current_lang)
if text:
await message.answer(text, reply_markup=keyboard, parse_mode="HTML")
else:
await message.answer(_("error_occurred_try_again"))
await state.clear()
except (ValueError, TypeError):
await message.answer(_("admin_promo_invalid_input"))
# Don't clear state, let them try again
async def manage_promo_codes_handler(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession):
await promo_management_handler(callback, i18n_data, settings, session) await promo_management_handler(callback, i18n_data, settings, session)
+17 -15
View File
@@ -10,7 +10,6 @@ from db.models import PromoCode, PromoCodeActivation, User, Payment
async def create_promo_code(session: AsyncSession, async def create_promo_code(session: AsyncSession,
promo_data: Dict[str, Any]) -> PromoCode: promo_data: Dict[str, Any]) -> PromoCode:
new_promo = PromoCode(**promo_data) new_promo = PromoCode(**promo_data)
session.add(new_promo) session.add(new_promo)
await session.flush() await session.flush()
@@ -65,24 +64,25 @@ async def get_all_promo_codes_with_details(session: AsyncSession, limit: int = 5
return result.scalars().all() return result.scalars().all()
async def get_promo_code_by_id(session: AsyncSession, promo_id: int) -> Optional[PromoCode]: async def get_promo_activations_by_code_id(session: AsyncSession, promo_code_id: int, limit: Optional[int] = None, offset: int = 0) -> List[PromoCodeActivation]:
"""Get promo code by ID""" """Get activation history for a specific promo code with optional pagination."""
stmt = select(PromoCode).where(PromoCode.promo_code_id == promo_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_promo_activations_by_code_id(session: AsyncSession, promo_code_id: int) -> List:
"""Get activation history for a specific promo code"""
from db.models import PromoCodeActivation
stmt = (select(PromoCodeActivation) stmt = (select(PromoCodeActivation)
.where(PromoCodeActivation.promo_code_id == promo_code_id) .where(PromoCodeActivation.promo_code_id == promo_code_id)
.order_by(PromoCodeActivation.activated_at.desc()) .order_by(PromoCodeActivation.activated_at.desc())
.limit(50)) # Limit to last 50 activations .offset(offset))
if limit is not None:
stmt = stmt.limit(limit)
result = await session.execute(stmt) result = await session.execute(stmt)
return result.scalars().all() return result.scalars().all()
async def count_promo_activations_by_code_id(session: AsyncSession, promo_code_id: int) -> int:
"""Count total activations for a specific promo code."""
stmt = select(func.count()).select_from(PromoCodeActivation).where(PromoCodeActivation.promo_code_id == promo_code_id)
result = await session.execute(stmt)
return result.scalar_one()
async def update_promo_code(session: AsyncSession, promo_id: int, async def update_promo_code(session: AsyncSession, promo_id: int,
update_data: Dict[str, Any]) -> Optional[PromoCode]: update_data: Dict[str, Any]) -> Optional[PromoCode]:
promo = await get_promo_code_by_id(session, promo_id) promo = await get_promo_code_by_id(session, promo_id)
@@ -99,6 +99,11 @@ async def delete_promo_code(session: AsyncSession, promo_id: int) -> Optional[Pr
promo = await get_promo_code_by_id(session, promo_id) promo = await get_promo_code_by_id(session, promo_id)
if not promo: if not promo:
return None return None
# First, delete related activations due to foreign key constraint
activations = await get_promo_activations_by_code_id(session, promo_id)
for activation in activations:
await session.delete(activation)
await session.delete(promo) await session.delete(promo)
await session.flush() await session.flush()
return promo return promo
@@ -124,7 +129,6 @@ async def increment_promo_code_usage(
async def get_user_activation_for_promo( async def get_user_activation_for_promo(
session: AsyncSession, promo_code_id: int, session: AsyncSession, promo_code_id: int,
user_id: int) -> Optional[PromoCodeActivation]: user_id: int) -> Optional[PromoCodeActivation]:
stmt = select(PromoCodeActivation).where( stmt = select(PromoCodeActivation).where(
PromoCodeActivation.promo_code_id == promo_code_id, PromoCodeActivation.promo_code_id == promo_code_id,
PromoCodeActivation.user_id == user_id).limit(1) PromoCodeActivation.user_id == user_id).limit(1)
@@ -137,7 +141,6 @@ async def record_promo_activation(
promo_code_id: int, promo_code_id: int,
user_id: int, user_id: int,
payment_id: Optional[int] = None) -> Optional[PromoCodeActivation]: payment_id: Optional[int] = None) -> Optional[PromoCodeActivation]:
existing_activation = await get_user_activation_for_promo( existing_activation = await get_user_activation_for_promo(
session, promo_code_id, user_id) session, promo_code_id, user_id)
if existing_activation: if existing_activation:
@@ -162,7 +165,6 @@ async def record_promo_activation(
logging.error( logging.error(
f"Cannot record promo activation: Payment {payment_id} not found." f"Cannot record promo activation: Payment {payment_id} not found."
) )
return None return None
activation_data = { activation_data = {
+13 -2
View File
@@ -161,9 +161,20 @@
"admin_promo_edit_button": "✏️ Edit", "admin_promo_edit_button": "✏️ Edit",
"admin_promo_delete_button": "🗑 Delete", "admin_promo_delete_button": "🗑 Delete",
"admin_promo_edit_prompt": "Send new details for <code>{code}</code> in format: CODE BONUS_DAYS MAX_USES [VALIDITY_DAYS]", "admin_promo_edit_prompt": "Send new details for <code>{code}</code> in format: CODE BONUS_DAYS MAX_USES [VALIDITY_DAYS]",
"admin_promo_updated_success": "Promo <code>{code}</code> updated.", "admin_promo_updated_success": "Promo {code} updated.",
"admin_promo_deleted_success": "Promo <code>{code}</code> deleted.", "admin_promo_deleted_success": "Promo {code} deleted.",
"admin_promo_not_found": "Promo not found.", "admin_promo_not_found": "Promo not found.",
"admin_promo_export_csv_button": "📄 Export to CSV",
"admin_promo_export_caption": "📄 Activations for promo code {code}",
"admin_promo_edit_select_field": "Select a field to edit:",
"admin_promo_prompt_bonus_days": "Enter the new number of bonus days:",
"admin_promo_prompt_max_activations": "Enter the new maximum number of activations:",
"admin_promo_prompt_validity_days": "Enter the new validity period in days (0 for indefinite):",
"admin_promo_edit_success": "Promo code updated successfully.",
"admin_promo_invalid_input": "Invalid input, please try again.",
"admin_promo_edit_bonus_days": "🎁 Bonus Days",
"admin_promo_edit_max_activations": "🔢 Max Activations",
"admin_promo_edit_validity": "⏰ Validity",
"admin_ban_user_prompt": "Enter user ID or @username to ban:", "admin_ban_user_prompt": "Enter user ID or @username to ban:",
"admin_user_not_found_in_bot_db": "User <code>{user_id}</code> not found in bot database.", "admin_user_not_found_in_bot_db": "User <code>{user_id}</code> not found in bot database.",
+13 -2
View File
@@ -161,9 +161,20 @@
"admin_promo_edit_button": "✏️ Изменить", "admin_promo_edit_button": "✏️ Изменить",
"admin_promo_delete_button": "🗑 Удалить", "admin_promo_delete_button": "🗑 Удалить",
"admin_promo_edit_prompt": "Отправьте новые данные для <code>{code}</code> в формате: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [СРОК]", "admin_promo_edit_prompt": "Отправьте новые данные для <code>{code}</code> в формате: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [СРОК]",
"admin_promo_updated_success": "Промокод <code>{code}</code> обновлен.", "admin_promo_updated_success": "Промокод {code} обновлен.",
"admin_promo_deleted_success": "Промокод <code>{code}</code> удален.", "admin_promo_deleted_success": "Промокод {code} удален.",
"admin_promo_not_found": "Промокод не найден.", "admin_promo_not_found": "Промокод не найден.",
"admin_promo_export_csv_button": "📄 Экспорт в CSV",
"admin_promo_export_caption": "📄 Активации промокода {code}",
"admin_promo_edit_select_field": "Выберите поле для редактирования:",
"admin_promo_prompt_bonus_days": "Введите новое количество бонусных дней:",
"admin_promo_prompt_max_activations": "Введите новое максимальное количество активаций:",
"admin_promo_prompt_validity_days": "Введите новый срок действия в днях (0 для бессрочного):",
"admin_promo_edit_success": "Промокод успешно обновлен.",
"admin_promo_invalid_input": "Неверный ввод, попробуйте еще раз.",
"admin_promo_edit_bonus_days": "🎁 Бонусные дни",
"admin_promo_edit_max_activations": "🔢 Макс. активации",
"admin_promo_edit_validity": "⏰ Срок действия",
"admin_ban_user_prompt": "Введите ID или @username пользователя для блокировки:", "admin_ban_user_prompt": "Введите ID или @username пользователя для блокировки:",
"admin_user_not_found_in_bot_db": "Пользователь <code>{user_id}</code> не найден в базе данных бота.", "admin_user_not_found_in_bot_db": "Пользователь <code>{user_id}</code> не найден в базе данных бота.",