From e8c1956de50e7c1138c1d9d0e9bdfcc0bc5acc52 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 31 Aug 2025 18:28:06 +0300 Subject: [PATCH] Update promocodes.py --- app/handlers/admin/promocodes.py | 528 +++++++++++++++++++++---------- 1 file changed, 367 insertions(+), 161 deletions(-) diff --git a/app/handlers/admin/promocodes.py b/app/handlers/admin/promocodes.py index 1e86b05d..05668522 100644 --- a/app/handlers/admin/promocodes.py +++ b/app/handlers/admin/promocodes.py @@ -183,6 +183,372 @@ async def show_promocode_management( await callback.answer() + +@admin_required +@error_handler +async def show_promocode_edit_menu( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession +): + """Показать меню редактирования промокода""" + promo_id = int(callback.data.split('_')[-1]) + + promo = await db.get(PromoCode, promo_id) + if not promo: + await callback.answer("❌ Промокод не найден", show_alert=True) + return + + text = f""" +✏️ Редактирование промокода {promo.code} + +Выберите параметр для изменения: +""" + + keyboard = [ + [ + types.InlineKeyboardButton( + text="📅 Дата окончания", + callback_data=f"promo_edit_date_{promo.id}" + ) + ], + [ + types.InlineKeyboardButton( + text="📊 Количество использований", + callback_data=f"promo_edit_uses_{promo.id}" + ) + ] + ] + + if promo.type == PromoCodeType.BALANCE.value: + keyboard.insert(1, [ + types.InlineKeyboardButton( + text="💰 Сумма бонуса", + callback_data=f"promo_edit_amount_{promo.id}" + ) + ]) + elif promo.type in [PromoCodeType.SUBSCRIPTION_DAYS.value, PromoCodeType.TRIAL_SUBSCRIPTION.value]: + keyboard.insert(1, [ + types.InlineKeyboardButton( + text="📅 Количество дней", + callback_data=f"promo_edit_days_{promo.id}" + ) + ]) + + keyboard.extend([ + [ + types.InlineKeyboardButton( + text="⬅️ Назад", + callback_data=f"promo_manage_{promo.id}" + ) + ] + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) + ) + await callback.answer() + + +@admin_required +@error_handler +async def start_edit_promocode_date( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + promo_id = int(callback.data.split('_')[-1]) + + await state.update_data( + editing_promo_id=promo_id, + edit_type="date" + ) + + text = """ +📅 Изменение даты окончания промокода + +Введите количество дней до окончания (от текущего момента): +• Введите 0 для бессрочного промокода +• Введите положительное число для установки срока + +Например: 30 (промокод будет действовать 30 дней) +""" + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")] + ]) + + await callback.message.edit_text(text, reply_markup=keyboard) + await state.set_state(AdminStates.setting_promocode_expiry) + await callback.answer() + + +@admin_required +@error_handler +async def start_edit_promocode_amount( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + """Начать редактирование суммы промокода""" + promo_id = int(callback.data.split('_')[-1]) + + await state.update_data( + editing_promo_id=promo_id, + edit_type="amount" + ) + + text = """ +💰 Изменение суммы бонуса промокода + +Введите новую сумму в рублях: +Например: 500 +""" + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")] + ]) + + await callback.message.edit_text(text, reply_markup=keyboard) + await state.set_state(AdminStates.setting_promocode_value) + await callback.answer() + + +@admin_required +@error_handler +async def start_edit_promocode_days( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + """Начать редактирование количества дней промокода""" + promo_id = int(callback.data.split('_')[-1]) + + await state.update_data( + editing_promo_id=promo_id, + edit_type="days" + ) + + text = """ +📅 Изменение количества дней подписки + +Введите новое количество дней: +Например: 30 +""" + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")] + ]) + + await callback.message.edit_text(text, reply_markup=keyboard) + await state.set_state(AdminStates.setting_promocode_value) + await callback.answer() + + +@admin_required +@error_handler +async def start_edit_promocode_uses( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + """Начать редактирование количества использований промокода""" + promo_id = int(callback.data.split('_')[-1]) + + await state.update_data( + editing_promo_id=promo_id, + edit_type="uses" + ) + + text = """ +📊 Изменение максимального количества использований + +Введите новое количество использований: +• Введите 0 для безлимитных использований +• Введите положительное число для ограничения + +Например: 100 +""" + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"promo_edit_{promo_id}")] + ]) + + await callback.message.edit_text(text, reply_markup=keyboard) + await state.set_state(AdminStates.setting_promocode_uses) + await callback.answer() + + +@admin_required +@error_handler +async def process_promocode_edit_value( + message: types.Message, + db_user: User, + state: FSMContext, + db: AsyncSession +): + data = await state.get_data() + promo_id = data.get('editing_promo_id') + edit_type = data.get('edit_type') + + if not promo_id or not edit_type: + await message.answer("❌ Ошибка: данные сессии утеряны") + await state.clear() + return + + promo = await db.get(PromoCode, promo_id) + if not promo: + await message.answer("❌ Промокод не найден") + await state.clear() + return + + try: + value = int(message.text.strip()) + + if edit_type == "amount": + if value < 1 or value > 10000: + await message.answer("❌ Сумма должна быть от 1 до 10,000 рублей") + return + + await update_promocode(db, promo, balance_bonus_kopeks=value * 100) + await message.answer( + f"✅ Сумма бонуса изменена на {value}₽", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")] + ]) + ) + + elif edit_type == "days": + if value < 1 or value > 3650: + await message.answer("❌ Количество дней должно быть от 1 до 3650") + return + + await update_promocode(db, promo, subscription_days=value) + await message.answer( + f"✅ Количество дней изменено на {value}", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")] + ]) + ) + + await state.clear() + logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: {edit_type} = {value}") + + except ValueError: + await message.answer("❌ Введите корректное число") + + +@admin_required +@error_handler +async def process_promocode_edit_uses( + message: types.Message, + db_user: User, + state: FSMContext, + db: AsyncSession +): + """Обработать новое количество использований""" + data = await state.get_data() + promo_id = data.get('editing_promo_id') + + if not promo_id: + await message.answer("❌ Ошибка: данные сессии утеряны") + await state.clear() + return + + promo = await db.get(PromoCode, promo_id) + if not promo: + await message.answer("❌ Промокод не найден") + await state.clear() + return + + try: + max_uses = int(message.text.strip()) + + if max_uses < 0 or max_uses > 100000: + await message.answer("❌ Количество использований должно быть от 0 до 100,000") + return + + if max_uses == 0: + max_uses = 999999 + + if max_uses < promo.current_uses: + await message.answer( + f"❌ Новый лимит ({max_uses}) не может быть меньше текущих использований ({promo.current_uses})" + ) + return + + await update_promocode(db, promo, max_uses=max_uses) + + uses_text = "безлимитное" if max_uses == 999999 else str(max_uses) + await message.answer( + f"✅ Максимальное количество использований изменено на {uses_text}", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")] + ]) + ) + + await state.clear() + logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: max_uses = {max_uses}") + + except ValueError: + await message.answer("❌ Введите корректное число") + + +@admin_required +@error_handler +async def process_promocode_edit_expiry( + message: types.Message, + db_user: User, + state: FSMContext, + db: AsyncSession +): + """Обработать новую дату окончания""" + data = await state.get_data() + promo_id = data.get('editing_promo_id') + + if not promo_id: + await message.answer("❌ Ошибка: данные сессии утеряны") + await state.clear() + return + + promo = await db.get(PromoCode, promo_id) + if not promo: + await message.answer("❌ Промокод не найден") + await state.clear() + return + + try: + expiry_days = int(message.text.strip()) + + if expiry_days < 0 or expiry_days > 3650: + await message.answer("❌ Срок действия должен быть от 0 до 3650 дней") + return + + valid_until = None + if expiry_days > 0: + valid_until = datetime.utcnow() + timedelta(days=expiry_days) + + await update_promocode(db, promo, valid_until=valid_until) + + if valid_until: + expiry_text = f"до {format_datetime(valid_until)}" + else: + expiry_text = "бессрочно" + + await message.answer( + f"✅ Срок действия промокода изменен: {expiry_text}", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🎫 К промокоду", callback_data=f"promo_manage_{promo_id}")] + ]) + ) + + await state.clear() + logger.info(f"Промокод {promo.code} отредактирован администратором {db_user.telegram_id}: expiry = {expiry_days} дней") + + except ValueError: + await message.answer("❌ Введите корректное число дней") + @admin_required @error_handler async def toggle_promocode_status( @@ -399,164 +765,4 @@ async def process_promocode_code( ) await state.set_state(AdminStates.setting_promocode_value) elif promo_type == "trial": - await message.answer( - f"🎁 Промокод: {code}\n\n" - f"Введите количество дней тестовой подписки:" - ) - await state.set_state(AdminStates.setting_promocode_value) - - -@admin_required -@error_handler -async def process_promocode_value( - message: types.Message, - db_user: User, - state: FSMContext -): - try: - value = int(message.text.strip()) - - data = await state.get_data() - promo_type = data.get('promocode_type') - - if promo_type == "balance" and (value < 1 or value > 10000): - await message.answer("❌ Сумма должна быть от 1 до 10,000 рублей") - return - elif promo_type in ["days", "trial"] and (value < 1 or value > 3650): - await message.answer("❌ Количество дней должно быть от 1 до 3650") - return - - await state.update_data(promocode_value=value) - - await message.answer( - f"📊 Введите количество использований промокода (или 0 для безлимита):" - ) - await state.set_state(AdminStates.setting_promocode_uses) - - except ValueError: - await message.answer("❌ Введите корректное число") - - -@admin_required -@error_handler -async def process_promocode_uses( - message: types.Message, - db_user: User, - state: FSMContext -): - try: - max_uses = int(message.text.strip()) - - if max_uses < 0 or max_uses > 100000: - await message.answer("❌ Количество использований должно быть от 0 до 100,000") - return - - if max_uses == 0: - max_uses = 999999 - - await state.update_data(promocode_max_uses=max_uses) - - await message.answer( - f"⏰ Введите срок действия промокода в днях (или 0 для бессрочного):" - ) - await state.set_state(AdminStates.setting_promocode_expiry) - - except ValueError: - await message.answer("❌ Введите корректное число") - - -@admin_required -@error_handler -async def process_promocode_expiry( - message: types.Message, - db_user: User, - state: FSMContext, - db: AsyncSession -): - try: - expiry_days = int(message.text.strip()) - - if expiry_days < 0 or expiry_days > 3650: - await message.answer("❌ Срок действия должен быть от 0 до 3650 дней") - return - - data = await state.get_data() - code = data.get('promocode_code') - promo_type = data.get('promocode_type') - value = data.get('promocode_value', 0) - max_uses = data.get('promocode_max_uses', 1) - - valid_until = None - if expiry_days > 0: - valid_until = datetime.utcnow() + timedelta(days=expiry_days) - - type_map = { - "balance": PromoCodeType.BALANCE, - "days": PromoCodeType.SUBSCRIPTION_DAYS, - "trial": PromoCodeType.TRIAL_SUBSCRIPTION - } - - promocode = await create_promocode( - db=db, - code=code, - type=type_map[promo_type], - balance_bonus_kopeks=value * 100 if promo_type == "balance" else 0, - subscription_days=value if promo_type in ["days", "trial"] else 0, - max_uses=max_uses, - valid_until=valid_until, - created_by=db_user.id - ) - - type_names = { - "balance": "Пополнение баланса", - "days": "Дни подписки", - "trial": "Тестовая подписка" - } - - summary_text = f""" -✅ Промокод создан! - -🎫 Код: {promocode.code} -📝 Тип: {type_names.get(promo_type)} -""" - - if promo_type == "balance": - summary_text += f"💰 Сумма: {settings.format_price(promocode.balance_bonus_kopeks)}\n" - elif promo_type in ["days", "trial"]: - summary_text += f"📅 Дней: {promocode.subscription_days}\n" - - summary_text += f"📊 Использований: {promocode.max_uses}\n" - - if promocode.valid_until: - summary_text += f"⏰ Действует до: {format_datetime(promocode.valid_until)}\n" - - await message.answer( - summary_text, - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="🎫 К промокодам", callback_data="admin_promocodes")] - ]) - ) - - await state.clear() - logger.info(f"Создан промокод {code} администратором {db_user.telegram_id}") - - except ValueError: - await message.answer("❌ Введите корректное число дней") - - -def register_handlers(dp: Dispatcher): - dp.callback_query.register(show_promocodes_menu, F.data == "admin_promocodes") - dp.callback_query.register(show_promocodes_list, F.data == "admin_promo_list") - dp.callback_query.register(start_promocode_creation, F.data == "admin_promo_create") - dp.callback_query.register(select_promocode_type, F.data.startswith("promo_type_")) - - dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_")) - dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_")) - dp.callback_query.register(confirm_delete_promocode, F.data.startswith("promo_delete_")) - dp.callback_query.register(delete_promocode_confirmed, F.data.startswith("promo_delete_confirm_")) - dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_")) - - dp.message.register(process_promocode_code, AdminStates.creating_promocode) - dp.message.register(process_promocode_value, AdminStates.setting_promocode_value) - dp.message.register(process_promocode_uses, AdminStates.setting_promocode_uses) - dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry) \ No newline at end of file + await message