mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-13 15:40:30 +00:00
Revert "Add trial and tariff settings to admin pricing"
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import html
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from aiogram import Bot, Dispatcher, F, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -22,425 +20,10 @@ logger = logging.getLogger(__name__)
|
||||
PriceItem = Tuple[str, str, int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SectionItem:
|
||||
key: str
|
||||
label: str
|
||||
value: Any
|
||||
display: str
|
||||
short_display: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SectionSettingDefinition:
|
||||
key: str
|
||||
label_key: str
|
||||
label_default: str
|
||||
type: str
|
||||
summary_label_key: Optional[str] = None
|
||||
summary_label_default: Optional[str] = None
|
||||
prompt_key: Optional[str] = None
|
||||
prompt_default: Optional[str] = None
|
||||
include_in_summary: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CustomSectionConfig:
|
||||
title_key: str
|
||||
title_default: str
|
||||
button_key: str
|
||||
button_default: str
|
||||
summary_key: str
|
||||
summary_default: str
|
||||
items: Tuple[SectionSettingDefinition, ...]
|
||||
|
||||
|
||||
PRICE_KEY_PREFIXES: Tuple[str, ...] = ("PRICE_",)
|
||||
PRICE_KEY_EXTRAS: Tuple[str, ...] = ("BASE_SUBSCRIPTION_PRICE", "PRICE_PER_DEVICE")
|
||||
ALLOWED_PERIOD_VALUES: Tuple[int, ...] = (14, 30, 60, 90, 180, 360)
|
||||
|
||||
|
||||
CUSTOM_SECTIONS: Dict[str, CustomSectionConfig] = {
|
||||
"trial": CustomSectionConfig(
|
||||
title_key="ADMIN_PRICING_SECTION_TRIAL_TITLE",
|
||||
title_default="🎁 Пробный период",
|
||||
button_key="ADMIN_PRICING_BUTTON_TRIAL",
|
||||
button_default="🎁 Пробный период",
|
||||
summary_key="ADMIN_PRICING_MENU_SUMMARY_TRIAL",
|
||||
summary_default="• Пробный период: {summary}",
|
||||
items=(
|
||||
SectionSettingDefinition(
|
||||
key="TRIAL_DURATION_DAYS",
|
||||
label_key="ADMIN_PRICING_TRIAL_DURATION",
|
||||
label_default="Длительность пробного периода (дней)",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_TRIAL_SUMMARY_DURATION",
|
||||
summary_label_default="Дни",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="TRIAL_TRAFFIC_LIMIT_GB",
|
||||
label_key="ADMIN_PRICING_TRIAL_TRAFFIC",
|
||||
label_default="Лимит трафика триала (ГБ)",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_TRIAL_SUMMARY_TRAFFIC",
|
||||
summary_label_default="Трафик",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="TRIAL_DEVICE_LIMIT",
|
||||
label_key="ADMIN_PRICING_TRIAL_DEVICES",
|
||||
label_default="Количество устройств в триале",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_TRIAL_SUMMARY_DEVICES",
|
||||
summary_label_default="Устройства",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="TRIAL_ADD_REMAINING_DAYS_TO_PAID",
|
||||
label_key="ADMIN_PRICING_TRIAL_ADD_REMAINING",
|
||||
label_default="Добавлять остаток триала к платной подписке",
|
||||
type="bool",
|
||||
summary_label_key="ADMIN_PRICING_TRIAL_SUMMARY_ADD_REMAINING",
|
||||
summary_label_default="Перенос дней",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="TRIAL_SQUAD_UUID",
|
||||
label_key="ADMIN_PRICING_TRIAL_SQUAD",
|
||||
label_default="UUID сквада для пробного периода",
|
||||
type="text",
|
||||
include_in_summary=False,
|
||||
),
|
||||
),
|
||||
),
|
||||
"subscription": CustomSectionConfig(
|
||||
title_key="ADMIN_PRICING_SECTION_SUBSCRIPTION_TITLE",
|
||||
title_default="⚙️ Параметры подписки",
|
||||
button_key="ADMIN_PRICING_BUTTON_SUBSCRIPTION",
|
||||
button_default="⚙️ Параметры подписки",
|
||||
summary_key="ADMIN_PRICING_MENU_SUMMARY_SUBSCRIPTION",
|
||||
summary_default="• Параметры подписки: {summary}",
|
||||
items=(
|
||||
SectionSettingDefinition(
|
||||
key="BASE_SUBSCRIPTION_PRICE",
|
||||
label_key="ADMIN_PRICING_SUBSCRIPTION_BASE_PRICE",
|
||||
label_default="Базовая стоимость подписки",
|
||||
type="price",
|
||||
summary_label_key="ADMIN_PRICING_SUBSCRIPTION_SUMMARY_PRICE",
|
||||
summary_label_default="База",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="DEFAULT_DEVICE_LIMIT",
|
||||
label_key="ADMIN_PRICING_SUBSCRIPTION_DEFAULT_DEVICES",
|
||||
label_default="Устройств по умолчанию",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_SUBSCRIPTION_SUMMARY_DEFAULT_DEVICES",
|
||||
summary_label_default="Устройств",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="MAX_DEVICES_LIMIT",
|
||||
label_key="ADMIN_PRICING_SUBSCRIPTION_MAX_DEVICES",
|
||||
label_default="Максимум устройств",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_SUBSCRIPTION_SUMMARY_MAX_DEVICES",
|
||||
summary_label_default="Макс устройств",
|
||||
),
|
||||
),
|
||||
),
|
||||
"availability": CustomSectionConfig(
|
||||
title_key="ADMIN_PRICING_SECTION_AVAILABILITY_TITLE",
|
||||
title_default="📆 Выводимые периоды",
|
||||
button_key="ADMIN_PRICING_BUTTON_AVAILABILITY",
|
||||
button_default="📆 Выводимые периоды",
|
||||
summary_key="ADMIN_PRICING_MENU_SUMMARY_AVAILABILITY",
|
||||
summary_default="• Выводимые периоды: {summary}",
|
||||
items=(
|
||||
SectionSettingDefinition(
|
||||
key="AVAILABLE_SUBSCRIPTION_PERIODS",
|
||||
label_key="ADMIN_PRICING_AVAILABILITY_SUBSCRIPTIONS",
|
||||
label_default="Периоды подписки",
|
||||
type="periods",
|
||||
summary_label_key="ADMIN_PRICING_AVAILABILITY_SUMMARY_SUBSCRIPTIONS",
|
||||
summary_label_default="Подписка",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="AVAILABLE_RENEWAL_PERIODS",
|
||||
label_key="ADMIN_PRICING_AVAILABILITY_RENEWALS",
|
||||
label_default="Периоды продления",
|
||||
type="periods",
|
||||
summary_label_key="ADMIN_PRICING_AVAILABILITY_SUMMARY_RENEWALS",
|
||||
summary_label_default="Продление",
|
||||
),
|
||||
),
|
||||
),
|
||||
"traffic_settings": CustomSectionConfig(
|
||||
title_key="ADMIN_PRICING_SECTION_TRAFFIC_SETTINGS_TITLE",
|
||||
title_default="📊 Лимиты трафика",
|
||||
button_key="ADMIN_PRICING_BUTTON_TRAFFIC_SETTINGS",
|
||||
button_default="📊 Лимиты трафика",
|
||||
summary_key="ADMIN_PRICING_MENU_SUMMARY_TRAFFIC_SETTINGS",
|
||||
summary_default="• Лимиты трафика: {summary}",
|
||||
items=(
|
||||
SectionSettingDefinition(
|
||||
key="DEFAULT_TRAFFIC_LIMIT_GB",
|
||||
label_key="ADMIN_PRICING_TRAFFIC_DEFAULT_LIMIT",
|
||||
label_default="Лимит трафика по умолчанию (ГБ)",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_TRAFFIC_SUMMARY_DEFAULT",
|
||||
summary_label_default="По умолчанию",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="TRAFFIC_SELECTION_MODE",
|
||||
label_key="ADMIN_PRICING_TRAFFIC_SELECTION_MODE",
|
||||
label_default="Режим выбора трафика",
|
||||
type="choice",
|
||||
summary_label_key="ADMIN_PRICING_TRAFFIC_SUMMARY_MODE",
|
||||
summary_label_default="Режим",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="FIXED_TRAFFIC_LIMIT_GB",
|
||||
label_key="ADMIN_PRICING_TRAFFIC_FIXED_LIMIT",
|
||||
label_default="Фиксированный лимит (ГБ)",
|
||||
type="int",
|
||||
summary_label_key="ADMIN_PRICING_TRAFFIC_SUMMARY_FIXED",
|
||||
summary_label_default="Фикс",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="DEFAULT_TRAFFIC_RESET_STRATEGY",
|
||||
label_key="ADMIN_PRICING_TRAFFIC_RESET_STRATEGY",
|
||||
label_default="Стратегия сброса трафика",
|
||||
type="choice",
|
||||
summary_label_key="ADMIN_PRICING_TRAFFIC_SUMMARY_RESET",
|
||||
summary_label_default="Сброс",
|
||||
),
|
||||
SectionSettingDefinition(
|
||||
key="RESET_TRAFFIC_ON_PAYMENT",
|
||||
label_key="ADMIN_PRICING_TRAFFIC_RESET_ON_PAYMENT",
|
||||
label_default="Сбрасывать трафик при оплате",
|
||||
type="bool",
|
||||
summary_label_key="ADMIN_PRICING_TRAFFIC_SUMMARY_RESET_PAYMENT",
|
||||
summary_label_default="Сброс при оплате",
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
CUSTOM_SECTION_ORDER: Tuple[str, ...] = tuple(CUSTOM_SECTIONS.keys())
|
||||
|
||||
CUSTOM_DEFINITION_BY_KEY: Dict[str, SectionSettingDefinition] = {}
|
||||
for _section_key in CUSTOM_SECTION_ORDER:
|
||||
_section_config = CUSTOM_SECTIONS[_section_key]
|
||||
for _definition in _section_config.items:
|
||||
CUSTOM_DEFINITION_BY_KEY[_definition.key] = _definition
|
||||
|
||||
|
||||
def _language_code(language: str | None) -> str:
|
||||
return (language or "ru").split("-")[0].lower()
|
||||
|
||||
|
||||
def _is_price_key(key: str) -> bool:
|
||||
return key in PRICE_KEY_EXTRAS or any(key.startswith(prefix) for prefix in PRICE_KEY_PREFIXES)
|
||||
|
||||
|
||||
def _get_item_type(key: str) -> str:
|
||||
definition = CUSTOM_DEFINITION_BY_KEY.get(key)
|
||||
if definition:
|
||||
return definition.type
|
||||
if _is_price_key(key):
|
||||
return "price"
|
||||
return "text"
|
||||
|
||||
|
||||
def _format_setting_value(key: str, language: str, *, short: bool = False) -> str:
|
||||
lang_code = _language_code(language)
|
||||
value = getattr(settings, key, None)
|
||||
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
if isinstance(value, str):
|
||||
if not value.strip():
|
||||
return "—"
|
||||
if not short:
|
||||
return value
|
||||
return value if len(value) <= 20 else f"{value[:17]}…"
|
||||
|
||||
if _is_price_key(key) and isinstance(value, (int, float)):
|
||||
return settings.format_price(int(value))
|
||||
|
||||
if isinstance(value, bool):
|
||||
if short:
|
||||
if lang_code == "ru":
|
||||
return "✅ Вкл" if value else "❌ Выкл"
|
||||
return "✅ On" if value else "❌ Off"
|
||||
if lang_code == "ru":
|
||||
return "Включено" if value else "Выключено"
|
||||
return "Enabled" if value else "Disabled"
|
||||
|
||||
if short:
|
||||
formatted = bot_configuration_service.format_value_for_list(key)
|
||||
if formatted:
|
||||
return formatted
|
||||
|
||||
formatted_full = bot_configuration_service.format_value_human(key, value)
|
||||
return formatted_full if formatted_full else str(value)
|
||||
|
||||
|
||||
def _build_custom_section_items(section: str, language: str) -> List[SectionItem]:
|
||||
texts = get_texts(language)
|
||||
items: List[SectionItem] = []
|
||||
|
||||
config = CUSTOM_SECTIONS[section]
|
||||
for definition in config.items:
|
||||
label = texts.t(definition.label_key, definition.label_default)
|
||||
value = getattr(settings, definition.key, None)
|
||||
display = _format_setting_value(definition.key, language)
|
||||
short_display = _format_setting_value(definition.key, language, short=True)
|
||||
items.append(
|
||||
SectionItem(
|
||||
key=definition.key,
|
||||
label=label,
|
||||
value=value,
|
||||
display=display,
|
||||
short_display=short_display,
|
||||
)
|
||||
)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def _build_custom_summary(
|
||||
section: str,
|
||||
items: Iterable[SectionItem],
|
||||
language: str,
|
||||
fallback: str,
|
||||
) -> str:
|
||||
texts = get_texts(language)
|
||||
config = CUSTOM_SECTIONS[section]
|
||||
definitions: Dict[str, SectionSettingDefinition] = {
|
||||
definition.key: definition for definition in config.items
|
||||
}
|
||||
|
||||
parts: List[str] = []
|
||||
for item in items:
|
||||
definition = definitions.get(item.key)
|
||||
if not definition or not definition.include_in_summary:
|
||||
continue
|
||||
|
||||
label_default = definition.summary_label_default or definition.label_default
|
||||
if definition.summary_label_key:
|
||||
label = texts.t(definition.summary_label_key, label_default)
|
||||
else:
|
||||
label = label_default
|
||||
|
||||
short_value = item.short_display or "—"
|
||||
parts.append(f"{label}: {short_value}")
|
||||
|
||||
return ", ".join(parts) if parts else fallback
|
||||
|
||||
|
||||
def _build_instruction(
|
||||
definition: Optional[SectionSettingDefinition],
|
||||
item_type: str,
|
||||
key: str,
|
||||
language: str,
|
||||
) -> str:
|
||||
texts = get_texts(language)
|
||||
|
||||
if definition and definition.prompt_key:
|
||||
return texts.t(
|
||||
definition.prompt_key,
|
||||
definition.prompt_default
|
||||
or texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_GENERIC",
|
||||
"Введите новое значение. Для отмены отправьте «отмена».",
|
||||
),
|
||||
)
|
||||
|
||||
if item_type == "int":
|
||||
return texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_INT",
|
||||
"Введите целое число. Для отмены отправьте «отмена».",
|
||||
)
|
||||
|
||||
if item_type == "text":
|
||||
return texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_TEXT",
|
||||
"Введите новое значение. Чтобы очистить параметр, отправьте «пусто». Для отмены — «отмена».",
|
||||
)
|
||||
|
||||
if item_type == "choice":
|
||||
options = bot_configuration_service.get_choice_options(key)
|
||||
if options:
|
||||
readable = ", ".join(
|
||||
f"{option.label} ({option.value})" for option in options
|
||||
)
|
||||
return texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_CHOICE",
|
||||
"Введите одно из значений: {options}. Для отмены — «отмена».",
|
||||
).format(options=readable)
|
||||
return texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_GENERIC",
|
||||
"Введите новое значение. Для отмены отправьте «отмена».",
|
||||
)
|
||||
|
||||
if item_type == "periods":
|
||||
allowed = ", ".join(str(value) for value in ALLOWED_PERIOD_VALUES)
|
||||
return texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_PERIODS",
|
||||
"Введите значения через запятую из допустимого набора: {values}. Для отмены — «отмена».",
|
||||
).format(values=allowed)
|
||||
|
||||
return texts.t(
|
||||
"ADMIN_PRICING_SETTING_PROMPT_GENERIC",
|
||||
"Введите новое значение. Для отмены отправьте «отмена».",
|
||||
)
|
||||
|
||||
|
||||
def _parse_periods_input(raw_value: str, language: str) -> str:
|
||||
texts = get_texts(language)
|
||||
cleaned = (raw_value or "").replace(" ", "").replace("\n", "").strip()
|
||||
|
||||
if not cleaned:
|
||||
raise ValueError(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_SETTING_PERIODS_EMPTY",
|
||||
"Список периодов не может быть пустым.",
|
||||
)
|
||||
)
|
||||
|
||||
parts = [part for part in cleaned.split(",") if part]
|
||||
parsed: List[int] = []
|
||||
|
||||
for part in parts:
|
||||
try:
|
||||
value = int(part)
|
||||
except ValueError as error:
|
||||
raise ValueError(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_SETTING_PERIODS_INVALID_NUMBER",
|
||||
"Не удалось распознать значение «{value}». Укажите числа через запятую.",
|
||||
).format(value=part)
|
||||
) from error
|
||||
|
||||
if value not in ALLOWED_PERIOD_VALUES:
|
||||
allowed = ", ".join(str(item) for item in ALLOWED_PERIOD_VALUES)
|
||||
raise ValueError(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_SETTING_PERIODS_INVALID",
|
||||
"Недопустимое значение {value}. Доступны только: {allowed}.",
|
||||
).format(value=value, allowed=allowed)
|
||||
)
|
||||
|
||||
parsed.append(value)
|
||||
|
||||
if not parsed:
|
||||
raise ValueError(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_SETTING_PERIODS_EMPTY",
|
||||
"Список периодов не может быть пустым.",
|
||||
)
|
||||
)
|
||||
|
||||
unique_sorted = sorted(set(parsed))
|
||||
return ",".join(str(value) for value in unique_sorted)
|
||||
def _format_period_label(days: int, lang_code: str, short: bool = False) -> str:
|
||||
if short:
|
||||
suffix = "д" if lang_code == "ru" else "d"
|
||||
@@ -554,75 +137,39 @@ def _build_overview(language: str) -> Tuple[str, types.InlineKeyboardMarkup]:
|
||||
summary_traffic = _build_traffic_summary(traffic_items, lang_code, fallback)
|
||||
summary_extra = _build_extra_summary(extra_items, fallback)
|
||||
|
||||
custom_items: Dict[str, List[SectionItem]] = {}
|
||||
for section_key in CUSTOM_SECTION_ORDER:
|
||||
custom_items[section_key] = _build_custom_section_items(section_key, language)
|
||||
|
||||
summary_lines: List[str] = [
|
||||
texts.t("ADMIN_PRICING_MENU_SUMMARY_PERIODS", "• Периоды: {summary}").format(
|
||||
summary=summary_periods
|
||||
),
|
||||
texts.t("ADMIN_PRICING_MENU_SUMMARY_TRAFFIC", "• Трафик: {summary}").format(
|
||||
summary=summary_traffic
|
||||
),
|
||||
texts.t("ADMIN_PRICING_MENU_SUMMARY_EXTRA", "• Дополнительно: {summary}").format(
|
||||
summary=summary_extra
|
||||
),
|
||||
]
|
||||
|
||||
for section_key in CUSTOM_SECTION_ORDER:
|
||||
config = CUSTOM_SECTIONS[section_key]
|
||||
section_summary = _build_custom_summary(
|
||||
section_key, custom_items[section_key], language, fallback
|
||||
)
|
||||
summary_lines.append(
|
||||
texts.t(config.summary_key, config.summary_default).format(summary=section_summary)
|
||||
)
|
||||
|
||||
text = (
|
||||
f"💰 <b>{texts.t('ADMIN_PRICING_MENU_TITLE', 'Управление ценами')}</b>\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_MENU_DESCRIPTION', 'Быстрый доступ к тарифам и пакетам.')}\n\n"
|
||||
f"<b>{texts.t('ADMIN_PRICING_MENU_SUMMARY', 'Краткая сводка:')}</b>\n"
|
||||
+ "\n".join(summary_lines)
|
||||
+ "\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_MENU_SUMMARY_PERIODS', '• Периоды: {summary}').format(summary=summary_periods)}\n"
|
||||
f"{texts.t('ADMIN_PRICING_MENU_SUMMARY_TRAFFIC', '• Трафик: {summary}').format(summary=summary_traffic)}\n"
|
||||
f"{texts.t('ADMIN_PRICING_MENU_SUMMARY_EXTRA', '• Дополнительно: {summary}').format(summary=summary_extra)}\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_MENU_PROMPT', 'Выберите раздел для редактирования:')}"
|
||||
)
|
||||
|
||||
keyboard_rows: List[List[types.InlineKeyboardButton]] = [
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BUTTON_PERIODS", "🗓 Периоды подписки"),
|
||||
callback_data="admin_pricing_section:periods",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BUTTON_TRAFFIC", "📦 Пакеты трафика"),
|
||||
callback_data="admin_pricing_section:traffic",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BUTTON_EXTRA", "➕ Дополнительно"),
|
||||
callback_data="admin_pricing_section:extra",
|
||||
)
|
||||
],
|
||||
]
|
||||
|
||||
for section_key in CUSTOM_SECTION_ORDER:
|
||||
config = CUSTOM_SECTIONS[section_key]
|
||||
keyboard_rows.append(
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t(config.button_key, config.button_default),
|
||||
callback_data=f"admin_pricing_section:{section_key}",
|
||||
text=texts.t("ADMIN_PRICING_BUTTON_PERIODS", "🗓 Периоды подписки"),
|
||||
callback_data="admin_pricing_section:periods",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
keyboard_rows.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")])
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BUTTON_TRAFFIC", "📦 Пакеты трафика"),
|
||||
callback_data="admin_pricing_section:traffic",
|
||||
)
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_BUTTON_EXTRA", "➕ Дополнительно"),
|
||||
callback_data="admin_pricing_section:extra",
|
||||
)
|
||||
],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")],
|
||||
]
|
||||
)
|
||||
|
||||
return text, keyboard
|
||||
|
||||
@@ -635,69 +182,32 @@ def _build_section(
|
||||
lang_code = _language_code(language)
|
||||
|
||||
if section == "periods":
|
||||
price_items = _get_period_items(lang_code)
|
||||
section_items = [
|
||||
SectionItem(
|
||||
key=key,
|
||||
label=label,
|
||||
value=price,
|
||||
display=settings.format_price(price),
|
||||
short_display=settings.format_price(price),
|
||||
)
|
||||
for key, label, price in price_items
|
||||
]
|
||||
items = _get_period_items(lang_code)
|
||||
title = texts.t("ADMIN_PRICING_SECTION_PERIODS_TITLE", "🗓 Периоды подписки")
|
||||
elif section == "traffic":
|
||||
price_items = _get_traffic_items(lang_code)
|
||||
section_items = [
|
||||
SectionItem(
|
||||
key=key,
|
||||
label=label,
|
||||
value=price,
|
||||
display=settings.format_price(price),
|
||||
short_display=settings.format_price(price),
|
||||
)
|
||||
for key, label, price in price_items
|
||||
]
|
||||
items = _get_traffic_items(lang_code)
|
||||
title = texts.t("ADMIN_PRICING_SECTION_TRAFFIC_TITLE", "📦 Пакеты трафика")
|
||||
elif section == "extra":
|
||||
price_items = _get_extra_items(lang_code)
|
||||
section_items = [
|
||||
SectionItem(
|
||||
key=key,
|
||||
label=label,
|
||||
value=price,
|
||||
display=settings.format_price(price),
|
||||
short_display=settings.format_price(price),
|
||||
)
|
||||
for key, label, price in price_items
|
||||
]
|
||||
title = texts.t("ADMIN_PRICING_SECTION_EXTRA_TITLE", "➕ Дополнительные опции")
|
||||
elif section in CUSTOM_SECTIONS:
|
||||
section_items = _build_custom_section_items(section, language)
|
||||
config = CUSTOM_SECTIONS[section]
|
||||
title = texts.t(config.title_key, config.title_default)
|
||||
else:
|
||||
section_items = []
|
||||
items = _get_extra_items(lang_code)
|
||||
title = texts.t("ADMIN_PRICING_SECTION_EXTRA_TITLE", "➕ Дополнительные опции")
|
||||
|
||||
lines = [title, ""]
|
||||
|
||||
if section_items:
|
||||
for item in section_items:
|
||||
lines.append(f"• {item.label} — {item.display}")
|
||||
if items:
|
||||
for key, label, price in items:
|
||||
lines.append(f"• {label} — {settings.format_price(price)}")
|
||||
lines.append("")
|
||||
lines.append(texts.t("ADMIN_PRICING_SECTION_PROMPT", "Выберите что изменить:"))
|
||||
else:
|
||||
lines.append(texts.t("ADMIN_PRICING_SECTION_EMPTY", "Нет доступных значений."))
|
||||
|
||||
keyboard_rows: List[List[types.InlineKeyboardButton]] = []
|
||||
for item in section_items:
|
||||
for key, label, price in items:
|
||||
keyboard_rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{item.label} • {item.short_display}",
|
||||
callback_data=f"admin_pricing_edit:{section}:{item.key}",
|
||||
text=f"{label} • {settings.format_price(price)}",
|
||||
callback_data=f"admin_pricing_edit:{section}:{key}",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -761,11 +271,6 @@ def _parse_price_input(text: str) -> int:
|
||||
|
||||
|
||||
def _resolve_label(section: str, key: str, language: str) -> str:
|
||||
custom_definition = CUSTOM_DEFINITION_BY_KEY.get(key)
|
||||
if custom_definition:
|
||||
texts = get_texts(language)
|
||||
return texts.t(custom_definition.label_key, custom_definition.label_default)
|
||||
|
||||
lang_code = _language_code(language)
|
||||
|
||||
if section == "periods" and key.startswith("PRICE_") and key.endswith("_DAYS"):
|
||||
@@ -834,27 +339,6 @@ async def start_price_edit(
|
||||
texts = get_texts(db_user.language)
|
||||
label = _resolve_label(section, key, db_user.language)
|
||||
|
||||
item_type = _get_item_type(key)
|
||||
|
||||
if item_type == "bool":
|
||||
current_value = getattr(settings, key, False)
|
||||
new_value = not bool(current_value)
|
||||
await bot_configuration_service.set_value(db, key, new_value)
|
||||
await db.commit()
|
||||
await state.clear()
|
||||
|
||||
value_text = _format_setting_value(key, db_user.language)
|
||||
success_text = texts.t(
|
||||
"ADMIN_PRICING_SETTING_SUCCESS",
|
||||
"Параметр {item} обновлен: {value}",
|
||||
).format(item=label, value=value_text)
|
||||
await callback.message.answer(success_text)
|
||||
|
||||
section_text, section_keyboard = _build_section(section, db_user.language)
|
||||
await _render_message(callback.message, section_text, section_keyboard)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
await state.update_data(
|
||||
pricing_key=key,
|
||||
pricing_section=section,
|
||||
@@ -862,23 +346,12 @@ async def start_price_edit(
|
||||
)
|
||||
await state.set_state(PricingStates.waiting_for_value)
|
||||
|
||||
if item_type == "price":
|
||||
current_price = getattr(settings, key, 0)
|
||||
prompt = (
|
||||
f"💰 <b>{texts.t('ADMIN_PRICING_EDIT_TITLE', 'Изменение цены')}</b>\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_EDIT_TARGET', 'Текущий тариф')}: <b>{html.escape(label)}</b>\n"
|
||||
f"{texts.t('ADMIN_PRICING_EDIT_CURRENT', 'Текущее значение')}: <b>{settings.format_price(current_price)}</b>\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_EDIT_PROMPT', 'Введите новую стоимость в рублях (например 990 или 990.50). Для бесплатного тарифа укажите 0.')}"
|
||||
)
|
||||
else:
|
||||
current_value = _format_setting_value(key, db_user.language)
|
||||
instruction = _build_instruction(definition, item_type, key, db_user.language)
|
||||
prompt = (
|
||||
f"⚙️ <b>{texts.t('ADMIN_PRICING_SETTING_EDIT_TITLE', 'Изменение параметра')}</b>\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_SETTING_EDIT_TARGET', 'Параметр')}: <b>{html.escape(label)}</b>\n"
|
||||
f"{texts.t('ADMIN_PRICING_SETTING_EDIT_CURRENT', 'Текущее значение')}: <b>{html.escape(current_value)}</b>\n\n"
|
||||
f"{html.escape(instruction)}"
|
||||
)
|
||||
prompt = (
|
||||
f"💰 <b>{texts.t('ADMIN_PRICING_EDIT_TITLE', 'Изменение цены')}</b>\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_EDIT_TARGET', 'Текущий тариф')}: <b>{label}</b>\n"
|
||||
f"{texts.t('ADMIN_PRICING_EDIT_CURRENT', 'Текущее значение')}: <b>{settings.format_price(getattr(settings, key, 0))}</b>\n\n"
|
||||
f"{texts.t('ADMIN_PRICING_EDIT_PROMPT', 'Введите новую стоимость в рублях (например 990 или 990.50). Для бесплатного тарифа укажите 0.')}"
|
||||
)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
@@ -928,60 +401,27 @@ async def process_price_input(
|
||||
await message.answer(texts.t("ADMIN_PRICING_EDIT_CANCELLED", "Изменения отменены."))
|
||||
return
|
||||
|
||||
item_type = _get_item_type(key)
|
||||
try:
|
||||
price_kopeks = _parse_price_input(raw_value)
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_EDIT_INVALID",
|
||||
"Не удалось распознать цену. Укажите число в рублях (например 990 или 990.50).",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if item_type == "price":
|
||||
try:
|
||||
parsed_value: Any = _parse_price_input(raw_value)
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_EDIT_INVALID",
|
||||
"Не удалось распознать цену. Укажите число в рублях (например 990 или 990.50).",
|
||||
)
|
||||
)
|
||||
return
|
||||
elif item_type == "periods":
|
||||
try:
|
||||
parsed_value = _parse_periods_input(raw_value, db_user.language)
|
||||
except ValueError as error:
|
||||
reason = str(error).strip() or texts.t(
|
||||
"ADMIN_PRICING_SETTING_INVALID_GENERIC",
|
||||
"Не удалось обновить параметр. Проверьте ввод и попробуйте снова.",
|
||||
)
|
||||
await message.answer(
|
||||
texts.t("ADMIN_PRICING_SETTING_INVALID", "Ошибка: {reason}").format(reason=reason)
|
||||
)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
parsed_value = bot_configuration_service.parse_user_value(key, raw_value)
|
||||
except ValueError as error:
|
||||
reason = str(error).strip() or texts.t(
|
||||
"ADMIN_PRICING_SETTING_INVALID_GENERIC",
|
||||
"Не удалось обновить параметр. Проверьте ввод и попробуйте снова.",
|
||||
)
|
||||
await message.answer(
|
||||
texts.t("ADMIN_PRICING_SETTING_INVALID", "Ошибка: {reason}").format(reason=reason)
|
||||
)
|
||||
return
|
||||
|
||||
await bot_configuration_service.set_value(db, key, parsed_value)
|
||||
await bot_configuration_service.set_value(db, key, price_kopeks)
|
||||
await db.commit()
|
||||
|
||||
label = _resolve_label(section, key, db_user.language)
|
||||
value_text = _format_setting_value(key, db_user.language)
|
||||
success_template = (
|
||||
texts.t("ADMIN_PRICING_EDIT_SUCCESS", "Цена для {item} обновлена: {price}")
|
||||
if item_type == "price"
|
||||
else texts.t("ADMIN_PRICING_SETTING_SUCCESS", "Параметр {item} обновлен: {value}")
|
||||
await message.answer(
|
||||
texts.t("ADMIN_PRICING_EDIT_SUCCESS", "Цена для {item} обновлена: {price}").format(
|
||||
item=label,
|
||||
price=settings.format_price(price_kopeks),
|
||||
)
|
||||
)
|
||||
format_kwargs = {"item": label}
|
||||
if item_type == "price":
|
||||
format_kwargs["price"] = settings.format_price(parsed_value)
|
||||
else:
|
||||
format_kwargs["value"] = value_text
|
||||
await message.answer(success_template.format(**format_kwargs))
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user