diff --git a/app/handlers/admin/campaigns.py b/app/handlers/admin/campaigns.py
index 7847ac75..1162495c 100644
--- a/app/handlers/admin/campaigns.py
+++ b/app/handlers/admin/campaigns.py
@@ -19,6 +19,7 @@ from app.database.crud.campaign import (
update_campaign,
)
from app.database.crud.server_squad import get_all_server_squads, get_server_squad_by_id
+from app.database.crud.tariff import get_all_tariffs, get_tariff_by_id
from app.database.models import User
from app.keyboards.admin import (
get_admin_campaigns_keyboard,
@@ -44,13 +45,13 @@ def _format_campaign_summary(campaign, texts) -> str:
if campaign.is_balance_bonus:
bonus_text = texts.format_price(campaign.balance_bonus_kopeks)
bonus_info = f"💰 Бонус на баланс: {bonus_text}"
- else:
+ elif campaign.is_subscription_bonus:
traffic_text = texts.format_traffic(campaign.subscription_traffic_gb or 0)
device_limit = campaign.subscription_device_limit
if device_limit is None:
device_limit = settings.DEFAULT_DEVICE_LIMIT
bonus_info = (
- "📱 Подписка: {days} д.\n"
+ "📱 Пробная подписка: {days} д.\n"
"🌐 Трафик: {traffic}\n"
"📱 Устройства: {devices}"
).format(
@@ -58,6 +59,21 @@ def _format_campaign_summary(campaign, texts) -> str:
traffic=traffic_text,
devices=device_limit,
)
+ elif campaign.is_tariff_bonus:
+ tariff_name = "Не выбран"
+ if hasattr(campaign, 'tariff') and campaign.tariff:
+ tariff_name = campaign.tariff.name
+ bonus_info = (
+ "🎁 Тариф: {tariff_name}\n"
+ "📅 Длительность: {days} д."
+ ).format(
+ tariff_name=tariff_name,
+ days=campaign.tariff_duration_days or 0,
+ )
+ elif campaign.is_none_bonus:
+ bonus_info = "🔗 Только ссылка (без награды)"
+ else:
+ bonus_info = "❓ Неизвестный тип бонуса"
return (
f"{campaign.name}\n"
@@ -138,7 +154,7 @@ async def _render_campaign_edit_menu(
message_id=message_id,
reply_markup=get_campaign_edit_keyboard(
campaign.id,
- is_balance_bonus=campaign.is_balance_bonus,
+ bonus_type=campaign.bonus_type,
language=language,
),
parse_mode="HTML",
@@ -1367,7 +1383,18 @@ async def select_campaign_bonus_type(
state: FSMContext,
db: AsyncSession,
):
- bonus_type = "balance" if callback.data.endswith("balance") else "subscription"
+ # Определяем тип бонуса из callback_data
+ if callback.data.endswith("balance"):
+ bonus_type = "balance"
+ elif callback.data.endswith("subscription"):
+ bonus_type = "subscription"
+ elif callback.data.endswith("tariff"):
+ bonus_type = "tariff"
+ elif callback.data.endswith("none"):
+ bonus_type = "none"
+ else:
+ bonus_type = "balance"
+
await state.update_data(campaign_bonus_type=bonus_type)
if bonus_type == "balance":
@@ -1384,10 +1411,10 @@ async def select_campaign_bonus_type(
]
),
)
- else:
+ elif bonus_type == "subscription":
await state.set_state(AdminStates.creating_campaign_subscription_days)
await callback.message.edit_text(
- "📅 Введите длительность подписки в днях (1-730):",
+ "📅 Введите длительность пробной подписки в днях (1-730):",
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[
@@ -1398,6 +1425,63 @@ async def select_campaign_bonus_type(
]
),
)
+ elif bonus_type == "tariff":
+ # Показываем выбор тарифа
+ tariffs = await get_all_tariffs(db, include_inactive=False)
+ if not tariffs:
+ await callback.answer(
+ "❌ Нет доступных тарифов. Сначала создайте тариф.",
+ show_alert=True,
+ )
+ return
+
+ keyboard = []
+ for tariff in tariffs[:15]: # Максимум 15 тарифов
+ keyboard.append([
+ types.InlineKeyboardButton(
+ text=f"🎁 {tariff.name}",
+ callback_data=f"campaign_select_tariff_{tariff.id}",
+ )
+ ])
+ keyboard.append([
+ types.InlineKeyboardButton(
+ text="⬅️ Назад", callback_data="admin_campaigns"
+ )
+ ])
+
+ await state.set_state(AdminStates.creating_campaign_tariff_select)
+ await callback.message.edit_text(
+ "🎁 Выберите тариф для выдачи:",
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
+ )
+ elif bonus_type == "none":
+ # Сразу создаём кампанию без бонуса
+ data = await state.get_data()
+ campaign = await create_campaign(
+ db,
+ name=data["campaign_name"],
+ start_parameter=data["campaign_start_parameter"],
+ bonus_type="none",
+ created_by=db_user.id,
+ )
+ await state.clear()
+
+ deep_link = await _get_bot_deep_link(callback, campaign.start_parameter)
+ texts = get_texts(db_user.language)
+ summary = _format_campaign_summary(campaign, texts)
+ text = (
+ "✅ Кампания создана!\n\n"
+ f"{summary}\n"
+ f"🔗 Ссылка: {deep_link}"
+ )
+
+ await callback.message.edit_text(
+ text,
+ reply_markup=get_campaign_management_keyboard(
+ campaign.id, campaign.is_active, db_user.language
+ ),
+ )
+
await callback.answer()
@@ -1615,6 +1699,276 @@ async def finalize_campaign_subscription(
await callback.answer()
+@admin_required
+@error_handler
+async def select_campaign_tariff(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ """Обработка выбора тарифа для кампании."""
+ tariff_id = int(callback.data.split("_")[-1])
+ tariff = await get_tariff_by_id(db, tariff_id)
+
+ if not tariff:
+ await callback.answer("❌ Тариф не найден", show_alert=True)
+ return
+
+ await state.update_data(campaign_tariff_id=tariff_id, campaign_tariff_name=tariff.name)
+ await state.set_state(AdminStates.creating_campaign_tariff_days)
+ await callback.message.edit_text(
+ f"🎁 Выбран тариф: {tariff.name}\n\n"
+ "📅 Введите длительность тарифа в днях (1-730):",
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="⬅️ Назад", callback_data="admin_campaigns"
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_campaign_tariff_days(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ """Обработка ввода длительности тарифа для кампании."""
+ try:
+ days = int(message.text.strip())
+ except ValueError:
+ await message.answer("❌ Введите число дней (1-730)")
+ return
+
+ if days <= 0 or days > 730:
+ await message.answer("❌ Длительность должна быть от 1 до 730 дней")
+ return
+
+ data = await state.get_data()
+ tariff_id = data.get("campaign_tariff_id")
+
+ if not tariff_id:
+ await message.answer("❌ Тариф не выбран. Начните создание кампании заново.")
+ await state.clear()
+ return
+
+ campaign = await create_campaign(
+ db,
+ name=data["campaign_name"],
+ start_parameter=data["campaign_start_parameter"],
+ bonus_type="tariff",
+ tariff_id=tariff_id,
+ tariff_duration_days=days,
+ created_by=db_user.id,
+ )
+
+ await state.clear()
+
+ deep_link = await _get_bot_deep_link_from_message(message, campaign.start_parameter)
+ texts = get_texts(db_user.language)
+ summary = _format_campaign_summary(campaign, texts)
+ text = (
+ "✅ Кампания создана!\n\n"
+ f"{summary}\n"
+ f"🔗 Ссылка: {deep_link}"
+ )
+
+ await message.answer(
+ text,
+ reply_markup=get_campaign_management_keyboard(
+ campaign.id, campaign.is_active, db_user.language
+ ),
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_tariff(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ """Начало редактирования тарифа кампании."""
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not campaign.is_tariff_bonus:
+ await callback.answer("❌ Эта кампания не использует тип 'Тариф'", show_alert=True)
+ return
+
+ tariffs = await get_all_tariffs(db, include_inactive=False)
+ if not tariffs:
+ await callback.answer("❌ Нет доступных тарифов", show_alert=True)
+ return
+
+ keyboard = []
+ for tariff in tariffs[:15]:
+ is_current = campaign.tariff_id == tariff.id
+ emoji = "✅" if is_current else "🎁"
+ keyboard.append([
+ types.InlineKeyboardButton(
+ text=f"{emoji} {tariff.name}",
+ callback_data=f"campaign_edit_set_tariff_{campaign_id}_{tariff.id}",
+ )
+ ])
+ keyboard.append([
+ types.InlineKeyboardButton(
+ text="⬅️ Назад", callback_data=f"admin_campaign_edit_{campaign_id}"
+ )
+ ])
+
+ current_tariff_name = "Не выбран"
+ if campaign.tariff:
+ current_tariff_name = campaign.tariff.name
+
+ await callback.message.edit_text(
+ f"🎁 Изменение тарифа кампании\n\n"
+ f"Текущий тариф: {current_tariff_name}\n"
+ "Выберите новый тариф:",
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def set_campaign_tariff(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ """Установка тарифа для кампании."""
+ parts = callback.data.split("_")
+ campaign_id = int(parts[-2])
+ tariff_id = int(parts[-1])
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ tariff = await get_tariff_by_id(db, tariff_id)
+ if not tariff:
+ await callback.answer("❌ Тариф не найден", show_alert=True)
+ return
+
+ await update_campaign(db, campaign, tariff_id=tariff_id)
+ await callback.answer(f"✅ Тариф изменён на '{tariff.name}'")
+
+ await _render_campaign_edit_menu(
+ callback.bot,
+ callback.message.chat.id,
+ callback.message.message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_tariff_days(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ """Начало редактирования длительности тарифа."""
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not campaign.is_tariff_bonus:
+ await callback.answer("❌ Эта кампания не использует тип 'Тариф'", show_alert=True)
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_tariff_days)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ await callback.message.edit_text(
+ f"📅 Изменение длительности тарифа\n\n"
+ f"Текущее значение: {campaign.tariff_duration_days or 0} д.\n"
+ "Введите новое количество дней (1-730):",
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_tariff_days(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ """Обработка ввода новой длительности тарифа."""
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ try:
+ days = int(message.text.strip())
+ except ValueError:
+ await message.answer("❌ Введите число дней (1-730)")
+ return
+
+ if days <= 0 or days > 730:
+ await message.answer("❌ Длительность должна быть от 1 до 730 дней")
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, tariff_duration_days=days)
+ await state.clear()
+
+ await message.answer("✅ Длительность тарифа обновлена.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_campaigns_menu, F.data == "admin_campaigns")
dp.callback_query.register(
@@ -1688,6 +2042,18 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
select_campaign_bonus_type, F.data.startswith("campaign_bonus_")
)
+ dp.callback_query.register(
+ select_campaign_tariff, F.data.startswith("campaign_select_tariff_")
+ )
+ dp.callback_query.register(
+ start_edit_campaign_tariff, F.data.startswith("admin_campaign_edit_tariff_")
+ )
+ dp.callback_query.register(
+ set_campaign_tariff, F.data.startswith("campaign_edit_set_tariff_")
+ )
+ dp.callback_query.register(
+ start_edit_campaign_tariff_days, F.data.startswith("admin_campaign_edit_tariff_days_")
+ )
dp.message.register(process_campaign_name, AdminStates.creating_campaign_name)
dp.message.register(
@@ -1731,3 +2097,11 @@ def register_handlers(dp: Dispatcher):
process_edit_campaign_subscription_devices,
AdminStates.editing_campaign_subscription_devices,
)
+ dp.message.register(
+ process_campaign_tariff_days,
+ AdminStates.creating_campaign_tariff_days,
+ )
+ dp.message.register(
+ process_edit_campaign_tariff_days,
+ AdminStates.editing_campaign_tariff_days,
+ )