mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Revert "Reorganize admin menu and add pricing management"
This commit is contained in:
@@ -30,7 +30,6 @@ from app.handlers.admin import (
|
||||
remnawave as admin_remnawave,
|
||||
statistics as admin_statistics,
|
||||
servers as admin_servers,
|
||||
pricing as admin_pricing,
|
||||
maintenance as admin_maintenance,
|
||||
promo_groups as admin_promo_groups,
|
||||
campaigns as admin_campaigns,
|
||||
@@ -127,8 +126,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_main.register_handlers(dp)
|
||||
admin_users.register_handlers(dp)
|
||||
admin_subscriptions.register_handlers(dp)
|
||||
admin_servers.register_handlers(dp)
|
||||
admin_pricing.register_handlers(dp)
|
||||
admin_servers.register_handlers(dp)
|
||||
admin_promocodes.register_handlers(dp)
|
||||
admin_messages.register_handlers(dp)
|
||||
admin_monitoring.register_handlers(dp)
|
||||
|
||||
@@ -142,28 +142,6 @@ for _group_key, _title, _category_keys in CATEGORY_GROUP_DEFINITIONS:
|
||||
CATEGORY_FALLBACK_KEY = "other"
|
||||
CATEGORY_FALLBACK_TITLE = "📦 Прочие настройки"
|
||||
|
||||
PRICING_SETTING_KEYS: set[str] = {
|
||||
"BASE_SUBSCRIPTION_PRICE",
|
||||
"PRICE_14_DAYS",
|
||||
"PRICE_30_DAYS",
|
||||
"PRICE_60_DAYS",
|
||||
"PRICE_90_DAYS",
|
||||
"PRICE_180_DAYS",
|
||||
"PRICE_360_DAYS",
|
||||
"PRICE_PER_DEVICE",
|
||||
"PRICE_TRAFFIC_5GB",
|
||||
"PRICE_TRAFFIC_10GB",
|
||||
"PRICE_TRAFFIC_25GB",
|
||||
"PRICE_TRAFFIC_50GB",
|
||||
"PRICE_TRAFFIC_100GB",
|
||||
"PRICE_TRAFFIC_250GB",
|
||||
"PRICE_TRAFFIC_500GB",
|
||||
"PRICE_TRAFFIC_1000GB",
|
||||
"PRICE_TRAFFIC_UNLIMITED",
|
||||
}
|
||||
|
||||
PRICING_CATEGORY_KEYS: set[str] = {"SUBSCRIPTION_PRICES", "TRAFFIC_PACKAGES"}
|
||||
|
||||
PRESET_CONFIGS: Dict[str, Dict[str, object]] = {
|
||||
"recommended": {
|
||||
"ENABLE_NOTIFICATIONS": True,
|
||||
@@ -224,49 +202,6 @@ def _get_group_description(group_key: str) -> str:
|
||||
return str(meta.get("description", ""))
|
||||
|
||||
|
||||
def _is_pricing_setting_key(key: str) -> bool:
|
||||
return key in PRICING_SETTING_KEYS
|
||||
|
||||
|
||||
def _filter_pricing_definitions(definitions):
|
||||
return [definition for definition in definitions if not _is_pricing_setting_key(definition.key)]
|
||||
|
||||
|
||||
def _get_visible_definitions(category_key: str):
|
||||
definitions = bot_configuration_service.get_settings_for_category(category_key)
|
||||
return _filter_pricing_definitions(definitions)
|
||||
|
||||
|
||||
def _is_pricing_category_key(category_key: str) -> bool:
|
||||
if category_key in PRICING_CATEGORY_KEYS:
|
||||
return True
|
||||
|
||||
definitions = bot_configuration_service.get_settings_for_category(category_key)
|
||||
if not definitions:
|
||||
return False
|
||||
|
||||
return not _filter_pricing_definitions(definitions)
|
||||
|
||||
|
||||
def _build_pricing_redirect_keyboard(texts) -> types.InlineKeyboardMarkup:
|
||||
return types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_GO_TO_SECTION", "Перейти к управлению ценами"),
|
||||
callback_data="admin_pricing",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BACK_TO_CONFIG", "⬅️ К разделам"),
|
||||
callback_data="admin_bot_config",
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _get_group_icon(group_key: str) -> str:
|
||||
meta = _get_group_meta(group_key)
|
||||
return str(meta.get("icon", "⚙️"))
|
||||
@@ -402,11 +337,9 @@ def _render_dashboard_overview() -> str:
|
||||
for group_key, _title, items in grouped:
|
||||
for category_key, _label, count in items:
|
||||
total_settings += count
|
||||
definitions = _get_visible_definitions(category_key)
|
||||
definitions = bot_configuration_service.get_settings_for_category(category_key)
|
||||
total_overrides += sum(
|
||||
1
|
||||
for definition in definitions
|
||||
if bot_configuration_service.has_override(definition.key)
|
||||
1 for definition in definitions if bot_configuration_service.has_override(definition.key)
|
||||
)
|
||||
|
||||
lines: List[str] = [
|
||||
@@ -458,13 +391,7 @@ def _perform_settings_search(query: str) -> List[Dict[str, object]]:
|
||||
else:
|
||||
category_page = 1
|
||||
|
||||
visible_definitions = [
|
||||
definition
|
||||
for definition in definitions
|
||||
if not _is_pricing_setting_key(definition.key)
|
||||
]
|
||||
|
||||
for definition_index, definition in enumerate(visible_definitions):
|
||||
for definition_index, definition in enumerate(definitions):
|
||||
fields = [definition.key.lower(), definition.display_name.lower()]
|
||||
guidance = bot_configuration_service.get_setting_guidance(definition.key)
|
||||
fields.extend(
|
||||
@@ -1127,34 +1054,25 @@ def _parse_group_payload(payload: str) -> Tuple[str, int]:
|
||||
|
||||
def _get_grouped_categories() -> List[Tuple[str, str, List[Tuple[str, str, int]]]]:
|
||||
categories = bot_configuration_service.get_categories()
|
||||
categories_map: Dict[str, Tuple[str, int]] = {}
|
||||
categories_map = {key: (label, count) for key, label, count in categories}
|
||||
used: set[str] = set()
|
||||
grouped: List[Tuple[str, str, List[Tuple[str, str, int]]]] = []
|
||||
|
||||
for key, label, _count in categories:
|
||||
if _is_pricing_category_key(key):
|
||||
continue
|
||||
visible = _get_visible_definitions(key)
|
||||
if not visible:
|
||||
continue
|
||||
categories_map[key] = (label, len(visible))
|
||||
|
||||
for group_key, title, category_keys in CATEGORY_GROUP_DEFINITIONS:
|
||||
items: List[Tuple[str, str, int]] = []
|
||||
for category_key in category_keys:
|
||||
if category_key not in categories_map:
|
||||
continue
|
||||
label, count = categories_map[category_key]
|
||||
items.append((category_key, label, count))
|
||||
used.add(category_key)
|
||||
if category_key in categories_map:
|
||||
label, count = categories_map[category_key]
|
||||
items.append((category_key, label, count))
|
||||
used.add(category_key)
|
||||
if items:
|
||||
grouped.append((group_key, title, items))
|
||||
|
||||
remaining: List[Tuple[str, str, int]] = []
|
||||
for key, (label, count) in categories_map.items():
|
||||
if key in used:
|
||||
continue
|
||||
remaining.append((key, label, count))
|
||||
remaining = [
|
||||
(key, label, count)
|
||||
for key, (label, count) in categories_map.items()
|
||||
if key not in used
|
||||
]
|
||||
|
||||
if remaining:
|
||||
remaining.sort(key=lambda item: item[1])
|
||||
@@ -1261,14 +1179,10 @@ def _build_categories_keyboard(
|
||||
|
||||
buttons: List[types.InlineKeyboardButton] = []
|
||||
for category_key, label, count in sliced:
|
||||
definitions = _get_visible_definitions(category_key)
|
||||
if not definitions:
|
||||
continue
|
||||
overrides = sum(
|
||||
1
|
||||
for definition in definitions
|
||||
if bot_configuration_service.has_override(definition.key)
|
||||
)
|
||||
overrides = 0
|
||||
for definition in bot_configuration_service.get_settings_for_category(category_key):
|
||||
if bot_configuration_service.has_override(definition.key):
|
||||
overrides += 1
|
||||
badge = "✳️" if overrides else "•"
|
||||
button_text = f"{badge} {label} ({count})"
|
||||
buttons.append(
|
||||
@@ -1324,26 +1238,7 @@ def _build_settings_keyboard(
|
||||
language: str,
|
||||
page: int = 1,
|
||||
) -> types.InlineKeyboardMarkup:
|
||||
definitions = _get_visible_definitions(category_key)
|
||||
if not definitions:
|
||||
texts = get_texts(language)
|
||||
return types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_GO_TO_SECTION", "Перейти к управлению ценами"),
|
||||
callback_data="admin_pricing",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BACK_TO_CONFIG", "⬅️ К разделам"),
|
||||
callback_data="admin_bot_config",
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
definitions = bot_configuration_service.get_settings_for_category(category_key)
|
||||
total_pages = max(1, math.ceil(len(definitions) / SETTINGS_PAGE_SIZE))
|
||||
page = max(1, min(page, total_pages))
|
||||
|
||||
@@ -1633,17 +1528,10 @@ async def show_bot_config_category(
|
||||
group_key, category_key, category_page, settings_page = _parse_category_payload(
|
||||
callback.data
|
||||
)
|
||||
definitions = _get_visible_definitions(category_key)
|
||||
definitions = bot_configuration_service.get_settings_for_category(category_key)
|
||||
|
||||
if not definitions:
|
||||
texts = get_texts(db_user.language)
|
||||
message = texts.t(
|
||||
"ADMIN_PRICING_MOVED_MESSAGE",
|
||||
"💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
)
|
||||
keyboard = _build_pricing_redirect_keyboard(texts)
|
||||
await callback.message.edit_text(message, reply_markup=keyboard, parse_mode="HTML")
|
||||
await callback.answer()
|
||||
await callback.answer("В этой категории пока нет настроек", show_alert=True)
|
||||
return
|
||||
|
||||
category_label = definitions[0].category_label
|
||||
@@ -2141,19 +2029,6 @@ async def show_bot_config_setting(
|
||||
except KeyError:
|
||||
await callback.answer("Эта настройка больше недоступна", show_alert=True)
|
||||
return
|
||||
if _is_pricing_setting_key(key):
|
||||
texts = get_texts(db_user.language)
|
||||
message = texts.t(
|
||||
"ADMIN_PRICING_MOVED_MESSAGE",
|
||||
"💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=_build_pricing_redirect_keyboard(texts),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
text = _render_setting_text(key)
|
||||
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
@@ -2191,19 +2066,6 @@ async def start_edit_setting(
|
||||
except KeyError:
|
||||
await callback.answer("Эта настройка больше недоступна", show_alert=True)
|
||||
return
|
||||
if _is_pricing_setting_key(key):
|
||||
texts = get_texts(db_user.language)
|
||||
message = texts.t(
|
||||
"ADMIN_PRICING_MOVED_MESSAGE",
|
||||
"💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=_build_pricing_redirect_keyboard(texts),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
definition = bot_configuration_service.get_definition(key)
|
||||
|
||||
summary = bot_configuration_service.get_setting_summary(key)
|
||||
@@ -2358,19 +2220,6 @@ async def reset_setting(
|
||||
except KeyError:
|
||||
await callback.answer("Эта настройка больше недоступна", show_alert=True)
|
||||
return
|
||||
if _is_pricing_setting_key(key):
|
||||
texts = get_texts(db_user.language)
|
||||
message = texts.t(
|
||||
"ADMIN_PRICING_MOVED_MESSAGE",
|
||||
"💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=_build_pricing_redirect_keyboard(texts),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
await bot_configuration_service.reset_value(db, key)
|
||||
await db.commit()
|
||||
|
||||
@@ -2411,19 +2260,6 @@ async def toggle_setting(
|
||||
except KeyError:
|
||||
await callback.answer("Эта настройка больше недоступна", show_alert=True)
|
||||
return
|
||||
if _is_pricing_setting_key(key):
|
||||
texts = get_texts(db_user.language)
|
||||
message = texts.t(
|
||||
"ADMIN_PRICING_MOVED_MESSAGE",
|
||||
"💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=_build_pricing_redirect_keyboard(texts),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
current = bot_configuration_service.get_current_value(key)
|
||||
new_value = not bool(current)
|
||||
await bot_configuration_service.set_value(db, key, new_value)
|
||||
@@ -2468,19 +2304,6 @@ async def apply_setting_choice(
|
||||
except KeyError:
|
||||
await callback.answer("Эта настройка больше недоступна", show_alert=True)
|
||||
return
|
||||
if _is_pricing_setting_key(key):
|
||||
texts = get_texts(db_user.language)
|
||||
message = texts.t(
|
||||
"ADMIN_PRICING_MOVED_MESSAGE",
|
||||
"💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=_build_pricing_redirect_keyboard(texts),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
try:
|
||||
value = bot_configuration_service.resolve_choice_token(key, choice_token)
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
import logging
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from aiogram import Dispatcher, F, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.keyboards.admin import get_admin_pricing_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.system_settings_service import bot_configuration_service
|
||||
from app.states import PricingStates
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SUBSCRIPTION_PRICE_ENTRIES: List[Tuple[str, str, str]] = [
|
||||
("base", "BASE_SUBSCRIPTION_PRICE", "Базовая цена"),
|
||||
("14", "PRICE_14_DAYS", "14 дней"),
|
||||
("30", "PRICE_30_DAYS", "30 дней"),
|
||||
("60", "PRICE_60_DAYS", "60 дней"),
|
||||
("90", "PRICE_90_DAYS", "90 дней"),
|
||||
("180", "PRICE_180_DAYS", "180 дней"),
|
||||
("360", "PRICE_360_DAYS", "360 дней"),
|
||||
]
|
||||
|
||||
TRAFFIC_PRICE_ENTRIES: List[Tuple[str, str, str]] = [
|
||||
("5", "PRICE_TRAFFIC_5GB", "5 ГБ"),
|
||||
("10", "PRICE_TRAFFIC_10GB", "10 ГБ"),
|
||||
("25", "PRICE_TRAFFIC_25GB", "25 ГБ"),
|
||||
("50", "PRICE_TRAFFIC_50GB", "50 ГБ"),
|
||||
("100", "PRICE_TRAFFIC_100GB", "100 ГБ"),
|
||||
("250", "PRICE_TRAFFIC_250GB", "250 ГБ"),
|
||||
("500", "PRICE_TRAFFIC_500GB", "500 ГБ"),
|
||||
("1000", "PRICE_TRAFFIC_1000GB", "1000 ГБ"),
|
||||
("unlimited", "PRICE_TRAFFIC_UNLIMITED", "Безлимит"),
|
||||
]
|
||||
|
||||
DEVICE_PRICE_ENTRY: Tuple[str, str, str] = (
|
||||
"devices",
|
||||
"PRICE_PER_DEVICE",
|
||||
"Дополнительное устройство",
|
||||
)
|
||||
|
||||
MAX_PRICE_RUBLES = 1_000_000
|
||||
|
||||
|
||||
def _format_price(value: int) -> str:
|
||||
return settings.format_price(int(value))
|
||||
|
||||
|
||||
def _build_price_buttons(
|
||||
entries: Iterable[Tuple[str, str, str]],
|
||||
prefix: str,
|
||||
) -> List[List[types.InlineKeyboardButton]]:
|
||||
buttons: List[List[types.InlineKeyboardButton]] = []
|
||||
row: List[types.InlineKeyboardButton] = []
|
||||
|
||||
for token, key, label in entries:
|
||||
current_value = bot_configuration_service.get_current_value(key)
|
||||
button = types.InlineKeyboardButton(
|
||||
text=f"{label} — {_format_price(current_value)}",
|
||||
callback_data=f"admin_pricing_edit_{prefix}_{token}",
|
||||
)
|
||||
row.append(button)
|
||||
if len(row) == 2:
|
||||
buttons.append(row)
|
||||
row = []
|
||||
|
||||
if row:
|
||||
buttons.append(row)
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
async def _prompt_price_input(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
key: str,
|
||||
label: str,
|
||||
return_callback: str,
|
||||
language: str,
|
||||
) -> None:
|
||||
texts = get_texts(language)
|
||||
current_value = bot_configuration_service.get_current_value(key)
|
||||
current_price = _format_price(current_value)
|
||||
|
||||
await state.set_state(PricingStates.waiting_for_value)
|
||||
await state.update_data(
|
||||
target_key=key,
|
||||
target_label=label,
|
||||
return_callback=return_callback,
|
||||
)
|
||||
|
||||
prompt_lines = [
|
||||
texts.t("ADMIN_PRICING_EDIT_TITLE", "💰 <b>Изменение цены</b>"),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_CURRENT_PRICE", "Текущая цена: {price}").format(
|
||||
price=current_price
|
||||
),
|
||||
texts.t("ADMIN_PRICING_PROMPT", "Отправьте новую цену для {name}:").format(
|
||||
name=label
|
||||
),
|
||||
texts.t(
|
||||
"ADMIN_PRICING_ENTER_PRICE",
|
||||
"Введите новую цену в рублях (можно использовать копейки через точку).",
|
||||
),
|
||||
texts.t("ADMIN_PRICING_CANCEL_HINT", "Для отмены воспользуйтесь кнопкой ниже."),
|
||||
]
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_CANCEL", "❌ Отмена"),
|
||||
callback_data=return_callback,
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"\n".join(prompt_lines),
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_pricing_menu(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
await state.clear()
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
summary_lines = [
|
||||
texts.t("ADMIN_PRICING_TITLE", "💰 <b>Управление ценами</b>"),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_DESCRIPTION",
|
||||
"Выберите раздел для редактирования стоимости подписок, трафика и устройств.",
|
||||
),
|
||||
"",
|
||||
]
|
||||
|
||||
summary_lines.append(texts.t("ADMIN_PRICING_OVERVIEW", "Ключевые значения:"))
|
||||
summary_lines.append(
|
||||
f"• 30 дней: {_format_price(settings.PRICE_30_DAYS)}"
|
||||
)
|
||||
summary_lines.append(
|
||||
f"• 90 дней: {_format_price(settings.PRICE_90_DAYS)}"
|
||||
)
|
||||
summary_lines.append(
|
||||
f"• {_format_price(settings.PRICE_PER_DEVICE)} — {texts.t('ADMIN_PRICING_DEVICE_SHORT', 'дополнительное устройство')}"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"\n".join(summary_lines),
|
||||
reply_markup=get_admin_pricing_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_subscription_prices(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
await state.clear()
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
lines = [
|
||||
texts.t("ADMIN_PRICING_SUBSCRIPTIONS_TITLE", "📅 <b>Стоимость подписок</b>"),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_SUBSCRIPTIONS_HELP",
|
||||
"Нажмите на период, чтобы изменить цену. Значения указаны в месяцах.",
|
||||
),
|
||||
"",
|
||||
]
|
||||
|
||||
for _token, key, label in SUBSCRIPTION_PRICE_ENTRIES:
|
||||
lines.append(f"• {label}: {_format_price(bot_configuration_service.get_current_value(key))}")
|
||||
|
||||
keyboard_rows = _build_price_buttons(SUBSCRIPTION_PRICE_ENTRIES, "subscription")
|
||||
keyboard_rows.append(
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"\n".join(lines),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_traffic_prices(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
await state.clear()
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
lines = [
|
||||
texts.t("ADMIN_PRICING_TRAFFIC_TITLE", "📦 <b>Стоимость пакетов трафика</b>"),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_TRAFFIC_HELP",
|
||||
"Нажмите на пакет, чтобы обновить цену. Цена применяется за выбранный объём трафика.",
|
||||
),
|
||||
"",
|
||||
]
|
||||
|
||||
for _token, key, label in TRAFFIC_PRICE_ENTRIES:
|
||||
lines.append(f"• {label}: {_format_price(bot_configuration_service.get_current_value(key))}")
|
||||
|
||||
keyboard_rows = _build_price_buttons(TRAFFIC_PRICE_ENTRIES, "traffic")
|
||||
keyboard_rows.append(
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"\n".join(lines),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_device_price(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
await state.clear()
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
_token, key, label = DEVICE_PRICE_ENTRY
|
||||
current_value = bot_configuration_service.get_current_value(key)
|
||||
|
||||
lines = [
|
||||
texts.t("ADMIN_PRICING_DEVICES_TITLE", "📱 <b>Стоимость дополнительных устройств</b>"),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_DEVICES_HELP",
|
||||
"Цена применяется за каждое устройство сверх базового лимита подписки.",
|
||||
),
|
||||
"",
|
||||
f"{label}: {_format_price(current_value)}",
|
||||
]
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_EDIT_BUTTON", "✏️ Изменить цену"),
|
||||
callback_data="admin_pricing_edit_devices",
|
||||
)
|
||||
],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")],
|
||||
]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"\n".join(lines),
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_subscription_price_edit(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
token = callback.data.split("_")[-1]
|
||||
for entry_token, key, label in SUBSCRIPTION_PRICE_ENTRIES:
|
||||
if entry_token == token:
|
||||
await _prompt_price_input(
|
||||
callback,
|
||||
state,
|
||||
key,
|
||||
label,
|
||||
"admin_pricing_subscriptions",
|
||||
db_user.language,
|
||||
)
|
||||
return
|
||||
|
||||
await callback.answer("❌ Значение недоступно", show_alert=True)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_traffic_price_edit(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
token = callback.data.split("_")[-1]
|
||||
for entry_token, key, label in TRAFFIC_PRICE_ENTRIES:
|
||||
if entry_token == token:
|
||||
await _prompt_price_input(
|
||||
callback,
|
||||
state,
|
||||
key,
|
||||
label,
|
||||
"admin_pricing_traffic",
|
||||
db_user.language,
|
||||
)
|
||||
return
|
||||
|
||||
await callback.answer("❌ Значение недоступно", show_alert=True)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_device_price_edit(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
_token, key, label = DEVICE_PRICE_ENTRY
|
||||
await _prompt_price_input(
|
||||
callback,
|
||||
state,
|
||||
key,
|
||||
label,
|
||||
"admin_pricing_devices",
|
||||
db_user.language,
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_price_input(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
data = await state.get_data()
|
||||
key = data.get("target_key")
|
||||
label = data.get("target_label", "")
|
||||
return_callback = data.get("return_callback", "admin_pricing")
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not key:
|
||||
await message.answer("❌ Не удалось определить настройку цены. Попробуйте снова из меню цен.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
raw_text = (message.text or "").strip()
|
||||
if raw_text.lower() in {"cancel", "отмена"}:
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
texts.t("ADMIN_PRICING_CANCELLED", "Отменено."),
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.BACK,
|
||||
callback_data=return_callback,
|
||||
)
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
price_rubles = float(raw_text.replace(",", "."))
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_INVALID_PRICE",
|
||||
"❌ Неверный формат цены. Используйте число, например 199.90",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if price_rubles < 0:
|
||||
await message.answer(
|
||||
texts.t("ADMIN_PRICING_INVALID_PRICE", "❌ Неверный формат цены. Используйте число, например 199.90"),
|
||||
)
|
||||
return
|
||||
|
||||
if price_rubles > MAX_PRICE_RUBLES:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_TOO_HIGH",
|
||||
"❌ Слишком высокая цена. Укажите значение до 1 000 000 ₽.",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
price_kopeks = int(round(price_rubles * 100))
|
||||
|
||||
await bot_configuration_service.set_value(db, key, price_kopeks)
|
||||
await state.clear()
|
||||
|
||||
logger.info("✅ Обновлена цена %s: %s ₽", key, price_rubles)
|
||||
|
||||
confirmation = texts.t(
|
||||
"ADMIN_PRICING_PRICE_UPDATED",
|
||||
"✅ Цена обновлена: {name} — {price}",
|
||||
).format(name=label or key, price=_format_price(price_kopeks))
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BACK_TO_SECTION", "⬅️ Назад к разделу"),
|
||||
callback_data=return_callback,
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
await message.answer(confirmation, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher) -> None:
|
||||
dp.callback_query.register(show_pricing_menu, F.data == "admin_pricing")
|
||||
dp.callback_query.register(
|
||||
show_subscription_prices, F.data == "admin_pricing_subscriptions"
|
||||
)
|
||||
dp.callback_query.register(
|
||||
show_traffic_prices, F.data == "admin_pricing_traffic"
|
||||
)
|
||||
dp.callback_query.register(show_device_price, F.data == "admin_pricing_devices")
|
||||
dp.callback_query.register(
|
||||
start_subscription_price_edit,
|
||||
F.data.startswith("admin_pricing_edit_subscription_"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
start_traffic_price_edit, F.data.startswith("admin_pricing_edit_traffic_"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
start_device_price_edit, F.data == "admin_pricing_edit_devices"
|
||||
)
|
||||
dp.message.register(
|
||||
process_price_input,
|
||||
PricingStates.waiting_for_value,
|
||||
)
|
||||
|
||||
@@ -14,8 +14,6 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_USERS_SUBSCRIPTIONS", "👥 Юзеры/Подписки"), callback_data="admin_submenu_users")],
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SERVERS", "🌐 Сервера"), callback_data="admin_servers")],
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_PRICING", "💰 Цены"), callback_data="admin_pricing")],
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_PROMO_STATS", "💰 Промокоды/Статистика"), callback_data="admin_submenu_promo")],
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SUPPORT", "🛟 Поддержка"), callback_data="admin_submenu_support")],
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_MESSAGES", "📨 Сообщения"), callback_data="admin_submenu_communications")],
|
||||
@@ -173,32 +171,6 @@ def get_admin_system_submenu_keyboard(language: str = "ru") -> InlineKeyboardMar
|
||||
])
|
||||
|
||||
|
||||
def get_admin_pricing_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_PRICING_SUBSCRIPTIONS_BUTTON", "📅 Подписки"),
|
||||
callback_data="admin_pricing_subscriptions",
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_PRICING_TRAFFIC_BUTTON", "📦 Трафик"),
|
||||
callback_data="admin_pricing_traffic",
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=_t(texts, "ADMIN_PRICING_DEVICES_BUTTON", "📱 Устройства"),
|
||||
callback_data="admin_pricing_devices",
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")],
|
||||
])
|
||||
|
||||
|
||||
def get_admin_reports_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
"ADMIN_MESSAGES": "📨 Рассылки",
|
||||
"ADMIN_MONITORING": "🔍 Мониторинг",
|
||||
"ADMIN_PANEL": "\n⚙️ <b>Административная панель</b>\n\nВыберите раздел для управления:\n",
|
||||
"ADMIN_MAIN_SERVERS": "🌐 Сервера",
|
||||
"ADMIN_MAIN_PRICING": "💰 Цены",
|
||||
"ADMIN_PROMOCODES": "🎫 Промокоды",
|
||||
"ADMIN_REFERRALS": "🤝 Партнерка",
|
||||
"ADMIN_REMNAWAVE": "🖥️ Remnawave",
|
||||
@@ -40,34 +38,6 @@
|
||||
"ADMIN_PROMO_GROUP_TOGGLE_ADDON_DISCOUNT_DISABLE": "🧩 Отключить скидки на доп. услуги",
|
||||
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_ENABLED": "🧩 Скидки на докупку доп. услуг <b>включены</b>.",
|
||||
"ADMIN_PROMO_GROUP_ADDON_DISCOUNT_UPDATED_DISABLED": "🧩 Скидки на докупку доп. услуг <b>отключены</b>.",
|
||||
"ADMIN_PRICING_SUBSCRIPTIONS_BUTTON": "📅 Подписки",
|
||||
"ADMIN_PRICING_TRAFFIC_BUTTON": "📦 Трафик",
|
||||
"ADMIN_PRICING_DEVICES_BUTTON": "📱 Устройства",
|
||||
"ADMIN_PRICING_TITLE": "💰 <b>Управление ценами</b>",
|
||||
"ADMIN_PRICING_DESCRIPTION": "Выберите раздел для редактирования стоимости подписок, трафика и устройств.",
|
||||
"ADMIN_PRICING_OVERVIEW": "Ключевые значения:",
|
||||
"ADMIN_PRICING_DEVICE_SHORT": "дополнительное устройство",
|
||||
"ADMIN_PRICING_SUBSCRIPTIONS_TITLE": "📅 <b>Стоимость подписок</b>",
|
||||
"ADMIN_PRICING_SUBSCRIPTIONS_HELP": "Нажмите на период, чтобы изменить цену. Значения указаны в месяцах.",
|
||||
"ADMIN_PRICING_TRAFFIC_TITLE": "📦 <b>Стоимость пакетов трафика</b>",
|
||||
"ADMIN_PRICING_TRAFFIC_HELP": "Нажмите на пакет, чтобы обновить цену. Цена применяется за выбранный объём трафика.",
|
||||
"ADMIN_PRICING_DEVICES_TITLE": "📱 <b>Стоимость дополнительных устройств</b>",
|
||||
"ADMIN_PRICING_DEVICES_HELP": "Цена применяется за каждое устройство сверх базового лимита подписки.",
|
||||
"ADMIN_PRICING_EDIT_BUTTON": "✏️ Изменить цену",
|
||||
"ADMIN_PRICING_EDIT_TITLE": "💰 <b>Изменение цены</b>",
|
||||
"ADMIN_PRICING_CURRENT_PRICE": "Текущая цена: {price}",
|
||||
"ADMIN_PRICING_PROMPT": "Отправьте новую цену для {name}:",
|
||||
"ADMIN_PRICING_ENTER_PRICE": "Введите новую цену в рублях (можно использовать копейки через точку).",
|
||||
"ADMIN_PRICING_CANCEL_HINT": "Для отмены воспользуйтесь кнопкой ниже.",
|
||||
"ADMIN_PRICING_CANCEL": "❌ Отмена",
|
||||
"ADMIN_PRICING_CANCELLED": "Отменено.",
|
||||
"ADMIN_PRICING_INVALID_PRICE": "❌ Неверный формат цены. Используйте число, например 199.90",
|
||||
"ADMIN_PRICING_TOO_HIGH": "❌ Слишком высокая цена. Укажите значение до 1 000 000 ₽.",
|
||||
"ADMIN_PRICING_PRICE_UPDATED": "✅ Цена обновлена: {name} — {price}",
|
||||
"ADMIN_PRICING_BACK_TO_SECTION": "⬅️ Назад к разделу",
|
||||
"ADMIN_PRICING_GO_TO_SECTION": "Перейти к управлению ценами",
|
||||
"ADMIN_PRICING_BACK_TO_CONFIG": "⬅️ К разделам",
|
||||
"ADMIN_PRICING_MOVED_MESSAGE": "💡 Управление ценами переехало в раздел «Цены» на главной странице админ-панели.",
|
||||
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)",
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}",
|
||||
"ADMIN_PROMO_GROUPS_EMPTY": "Промогруппы не найдены.",
|
||||
|
||||
@@ -135,10 +135,6 @@ class BotConfigStates(StatesGroup):
|
||||
waiting_for_search_query = State()
|
||||
waiting_for_import_file = State()
|
||||
|
||||
|
||||
class PricingStates(StatesGroup):
|
||||
waiting_for_value = State()
|
||||
|
||||
class AutoPayStates(StatesGroup):
|
||||
setting_autopay_days = State()
|
||||
confirming_autopay_toggle = State()
|
||||
|
||||
Reference in New Issue
Block a user