diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py index 4d150780..3e16e198 100644 --- a/handlers/admin/tariffs/tariffs_handler.py +++ b/handlers/admin/tariffs/tariffs_handler.py @@ -1,4 +1,5 @@ from datetime import datetime +import re from aiogram import F, Router from aiogram.fsm.context import FSMContext @@ -14,7 +15,7 @@ from sqlalchemy import delete, distinct, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from database import create_tariff -from database.models import Key, Server, Tariff +from database.models import Key, Server, Tariff, Gift from database.tariffs import create_subgroup_hash, find_subgroup_by_hash from filters.admin import IsAdminFilter @@ -109,6 +110,14 @@ async def start_tariff_creation(callback: CallbackQuery, state: FSMContext): @router.message(TariffCreateState.group, IsAdminFilter()) async def process_tariff_group(message: Message, state: FSMContext): group_code = message.text.strip().lower() + + if not re.fullmatch(r"[a-z0-9_-]+", group_code): + await message.answer( + "❌ Код группы должен содержать только латинские буквы, цифры, дефисы и подчёркивания.\n\nПовторите ввод:", + reply_markup=build_cancel_kb(), + ) + return + await state.update_data(group_code=group_code) await state.set_state(TariffCreateState.name) await message.answer( @@ -324,8 +333,58 @@ async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallbac @router.callback_query(AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter()) -async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback): +async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): tariff_id = int(callback_data.action.split("|", 1)[1]) + + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) + tariff = result.scalar_one_or_none() + + if not tariff: + await callback.message.edit_text("❌ Тариф не найден.") + return + + group_code = tariff.group_code + + if group_code == "gifts": + gift_check = await session.execute(select(Gift).where(Gift.tariff_id == tariff_id).limit(1)) + if gift_check.scalar_one_or_none(): + result = await session.execute( + select(Tariff).where(Tariff.group_code == "gifts", Tariff.id != tariff_id) + ) + other_tariffs = result.scalars().all() + + if not other_tariffs: + await callback.message.edit_text( + "❌ Нельзя удалить тариф — он используется в подарках, а других тарифов в группе 'gifts' нет.", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() + ) + ] + ] + ), + ) + return + + builder = InlineKeyboardBuilder() + for t in other_tariffs: + builder.button( + text=f"{t.name} — {t.price_rub}₽", + callback_data=f"confirm_delete_tariff_with_replace|{tariff_id}|{t.id}" + ) + builder.button( + text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() + ) + + await callback.message.edit_text( + "Этот тариф используется в подарках.\n\n" + "Выберите тариф, на который заменить его во всех подарках перед удалением:", + reply_markup=builder.as_markup() + ) + return + await callback.message.edit_text( "⚠️ Вы уверены, что хотите удалить этот тариф?", reply_markup=InlineKeyboardMarkup( @@ -339,6 +398,49 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT ) +@router.callback_query(F.data.startswith("confirm_delete_tariff_with_replace|"), IsAdminFilter()) +async def delete_tariff_with_gift_replacement(callback: CallbackQuery, session: AsyncSession): + _, tariff_id_str, replacement_id_str = callback.data.split("|") + tariff_id = int(tariff_id_str) + replacement_id = int(replacement_id_str) + + await session.execute( + update(Gift).where(Gift.tariff_id == tariff_id).values(tariff_id=replacement_id) + ) + + await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None)) + + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) + tariff = result.scalar_one_or_none() + + if not tariff: + await callback.message.edit_text("❌ Тариф не найден.") + return + + group_code = tariff.group_code + + await session.execute(delete(Tariff).where(Tariff.id == tariff_id)) + + result = await session.execute(select(Tariff).where(Tariff.group_code == group_code)) + remaining_tariffs = result.scalars().all() + if not remaining_tariffs: + await session.execute(update(Server).where(Server.tariff_group == group_code).values(tariff_group=None)) + + await session.commit() + await callback.message.edit_text("🗑 Тариф удалён. Все подарки обновлены.", reply_markup=build_tariff_menu_kb()) + + +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter()) +async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext): + tariff_id = int(callback_data.action.split("|")[1]) + await state.update_data(tariff_id=tariff_id) + await state.set_state(TariffEditState.choosing_field) + await callback.message.edit_text( + "✏️ Что вы хотите изменить?", + reply_markup=build_edit_tariff_fields_kb(tariff_id), + ) + + @router.callback_query(F.data.startswith("confirm_delete_tariff|"), IsAdminFilter()) async def delete_tariff(callback: CallbackQuery, session: AsyncSession): tariff_id = int(callback.data.split("|", 1)[1]) @@ -353,7 +455,6 @@ async def delete_tariff(callback: CallbackQuery, session: AsyncSession): group_code = tariff.group_code await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None)) - await session.execute(delete(Tariff).where(Tariff.id == tariff_id)) result = await session.execute(select(Tariff).where(Tariff.group_code == group_code)) @@ -366,16 +467,6 @@ async def delete_tariff(callback: CallbackQuery, session: AsyncSession): await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb()) -@router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter()) -async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext): - tariff_id = int(callback_data.action.split("|")[1]) - await state.update_data(tariff_id=tariff_id) - await state.set_state(TariffEditState.choosing_field) - await callback.message.edit_text( - "✏️ Что вы хотите изменить?", - reply_markup=build_edit_tariff_fields_kb(tariff_id), - ) - @router.callback_query(F.data.startswith("edit_field|"), IsAdminFilter()) async def ask_new_value(callback: CallbackQuery, state: FSMContext): diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index d10f931a..7ffb3ab3 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -25,7 +25,6 @@ from handlers.buttons import ( PC_PC, SUPPORT, TV_CONTINUE, - TV_INSTRUCTIONS, ) from handlers.texts import ( CHOOSE_DEVICE_TEXT, diff --git a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so index 0504539f..2e87eaaa 100644 Binary files a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so differ