mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
2) У промогрупп появится приоритет 3) У пользователя может быть несколько промогрупп, но влиять будет только с наивысшим приоритетом 4) К промокодам можно будет добавить промогруппу. Все активировавшие промокод получат её 5) При выводе пользователей с промогруппой будет также выводиться ссылка на каждого. Можно будет отследить сливы промокодов "для своих". Я в целом это добавлю во все места, где пользователь выводится в админке 6) Исправить баг исчезновения триалки при пополнении 7) Исправить падающие тесты и добавить новых 8) Трафик: 0 ГБ в тестовой подписке исправить на Трафик: Безлимит
1471 lines
46 KiB
Python
1471 lines
46 KiB
Python
import logging
|
||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||
from typing import Dict, Optional, Tuple
|
||
|
||
from aiogram import Dispatcher, types, F
|
||
from aiogram.exceptions import TelegramBadRequest
|
||
from aiogram.fsm.context import FSMContext
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.database.crud.promo_group import (
|
||
get_promo_groups_with_counts,
|
||
get_promo_group_by_id,
|
||
create_promo_group,
|
||
update_promo_group,
|
||
delete_promo_group,
|
||
get_promo_group_members,
|
||
count_promo_group_members,
|
||
)
|
||
from app.database.models import PromoGroup
|
||
from app.localization.texts import get_texts
|
||
from app.states import AdminStates
|
||
from app.utils.decorators import admin_required, error_handler
|
||
from app.keyboards.admin import (
|
||
get_admin_pagination_keyboard,
|
||
get_confirmation_keyboard,
|
||
)
|
||
from app.utils.pricing_utils import format_period_description
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _format_discount_lines(texts, group) -> list[str]:
|
||
return [
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_DISCOUNTS_HEADER",
|
||
"💸 Скидки промогруппы:",
|
||
),
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_DISCOUNT_LINE_SERVERS",
|
||
"• Серверы: {percent}%",
|
||
).format(percent=group.server_discount_percent),
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_DISCOUNT_LINE_TRAFFIC",
|
||
"• Трафик: {percent}%",
|
||
).format(percent=group.traffic_discount_percent),
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_DISCOUNT_LINE_DEVICES",
|
||
"• Устройства: {percent}%",
|
||
).format(percent=group.device_discount_percent),
|
||
]
|
||
|
||
|
||
def _format_addon_discounts_line(texts, group: PromoGroup) -> str:
|
||
enabled = getattr(group, "apply_discounts_to_addons", True)
|
||
if enabled:
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_ENABLED",
|
||
"🧩 Скидки на доп. услуги: <b>включены</b>",
|
||
)
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_DISABLED",
|
||
"🧩 Скидки на доп. услуги: <b>отключены</b>",
|
||
)
|
||
|
||
|
||
def _get_addon_discounts_button_text(texts, group: PromoGroup) -> str:
|
||
enabled = getattr(group, "apply_discounts_to_addons", True)
|
||
if enabled:
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE",
|
||
"🧩 Отключить скидки на доп. услуги",
|
||
)
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_ENABLE",
|
||
"🧩 Включить скидки на доп. услуги",
|
||
)
|
||
|
||
|
||
def _normalize_periods_dict(raw: Optional[Dict]) -> Dict[int, int]:
|
||
if not raw or not isinstance(raw, dict):
|
||
return {}
|
||
|
||
normalized: Dict[int, int] = {}
|
||
|
||
for key, value in raw.items():
|
||
try:
|
||
period = int(key)
|
||
percent = int(value)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
|
||
normalized[period] = max(0, min(100, percent))
|
||
|
||
return normalized
|
||
|
||
|
||
def _collect_period_discounts(group: PromoGroup) -> Dict[int, int]:
|
||
discounts = _normalize_periods_dict(getattr(group, "period_discounts", None))
|
||
|
||
if discounts:
|
||
return dict(sorted(discounts.items()))
|
||
|
||
if group.is_default and settings.is_base_promo_group_period_discount_enabled():
|
||
try:
|
||
base_discounts = settings.get_base_promo_group_period_discounts()
|
||
normalized = _normalize_periods_dict(base_discounts)
|
||
return dict(sorted(normalized.items()))
|
||
except Exception:
|
||
return {}
|
||
|
||
return {}
|
||
|
||
|
||
def _format_period_discounts_lines(texts, group: PromoGroup, language: str) -> list:
|
||
discounts = _collect_period_discounts(group)
|
||
|
||
if not discounts:
|
||
return []
|
||
|
||
header = texts.t(
|
||
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER",
|
||
"⏳ Скидки по периодам:",
|
||
)
|
||
|
||
lines = [header]
|
||
|
||
for period_days, percent in discounts.items():
|
||
period_display = format_period_description(period_days, language)
|
||
lines.append(
|
||
texts.t("PROMO_GROUP_PERIOD_DISCOUNT_ITEM", "{period} — {percent}%").format(
|
||
period=period_display,
|
||
percent=percent,
|
||
)
|
||
)
|
||
|
||
return lines
|
||
|
||
|
||
def _format_period_discounts_value(discounts: Dict[int, int]) -> str:
|
||
if not discounts:
|
||
return "0"
|
||
|
||
return ", ".join(
|
||
f"{period}:{percent}"
|
||
for period, percent in sorted(discounts.items())
|
||
)
|
||
|
||
|
||
def _parse_period_discounts_input(value: str) -> Dict[int, int]:
|
||
cleaned = (value or "").strip()
|
||
|
||
if not cleaned or cleaned in {"0", "-"}:
|
||
return {}
|
||
|
||
cleaned = cleaned.replace(";", ",").replace("\n", ",")
|
||
parts = [part.strip() for part in cleaned.split(",") if part.strip()]
|
||
|
||
if not parts:
|
||
return {}
|
||
|
||
discounts: Dict[int, int] = {}
|
||
|
||
for part in parts:
|
||
if ":" not in part:
|
||
raise ValueError
|
||
|
||
period_raw, percent_raw = part.split(":", 1)
|
||
|
||
period = int(period_raw.strip())
|
||
percent = int(percent_raw.strip())
|
||
|
||
if period <= 0:
|
||
raise ValueError
|
||
|
||
discounts[period] = max(0, min(100, percent))
|
||
|
||
return discounts
|
||
|
||
|
||
async def _prompt_for_period_discounts(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
prompt_key: str,
|
||
default_text: str,
|
||
*,
|
||
current_value: Optional[str] = None,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", "ru"))
|
||
prompt_text = texts.t(prompt_key, default_text)
|
||
|
||
if current_value is not None:
|
||
try:
|
||
prompt_text = prompt_text.format(current=current_value)
|
||
except KeyError:
|
||
pass
|
||
|
||
await message.answer(prompt_text)
|
||
|
||
|
||
def _format_rubles(amount_kopeks: int) -> str:
|
||
if amount_kopeks <= 0:
|
||
return "0"
|
||
|
||
rubles = Decimal(amount_kopeks) / Decimal(100)
|
||
if rubles == rubles.to_integral_value():
|
||
formatted = f"{rubles:,.0f}"
|
||
else:
|
||
formatted = f"{rubles:,.2f}"
|
||
|
||
return formatted.replace(",", " ")
|
||
|
||
|
||
def _format_priority_line(texts, group: PromoGroup) -> str:
|
||
priority = getattr(group, "priority", 0)
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_PRIORITY_LINE",
|
||
"🎯 Приоритет: {priority}",
|
||
).format(priority=priority)
|
||
|
||
|
||
def _format_auto_assign_line(texts, group: PromoGroup) -> str:
|
||
threshold = getattr(group, "auto_assign_total_spent_kopeks", 0) or 0
|
||
|
||
if threshold <= 0:
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_AUTO_ASSIGN_DISABLED",
|
||
"Автовыдача по суммарным тратам: отключена",
|
||
)
|
||
|
||
amount = _format_rubles(threshold)
|
||
return texts.t(
|
||
"ADMIN_PROMO_GROUP_AUTO_ASSIGN_LINE",
|
||
"Автовыдача по суммарным тратам: от {amount} ₽",
|
||
).format(amount=amount)
|
||
|
||
|
||
def _format_auto_assign_value(value_kopeks: Optional[int]) -> str:
|
||
if not value_kopeks or value_kopeks <= 0:
|
||
return "0"
|
||
|
||
rubles = Decimal(value_kopeks) / Decimal(100)
|
||
quantized = (
|
||
rubles.quantize(Decimal("1"))
|
||
if rubles == rubles.to_integral_value()
|
||
else rubles.quantize(Decimal("0.01"))
|
||
)
|
||
return str(quantized)
|
||
|
||
|
||
def _parse_auto_assign_threshold_input(value: str) -> int:
|
||
cleaned = (value or "").strip()
|
||
|
||
if not cleaned or cleaned in {"0", "-", "off", "нет"}:
|
||
return 0
|
||
|
||
normalized = cleaned.replace(" ", "").replace(",", ".")
|
||
|
||
try:
|
||
amount = Decimal(normalized)
|
||
except InvalidOperation:
|
||
raise ValueError
|
||
|
||
if amount < 0:
|
||
raise ValueError
|
||
|
||
kopeks = int((amount * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
||
return max(0, kopeks)
|
||
|
||
|
||
async def _prompt_for_auto_assign_threshold(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
prompt_key: str,
|
||
default_text: str,
|
||
*,
|
||
current_value: Optional[str] = None,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", "ru"))
|
||
prompt_text = texts.t(prompt_key, default_text)
|
||
|
||
if current_value is not None:
|
||
try:
|
||
prompt_text = prompt_text.format(current=current_value)
|
||
except KeyError:
|
||
pass
|
||
|
||
await message.answer(prompt_text)
|
||
|
||
|
||
def _build_edit_menu_content(
|
||
texts,
|
||
group: PromoGroup,
|
||
language: str,
|
||
) -> Tuple[str, types.InlineKeyboardMarkup]:
|
||
header = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_MENU_TITLE",
|
||
"✏️ Настройки промогруппы «{name}»",
|
||
).format(name=group.name)
|
||
|
||
lines = [header]
|
||
lines.extend(_format_discount_lines(texts, group))
|
||
lines.append(_format_addon_discounts_line(texts, group))
|
||
lines.append(_format_priority_line(texts, group))
|
||
lines.append(_format_auto_assign_line(texts, group))
|
||
|
||
period_lines = _format_period_discounts_lines(texts, group, language)
|
||
lines.extend(period_lines)
|
||
|
||
lines.append(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_MENU_HINT",
|
||
"Выберите параметр для изменения:",
|
||
)
|
||
)
|
||
|
||
text = "\n".join(line for line in lines if line)
|
||
|
||
keyboard_rows = [
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_NAME",
|
||
"✏️ Изменить название",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_name",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_PRIORITY",
|
||
"🎯 Приоритет",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_priority",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_TRAFFIC",
|
||
"🌐 Скидка на трафик",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_traffic",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_SERVERS",
|
||
"🖥 Скидка на серверы",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_servers",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_DEVICES",
|
||
"📱 Скидка на устройства",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_devices",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_PERIODS",
|
||
"⏳ Скидки по периодам",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_periods",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=_get_addon_discounts_button_text(texts, group),
|
||
callback_data=f"promo_group_toggle_addons_{group.id}",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_FIELD_AUTO_ASSIGN",
|
||
"🤖 Автовыдача по тратам",
|
||
),
|
||
callback_data=f"promo_group_edit_field_{group.id}_auto",
|
||
)
|
||
],
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.BACK,
|
||
callback_data=f"promo_group_manage_{group.id}",
|
||
)
|
||
],
|
||
]
|
||
|
||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
|
||
return text, keyboard
|
||
|
||
|
||
def _get_edit_prompt_keyboard(group_id: int, texts) -> types.InlineKeyboardMarkup:
|
||
return types.InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.BACK,
|
||
callback_data=f"promo_group_edit_{group_id}",
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
async def _send_edit_menu_after_update(
|
||
message: types.Message,
|
||
texts,
|
||
group: PromoGroup,
|
||
language: str,
|
||
success_message: Optional[str] = None,
|
||
):
|
||
menu_text, keyboard = _build_edit_menu_content(texts, group, language)
|
||
parts = [part for part in [success_message, menu_text] if part]
|
||
|
||
text = "\n\n".join(parts)
|
||
|
||
from_user = getattr(message, "from_user", None)
|
||
|
||
if getattr(from_user, "is_bot", False):
|
||
try:
|
||
await message.edit_text(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
return
|
||
except TelegramBadRequest:
|
||
pass
|
||
|
||
await message.answer(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promo_groups_menu(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
texts = get_texts(db_user.language)
|
||
groups = await get_promo_groups_with_counts(db)
|
||
|
||
total_members = sum(count for _, count in groups)
|
||
header = texts.t("ADMIN_PROMO_GROUPS_TITLE", "💳 <b>Промогруппы</b>")
|
||
|
||
if groups:
|
||
summary = texts.t(
|
||
"ADMIN_PROMO_GROUPS_SUMMARY",
|
||
"Всего групп: {count}\nВсего участников: {members}",
|
||
).format(count=len(groups), members=total_members)
|
||
lines = [header, "", summary, ""]
|
||
|
||
keyboard_rows = []
|
||
for group, member_count in groups:
|
||
default_suffix = (
|
||
texts.t("ADMIN_PROMO_GROUPS_DEFAULT_LABEL", " (базовая)")
|
||
if group.is_default
|
||
else ""
|
||
)
|
||
group_lines = [
|
||
f"{'⭐' if group.is_default else '🎯'} <b>{group.name}</b>{default_suffix}",
|
||
]
|
||
group_lines.extend(_format_discount_lines(texts, group))
|
||
group_lines.append(_format_auto_assign_line(texts, group))
|
||
group_lines.append(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT",
|
||
"Участников: {count}",
|
||
).format(count=member_count)
|
||
)
|
||
|
||
period_lines = _format_period_discounts_lines(texts, group, db_user.language)
|
||
group_lines.extend(period_lines)
|
||
group_lines.append("")
|
||
|
||
lines.extend(group_lines)
|
||
keyboard_rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=f"{'⭐' if group.is_default else '🎯'} {group.name}",
|
||
callback_data=f"promo_group_manage_{group.id}",
|
||
)
|
||
])
|
||
else:
|
||
lines = [header, "", texts.t("ADMIN_PROMO_GROUPS_EMPTY", "Промогруппы не найдены.")]
|
||
keyboard_rows = []
|
||
|
||
keyboard_rows.append(
|
||
[types.InlineKeyboardButton(text="➕ Создать", callback_data="admin_promo_group_create")]
|
||
)
|
||
keyboard_rows.append(
|
||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_promo")]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
"\n".join(line for line in lines if line is not None),
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
|
||
parse_mode="HTML",
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
async def _get_group_or_alert(
|
||
callback: types.CallbackQuery,
|
||
db: AsyncSession,
|
||
) -> Optional[PromoGroup]:
|
||
group_id = int(callback.data.split("_")[-1])
|
||
group = await get_promo_group_by_id(db, group_id)
|
||
if not group:
|
||
await callback.answer("❌ Промогруппа не найдена", show_alert=True)
|
||
return None
|
||
return group
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promo_group_details(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
group = await _get_group_or_alert(callback, db)
|
||
if not group:
|
||
return
|
||
|
||
texts = get_texts(db_user.language)
|
||
member_count = await count_promo_group_members(db, group.id)
|
||
|
||
default_note = (
|
||
texts.t("ADMIN_PROMO_GROUP_DETAILS_DEFAULT", "Это базовая группа.")
|
||
if group.is_default
|
||
else ""
|
||
)
|
||
|
||
lines = [
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_DETAILS_TITLE",
|
||
"💳 <b>Промогруппа:</b> {name}",
|
||
).format(name=group.name)
|
||
]
|
||
lines.extend(_format_discount_lines(texts, group))
|
||
lines.append(_format_auto_assign_line(texts, group))
|
||
lines.append(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_DETAILS_MEMBERS",
|
||
"Участников: {count}",
|
||
).format(count=member_count)
|
||
)
|
||
|
||
period_lines = _format_period_discounts_lines(texts, group, db_user.language)
|
||
lines.extend(period_lines)
|
||
|
||
if default_note:
|
||
lines.append(default_note)
|
||
|
||
text = "\n".join(line for line in lines if line)
|
||
|
||
keyboard_rows = []
|
||
if member_count > 0:
|
||
keyboard_rows.append(
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_PROMO_GROUP_MEMBERS_BUTTON", "👥 Участники"),
|
||
callback_data=f"promo_group_members_{group.id}_page_1",
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard_rows.append(
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_PROMO_GROUP_EDIT_BUTTON", "✏️ Изменить"),
|
||
callback_data=f"promo_group_edit_{group.id}",
|
||
)
|
||
]
|
||
)
|
||
|
||
if not group.is_default:
|
||
keyboard_rows.append(
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_PROMO_GROUP_DELETE_BUTTON", "🗑️ Удалить"),
|
||
callback_data=f"promo_group_delete_{group.id}",
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard_rows.append(
|
||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_promo_groups")]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
text.strip(),
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
|
||
parse_mode="HTML",
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
def _validate_percent(value: str) -> int:
|
||
percent = int(value)
|
||
if percent < 0 or percent > 100:
|
||
raise ValueError
|
||
return percent
|
||
|
||
|
||
async def _prompt_for_discount(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
prompt_key: str,
|
||
default_text: str,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", "ru"))
|
||
await message.answer(texts.t(prompt_key, default_text))
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_create_promo_group(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
state: FSMContext,
|
||
db: AsyncSession,
|
||
):
|
||
texts = get_texts(db_user.language)
|
||
await state.set_state(AdminStates.creating_promo_group_name)
|
||
await state.update_data(language=db_user.language)
|
||
await callback.message.edit_text(
|
||
texts.t("ADMIN_PROMO_GROUP_CREATE_NAME_PROMPT", "Введите название новой промогруппы:"),
|
||
reply_markup=types.InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_promo_groups")]
|
||
]
|
||
),
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
async def process_create_group_name(message: types.Message, state: FSMContext):
|
||
name = message.text.strip()
|
||
if not name:
|
||
texts = get_texts((await state.get_data()).get("language", "ru"))
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_NAME", "Название не может быть пустым."))
|
||
return
|
||
|
||
await state.update_data(new_group_name=name)
|
||
await state.set_state(AdminStates.creating_promo_group_priority)
|
||
texts = get_texts((await state.get_data()).get("language", "ru"))
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_CREATE_PRIORITY_PROMPT",
|
||
"Введите приоритет группы (0 = базовая, чем больше - тем выше приоритет):",
|
||
)
|
||
)
|
||
|
||
|
||
async def process_create_group_priority(message: types.Message, state: FSMContext):
|
||
texts = get_texts((await state.get_data()).get("language", "ru"))
|
||
try:
|
||
priority = int(message.text)
|
||
if priority < 0:
|
||
raise ValueError
|
||
except (ValueError, TypeError):
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_INVALID_PRIORITY",
|
||
"❌ Приоритет должен быть неотрицательным целым числом",
|
||
)
|
||
)
|
||
return
|
||
|
||
await state.update_data(new_group_priority=priority)
|
||
await state.set_state(AdminStates.creating_promo_group_traffic_discount)
|
||
await _prompt_for_discount(
|
||
message,
|
||
state,
|
||
"ADMIN_PROMO_GROUP_CREATE_TRAFFIC_PROMPT",
|
||
"Введите скидку на трафик (0-100):",
|
||
)
|
||
|
||
|
||
async def process_create_group_traffic(message: types.Message, state: FSMContext):
|
||
texts = get_texts((await state.get_data()).get("language", "ru"))
|
||
try:
|
||
value = _validate_percent(message.text)
|
||
except (ValueError, TypeError):
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||
return
|
||
|
||
await state.update_data(new_group_traffic=value)
|
||
await state.set_state(AdminStates.creating_promo_group_server_discount)
|
||
await _prompt_for_discount(
|
||
message,
|
||
state,
|
||
"ADMIN_PROMO_GROUP_CREATE_SERVERS_PROMPT",
|
||
"Введите скидку на серверы (0-100):",
|
||
)
|
||
|
||
|
||
async def process_create_group_servers(message: types.Message, state: FSMContext):
|
||
texts = get_texts((await state.get_data()).get("language", "ru"))
|
||
try:
|
||
value = _validate_percent(message.text)
|
||
except (ValueError, TypeError):
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||
return
|
||
|
||
await state.update_data(new_group_servers=value)
|
||
await state.set_state(AdminStates.creating_promo_group_device_discount)
|
||
await _prompt_for_discount(
|
||
message,
|
||
state,
|
||
"ADMIN_PROMO_GROUP_CREATE_DEVICES_PROMPT",
|
||
"Введите скидку на устройства (0-100):",
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_create_group_devices(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
devices_discount = _validate_percent(message.text)
|
||
except (ValueError, TypeError):
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||
return
|
||
|
||
await state.update_data(new_group_devices=devices_discount)
|
||
await state.set_state(AdminStates.creating_promo_group_period_discount)
|
||
|
||
await _prompt_for_period_discounts(
|
||
message,
|
||
state,
|
||
"ADMIN_PROMO_GROUP_CREATE_PERIOD_PROMPT",
|
||
"Введите скидки на периоды подписки (например, 30:10, 90:15). Отправьте 0, если без скидок.",
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_create_group_period_discounts(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
period_discounts = _parse_period_discounts_input(message.text)
|
||
except ValueError:
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS",
|
||
"Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.",
|
||
)
|
||
)
|
||
return
|
||
|
||
await state.update_data(new_group_period_discounts=period_discounts)
|
||
await state.set_state(AdminStates.creating_promo_group_auto_assign)
|
||
|
||
await _prompt_for_auto_assign_threshold(
|
||
message,
|
||
state,
|
||
"ADMIN_PROMO_GROUP_CREATE_AUTO_ASSIGN_PROMPT",
|
||
"Введите сумму общих трат (в ₽) для автоматической выдачи этой группы. Отправьте 0, чтобы отключить.",
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_create_group_auto_assign(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
auto_assign_kopeks = _parse_auto_assign_threshold_input(message.text)
|
||
except ValueError:
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_INVALID_AUTO_ASSIGN",
|
||
"Введите неотрицательное число в рублях или 0 для отключения.",
|
||
)
|
||
)
|
||
return
|
||
|
||
try:
|
||
group = await create_promo_group(
|
||
db,
|
||
data["new_group_name"],
|
||
priority=data.get("new_group_priority", 0),
|
||
traffic_discount_percent=data["new_group_traffic"],
|
||
server_discount_percent=data["new_group_servers"],
|
||
device_discount_percent=data["new_group_devices"],
|
||
period_discounts=data.get("new_group_period_discounts"),
|
||
auto_assign_total_spent_kopeks=auto_assign_kopeks,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Не удалось создать промогруппу: {e}")
|
||
await message.answer(texts.ERROR)
|
||
await state.clear()
|
||
return
|
||
|
||
await state.clear()
|
||
await message.answer(
|
||
texts.t("ADMIN_PROMO_GROUP_CREATED", "Промогруппа «{name}» создана.").format(
|
||
name=group.name
|
||
),
|
||
reply_markup=types.InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
types.InlineKeyboardButton(
|
||
text=texts.t(
|
||
"ADMIN_PROMO_GROUP_CREATED_BACK_BUTTON",
|
||
"↩️ К промогруппам",
|
||
),
|
||
callback_data="admin_promo_groups",
|
||
)
|
||
]
|
||
]
|
||
),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_promo_group(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
state: FSMContext,
|
||
db: AsyncSession,
|
||
):
|
||
group = await _get_group_or_alert(callback, db)
|
||
if not group:
|
||
return
|
||
|
||
texts = get_texts(db_user.language)
|
||
await state.update_data(edit_group_id=group.id, language=db_user.language)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
text, keyboard = _build_edit_menu_content(texts, group, db_user.language)
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML",
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def prompt_edit_promo_group_field(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
state: FSMContext,
|
||
db: AsyncSession,
|
||
):
|
||
parts = callback.data.split("_")
|
||
if len(parts) < 6:
|
||
await callback.answer("❌ Неверная команда", show_alert=True)
|
||
return
|
||
|
||
group_id = int(parts[4])
|
||
field = parts[5]
|
||
|
||
group = await get_promo_group_by_id(db, group_id)
|
||
if not group:
|
||
await callback.answer("❌ Промогруппа не найдена", show_alert=True)
|
||
return
|
||
|
||
await state.update_data(edit_group_id=group.id, language=db_user.language)
|
||
|
||
texts = get_texts(db_user.language)
|
||
reply_markup = _get_edit_prompt_keyboard(group.id, texts)
|
||
|
||
if field == "name":
|
||
await state.set_state(AdminStates.editing_promo_group_name)
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_NAME_PROMPT",
|
||
"Введите новое название промогруппы (текущее: {name}):",
|
||
).format(name=group.name)
|
||
elif field == "priority":
|
||
await state.set_state(AdminStates.editing_promo_group_priority)
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_PRIORITY_PROMPT",
|
||
"Введите новый приоритет (текущий: {current}):",
|
||
).format(current=getattr(group, "priority", 0))
|
||
elif field == "traffic":
|
||
await state.set_state(AdminStates.editing_promo_group_traffic_discount)
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT",
|
||
"Введите новую скидку на трафик (текущее значение: {current}%):",
|
||
).format(current=group.traffic_discount_percent)
|
||
elif field == "servers":
|
||
await state.set_state(AdminStates.editing_promo_group_server_discount)
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT",
|
||
"Введите новую скидку на серверы (текущее значение: {current}%):",
|
||
).format(current=group.server_discount_percent)
|
||
elif field == "devices":
|
||
await state.set_state(AdminStates.editing_promo_group_device_discount)
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT",
|
||
"Введите новую скидку на устройства (текущее значение: {current}%):",
|
||
).format(current=group.device_discount_percent)
|
||
elif field == "periods":
|
||
await state.set_state(AdminStates.editing_promo_group_period_discount)
|
||
current_discounts = _normalize_periods_dict(getattr(group, "period_discounts", None))
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT",
|
||
"Введите новые скидки на периоды (текущие: {current}). Отправьте 0, если без скидок.",
|
||
).format(current=_format_period_discounts_value(current_discounts))
|
||
elif field == "auto":
|
||
await state.set_state(AdminStates.editing_promo_group_auto_assign)
|
||
prompt = texts.t(
|
||
"ADMIN_PROMO_GROUP_EDIT_AUTO_ASSIGN_PROMPT",
|
||
"Введите сумму общих трат (в ₽) для автовыдачи. Текущее значение: {current}.",
|
||
).format(current=_format_auto_assign_value(group.auto_assign_total_spent_kopeks))
|
||
else:
|
||
await callback.answer("❌ Неизвестный параметр", show_alert=True)
|
||
return
|
||
|
||
await callback.message.edit_text(prompt, reply_markup=reply_markup)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_name(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
name = message.text.strip()
|
||
if not name:
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_NAME", "Название не может быть пустым."))
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(db, group, name=name)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_priority(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
priority = int(message.text)
|
||
if priority < 0:
|
||
raise ValueError
|
||
except (ValueError, TypeError):
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_INVALID_PRIORITY",
|
||
"❌ Приоритет должен быть неотрицательным целым числом",
|
||
)
|
||
)
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(db, group, priority=priority)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_traffic(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
value = _validate_percent(message.text)
|
||
except (ValueError, TypeError):
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(db, group, traffic_discount_percent=value)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_servers(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
value = _validate_percent(message.text)
|
||
except (ValueError, TypeError):
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(db, group, server_discount_percent=value)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_devices(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
devices_discount = _validate_percent(message.text)
|
||
except (ValueError, TypeError):
|
||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(db, group, device_discount_percent=devices_discount)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_period_discounts(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
period_discounts = _parse_period_discounts_input(message.text)
|
||
except ValueError:
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS",
|
||
"Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.",
|
||
)
|
||
)
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(db, group, period_discounts=period_discounts)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def process_edit_group_auto_assign(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
data = await state.get_data()
|
||
texts = get_texts(data.get("language", db_user.language))
|
||
|
||
try:
|
||
auto_assign_kopeks = _parse_auto_assign_threshold_input(message.text)
|
||
except ValueError:
|
||
await message.answer(
|
||
texts.t(
|
||
"ADMIN_PROMO_GROUP_INVALID_AUTO_ASSIGN",
|
||
"Введите неотрицательное число в рублях или 0 для отключения.",
|
||
)
|
||
)
|
||
return
|
||
|
||
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
|
||
if not group:
|
||
await message.answer("❌ Промогруппа не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
group = await update_promo_group(
|
||
db,
|
||
group,
|
||
auto_assign_total_spent_kopeks=auto_assign_kopeks,
|
||
)
|
||
await state.set_state(AdminStates.editing_promo_group_menu)
|
||
|
||
await _send_edit_menu_after_update(
|
||
message,
|
||
texts,
|
||
group,
|
||
data.get("language", db_user.language),
|
||
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
|
||
)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_promo_group_members(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
parts = callback.data.split("_")
|
||
group_id = int(parts[3])
|
||
page = int(parts[-1])
|
||
limit = 10
|
||
offset = (page - 1) * limit
|
||
|
||
group = await get_promo_group_by_id(db, group_id)
|
||
if not group:
|
||
await callback.answer("❌ Промогруппа не найдена", show_alert=True)
|
||
return
|
||
|
||
texts = get_texts(db_user.language)
|
||
members = await get_promo_group_members(db, group_id, offset=offset, limit=limit)
|
||
total_members = await count_promo_group_members(db, group_id)
|
||
total_pages = max(1, (total_members + limit - 1) // limit)
|
||
|
||
title = texts.t(
|
||
"ADMIN_PROMO_GROUP_MEMBERS_TITLE",
|
||
"👥 Участники группы {name}",
|
||
).format(name=group.name)
|
||
|
||
if not members:
|
||
body = texts.t("ADMIN_PROMO_GROUP_MEMBERS_EMPTY", "В этой группе пока нет участников.")
|
||
else:
|
||
lines = []
|
||
for index, user in enumerate(members, start=offset + 1):
|
||
username = f"@{user.username}" if user.username else "—"
|
||
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
|
||
lines.append(
|
||
f"{index}. {user_link} (ID {user.id}, {username}, TG {user.telegram_id})"
|
||
)
|
||
body = "\n".join(lines)
|
||
|
||
keyboard = []
|
||
if total_pages > 1:
|
||
pagination = get_admin_pagination_keyboard(
|
||
page,
|
||
total_pages,
|
||
f"promo_group_members_{group_id}",
|
||
f"promo_group_manage_{group_id}",
|
||
db_user.language,
|
||
)
|
||
keyboard.extend(pagination.inline_keyboard)
|
||
|
||
keyboard.append(
|
||
[types.InlineKeyboardButton(text=texts.BACK, callback_data=f"promo_group_manage_{group_id}")]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
f"{title}\n\n{body}",
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||
parse_mode="HTML",
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def request_delete_promo_group(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
group = await _get_group_or_alert(callback, db)
|
||
if not group:
|
||
return
|
||
|
||
texts = get_texts(db_user.language)
|
||
|
||
if group.is_default:
|
||
await callback.answer(
|
||
texts.t("ADMIN_PROMO_GROUP_DELETE_FORBIDDEN", "Базовую промогруппу нельзя удалить."),
|
||
show_alert=True,
|
||
)
|
||
return
|
||
|
||
confirm_text = texts.t(
|
||
"ADMIN_PROMO_GROUP_DELETE_CONFIRM",
|
||
"Удалить промогруппу «{name}»? Все пользователи будут переведены в базовую группу.",
|
||
).format(name=group.name)
|
||
|
||
await callback.message.edit_text(
|
||
confirm_text,
|
||
reply_markup=get_confirmation_keyboard(
|
||
confirm_action=f"promo_group_delete_confirm_{group.id}",
|
||
cancel_action=f"promo_group_manage_{group.id}",
|
||
language=db_user.language,
|
||
),
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def delete_promo_group_confirmed(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
group = await _get_group_or_alert(callback, db)
|
||
if not group:
|
||
return
|
||
|
||
texts = get_texts(db_user.language)
|
||
|
||
success = await delete_promo_group(db, group)
|
||
if not success:
|
||
await callback.answer(
|
||
texts.t("ADMIN_PROMO_GROUP_DELETE_FORBIDDEN", "Базовую промогруппу нельзя удалить."),
|
||
show_alert=True,
|
||
)
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
texts.t("ADMIN_PROMO_GROUP_DELETED", "Промогруппа «{name}» удалена.").format(name=group.name),
|
||
reply_markup=types.InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_promo_groups")]
|
||
]
|
||
),
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_promo_group_addon_discounts(
|
||
callback: types.CallbackQuery,
|
||
db_user,
|
||
db: AsyncSession,
|
||
):
|
||
group = await _get_group_or_alert(callback, db)
|
||
if not group:
|
||
return
|
||
|
||
texts = get_texts(db_user.language)
|
||
|
||
new_value = not getattr(group, "apply_discounts_to_addons", True)
|
||
|
||
group = await update_promo_group(
|
||
db,
|
||
group,
|
||
apply_discounts_to_addons=new_value,
|
||
)
|
||
|
||
status_text = texts.t(
|
||
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED"
|
||
if new_value
|
||
else "ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED",
|
||
"🧩 Скидки на докупку доп. услуг {status}.",
|
||
).format(status="<b>включены</b>" if new_value else "<b>отключены</b>")
|
||
|
||
await _send_edit_menu_after_update(
|
||
callback.message,
|
||
texts,
|
||
group,
|
||
db_user.language,
|
||
status_text,
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
|
||
def register_handlers(dp: Dispatcher):
|
||
dp.callback_query.register(show_promo_groups_menu, F.data == "admin_promo_groups")
|
||
dp.callback_query.register(show_promo_group_details, F.data.startswith("promo_group_manage_"))
|
||
dp.callback_query.register(start_create_promo_group, F.data == "admin_promo_group_create")
|
||
dp.callback_query.register(
|
||
prompt_edit_promo_group_field,
|
||
F.data.startswith("promo_group_edit_field_"),
|
||
)
|
||
dp.callback_query.register(
|
||
toggle_promo_group_addon_discounts,
|
||
F.data.startswith("promo_group_toggle_addons_"),
|
||
)
|
||
dp.callback_query.register(
|
||
start_edit_promo_group,
|
||
F.data.regexp(r"^promo_group_edit_\d+$"),
|
||
)
|
||
dp.callback_query.register(
|
||
request_delete_promo_group,
|
||
F.data.startswith("promo_group_delete_")
|
||
& ~F.data.startswith("promo_group_delete_confirm_"),
|
||
)
|
||
dp.callback_query.register(
|
||
delete_promo_group_confirmed,
|
||
F.data.startswith("promo_group_delete_confirm_"),
|
||
)
|
||
dp.callback_query.register(
|
||
show_promo_group_members,
|
||
F.data.regexp(r"^promo_group_members_\d+_page_\d+$"),
|
||
)
|
||
|
||
dp.message.register(process_create_group_name, AdminStates.creating_promo_group_name)
|
||
dp.message.register(
|
||
process_create_group_priority,
|
||
AdminStates.creating_promo_group_priority,
|
||
)
|
||
dp.message.register(
|
||
process_create_group_traffic,
|
||
AdminStates.creating_promo_group_traffic_discount,
|
||
)
|
||
dp.message.register(
|
||
process_create_group_servers,
|
||
AdminStates.creating_promo_group_server_discount,
|
||
)
|
||
dp.message.register(
|
||
process_create_group_devices,
|
||
AdminStates.creating_promo_group_device_discount,
|
||
)
|
||
dp.message.register(
|
||
process_create_group_period_discounts,
|
||
AdminStates.creating_promo_group_period_discount,
|
||
)
|
||
dp.message.register(
|
||
process_create_group_auto_assign,
|
||
AdminStates.creating_promo_group_auto_assign,
|
||
)
|
||
|
||
dp.message.register(process_edit_group_name, AdminStates.editing_promo_group_name)
|
||
dp.message.register(
|
||
process_edit_group_priority,
|
||
AdminStates.editing_promo_group_priority,
|
||
)
|
||
dp.message.register(
|
||
process_edit_group_traffic,
|
||
AdminStates.editing_promo_group_traffic_discount,
|
||
)
|
||
dp.message.register(
|
||
process_edit_group_servers,
|
||
AdminStates.editing_promo_group_server_discount,
|
||
)
|
||
dp.message.register(
|
||
process_edit_group_devices,
|
||
AdminStates.editing_promo_group_device_discount,
|
||
)
|
||
dp.message.register(
|
||
process_edit_group_period_discounts,
|
||
AdminStates.editing_promo_group_period_discount,
|
||
)
|
||
dp.message.register(
|
||
process_edit_group_auto_assign,
|
||
AdminStates.editing_promo_group_auto_assign,
|
||
)
|