diff --git a/app/bot.py b/app/bot.py
index 36532569..ebae217e 100644
--- a/app/bot.py
+++ b/app/bot.py
@@ -41,6 +41,7 @@ from app.handlers.admin import (
tickets as admin_tickets,
reports as admin_reports,
bot_configuration as admin_bot_configuration,
+ pricing as admin_pricing,
)
from app.handlers.stars_payments import register_stars_handlers
@@ -145,6 +146,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
admin_tickets.register_handlers(dp)
admin_reports.register_handlers(dp)
admin_bot_configuration.register_handlers(dp)
+ admin_pricing.register_handlers(dp)
common.register_handlers(dp)
register_stars_handlers(dp)
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
diff --git a/app/handlers/admin/pricing.py b/app/handlers/admin/pricing.py
new file mode 100644
index 00000000..dbccaa83
--- /dev/null
+++ b/app/handlers/admin/pricing.py
@@ -0,0 +1,455 @@
+import logging
+from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
+from typing import Iterable, List, Tuple
+
+from aiogram import Bot, Dispatcher, F, types
+from aiogram.fsm.context import FSMContext
+from aiogram.exceptions import TelegramBadRequest
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.config import settings
+from app.database.models import User
+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__)
+
+
+PriceItem = Tuple[str, str, int]
+
+
+def _language_code(language: str | None) -> str:
+ return (language or "ru").split("-")[0].lower()
+
+
+def _format_period_label(days: int, lang_code: str, short: bool = False) -> str:
+ if short:
+ suffix = "д" if lang_code == "ru" else "d"
+ return f"{days}{suffix}"
+ if lang_code == "ru":
+ return f"{days} дней"
+ if days == 1:
+ return "1 day"
+ return f"{days}-day plan"
+
+
+def _format_traffic_label(gb: int, lang_code: str, short: bool = False) -> str:
+ if gb == 0:
+ return "∞" if short else ("Безлимит" if lang_code == "ru" else "Unlimited")
+ unit = "ГБ" if lang_code == "ru" else "GB"
+ if short:
+ return f"{gb}{unit}" if lang_code == "ru" else f"{gb}{unit}"
+ return f"{gb} {unit}"
+
+
+def _get_period_items(lang_code: str) -> List[PriceItem]:
+ items: List[PriceItem] = []
+ for days in settings.get_available_subscription_periods():
+ key = f"PRICE_{days}_DAYS"
+ if hasattr(settings, key):
+ price = getattr(settings, key)
+ items.append((key, _format_period_label(days, lang_code), price))
+ return items
+
+
+def _get_traffic_items(lang_code: str) -> List[PriceItem]:
+ traffic_keys: Tuple[Tuple[int, str], ...] = (
+ (5, "PRICE_TRAFFIC_5GB"),
+ (10, "PRICE_TRAFFIC_10GB"),
+ (25, "PRICE_TRAFFIC_25GB"),
+ (50, "PRICE_TRAFFIC_50GB"),
+ (100, "PRICE_TRAFFIC_100GB"),
+ (250, "PRICE_TRAFFIC_250GB"),
+ (500, "PRICE_TRAFFIC_500GB"),
+ (1000, "PRICE_TRAFFIC_1000GB"),
+ (0, "PRICE_TRAFFIC_UNLIMITED"),
+ )
+
+ items: List[PriceItem] = []
+ for gb, key in traffic_keys:
+ if hasattr(settings, key):
+ price = getattr(settings, key)
+ items.append((key, _format_traffic_label(gb, lang_code), price))
+ return items
+
+
+def _get_extra_items(lang_code: str) -> List[PriceItem]:
+ items: List[PriceItem] = []
+
+ if hasattr(settings, "PRICE_PER_DEVICE"):
+ label = "Дополнительное устройство" if lang_code == "ru" else "Extra device"
+ items.append(("PRICE_PER_DEVICE", label, settings.PRICE_PER_DEVICE))
+
+ return items
+
+
+def _build_period_summary(items: Iterable[PriceItem], lang_code: str, fallback: str) -> str:
+ parts: List[str] = []
+ for key, label, price in items:
+ try:
+ days = int(key.replace("PRICE_", "").replace("_DAYS", ""))
+ except ValueError:
+ days = None
+
+ if days is not None:
+ suffix = "д" if lang_code == "ru" else "d"
+ short_label = f"{days}{suffix}"
+ else:
+ short_label = label
+
+ parts.append(f"{short_label}: {settings.format_price(price)}")
+
+ return ", ".join(parts) if parts else fallback
+
+
+def _build_traffic_summary(items: Iterable[PriceItem], lang_code: str, fallback: str) -> str:
+ parts: List[str] = []
+ for key, label, price in items:
+ if key.endswith("UNLIMITED"):
+ short_label = "∞"
+ else:
+ digits = ''.join(ch for ch in key if ch.isdigit())
+ unit = "ГБ" if lang_code == "ru" else "GB"
+ short_label = f"{digits}{unit}" if digits else label
+
+ parts.append(f"{short_label}: {settings.format_price(price)}")
+
+ return ", ".join(parts) if parts else fallback
+
+
+def _build_extra_summary(items: Iterable[PriceItem], fallback: str) -> str:
+ parts = [f"{label}: {settings.format_price(price)}" for key, label, price in items]
+ return ", ".join(parts) if parts else fallback
+
+
+def _build_overview(language: str) -> Tuple[str, types.InlineKeyboardMarkup]:
+ texts = get_texts(language)
+ lang_code = _language_code(language)
+
+ period_items = _get_period_items(lang_code)
+ traffic_items = _get_traffic_items(lang_code)
+ extra_items = _get_extra_items(lang_code)
+
+ fallback = texts.t("ADMIN_PRICING_SUMMARY_EMPTY", "—")
+ summary_periods = _build_period_summary(period_items, lang_code, fallback)
+ summary_traffic = _build_traffic_summary(traffic_items, lang_code, fallback)
+ summary_extra = _build_extra_summary(extra_items, fallback)
+
+ text = (
+ f"💰 {texts.t('ADMIN_PRICING_MENU_TITLE', 'Управление ценами')}\n\n"
+ f"{texts.t('ADMIN_PRICING_MENU_DESCRIPTION', 'Быстрый доступ к тарифам и пакетам.')}\n\n"
+ f"{texts.t('ADMIN_PRICING_MENU_SUMMARY', 'Краткая сводка:')}\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 = types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ 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",
+ )
+ ],
+ [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")],
+ ]
+ )
+
+ return text, keyboard
+
+
+def _build_section(
+ section: str,
+ language: str,
+) -> Tuple[str, types.InlineKeyboardMarkup]:
+ texts = get_texts(language)
+ lang_code = _language_code(language)
+
+ if section == "periods":
+ items = _get_period_items(lang_code)
+ title = texts.t("ADMIN_PRICING_SECTION_PERIODS_TITLE", "🗓 Периоды подписки")
+ elif section == "traffic":
+ items = _get_traffic_items(lang_code)
+ title = texts.t("ADMIN_PRICING_SECTION_TRAFFIC_TITLE", "📦 Пакеты трафика")
+ else:
+ items = _get_extra_items(lang_code)
+ title = texts.t("ADMIN_PRICING_SECTION_EXTRA_TITLE", "➕ Дополнительные опции")
+
+ lines = [title, ""]
+
+ 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 key, label, price in items:
+ keyboard_rows.append(
+ [
+ types.InlineKeyboardButton(
+ text=f"{label} • {settings.format_price(price)}",
+ callback_data=f"admin_pricing_edit:{section}:{key}",
+ )
+ ]
+ )
+
+ keyboard_rows.append(
+ [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")]
+ )
+
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
+ return "\n".join(lines), keyboard
+
+
+async def _render_message(
+ message: types.Message,
+ text: str,
+ keyboard: types.InlineKeyboardMarkup,
+) -> None:
+ try:
+ await message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
+ except TelegramBadRequest as error: # message changed elsewhere
+ logger.debug("Failed to edit pricing message: %s", error)
+ await message.answer(text, reply_markup=keyboard, parse_mode="HTML")
+
+
+async def _render_message_by_id(
+ bot: Bot,
+ chat_id: int,
+ message_id: int,
+ text: str,
+ keyboard: types.InlineKeyboardMarkup,
+) -> None:
+ try:
+ await bot.edit_message_text(
+ text=text,
+ chat_id=chat_id,
+ message_id=message_id,
+ reply_markup=keyboard,
+ parse_mode="HTML",
+ )
+ except TelegramBadRequest as error:
+ logger.debug("Failed to edit pricing message by id: %s", error)
+ await bot.send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML")
+
+
+def _parse_price_input(text: str) -> int:
+ normalized = text.replace("₽", "").replace("р", "").replace("RUB", "")
+ normalized = normalized.replace(" ", "").replace(",", ".").strip()
+ if not normalized:
+ raise ValueError("empty")
+
+ try:
+ value = Decimal(normalized)
+ except InvalidOperation as error:
+ raise ValueError("invalid") from error
+
+ if value < 0:
+ raise ValueError("negative")
+
+ kopeks = int((value * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
+ return kopeks
+
+
+def _resolve_label(section: str, key: str, language: str) -> str:
+ lang_code = _language_code(language)
+
+ if section == "periods" and key.startswith("PRICE_") and key.endswith("_DAYS"):
+ try:
+ days = int(key.replace("PRICE_", "").replace("_DAYS", ""))
+ except ValueError:
+ days = None
+ if days is not None:
+ return _format_period_label(days, lang_code)
+
+ if section == "traffic" and key.startswith("PRICE_TRAFFIC_"):
+ if key.endswith("UNLIMITED"):
+ return _format_traffic_label(0, lang_code)
+ digits = ''.join(ch for ch in key if ch.isdigit())
+ try:
+ gb = int(digits)
+ except ValueError:
+ gb = None
+ if gb is not None:
+ return _format_traffic_label(gb, lang_code)
+
+ if key == "PRICE_PER_DEVICE":
+ return "Дополнительное устройство" if lang_code == "ru" else "Extra device"
+
+ return key
+
+
+@admin_required
+@error_handler
+async def show_pricing_menu(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+) -> None:
+ text, keyboard = _build_overview(db_user.language)
+ await _render_message(callback.message, text, keyboard)
+ await state.clear()
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def show_pricing_section(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+) -> None:
+ section = callback.data.split(":", 1)[1]
+ text, keyboard = _build_section(section, db_user.language)
+ await _render_message(callback.message, text, keyboard)
+ await state.clear()
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def start_price_edit(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+) -> None:
+ _, section, key = callback.data.split(":", 2)
+ texts = get_texts(db_user.language)
+ label = _resolve_label(section, key, db_user.language)
+
+ await state.update_data(
+ pricing_key=key,
+ pricing_section=section,
+ pricing_message_id=callback.message.message_id,
+ )
+ await state.set_state(PricingStates.waiting_for_value)
+
+ prompt = (
+ f"💰 {texts.t('ADMIN_PRICING_EDIT_TITLE', 'Изменение цены')}\n\n"
+ f"{texts.t('ADMIN_PRICING_EDIT_TARGET', 'Текущий тариф')}: {label}\n"
+ f"{texts.t('ADMIN_PRICING_EDIT_CURRENT', 'Текущее значение')}: {settings.format_price(getattr(settings, key, 0))}\n\n"
+ f"{texts.t('ADMIN_PRICING_EDIT_PROMPT', 'Введите новую стоимость в рублях (например 990 или 990.50). Для бесплатного тарифа укажите 0.')}"
+ )
+
+ keyboard = types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text=texts.t("ADMIN_PRICING_EDIT_CANCEL", "❌ Отмена"),
+ callback_data=f"admin_pricing_section:{section}",
+ )
+ ]
+ ]
+ )
+
+ await _render_message(callback.message, prompt, keyboard)
+ await callback.answer()
+
+
+async def process_price_input(
+ message: types.Message,
+ state: FSMContext,
+ db_user: User,
+ db: AsyncSession,
+) -> None:
+ data = await state.get_data()
+ key = data.get("pricing_key")
+ section = data.get("pricing_section", "periods")
+ message_id = data.get("pricing_message_id")
+
+ texts = get_texts(db_user.language)
+
+ if not key:
+ await message.answer(texts.t("ADMIN_PRICING_EDIT_EXPIRED", "Сессия редактирования истекла."))
+ await state.clear()
+ return
+
+ raw_value = message.text or ""
+ if raw_value.strip().lower() in {"cancel", "отмена"}:
+ await state.clear()
+ section_text, section_keyboard = _build_section(section, db_user.language)
+ if message_id:
+ await _render_message_by_id(
+ message.bot,
+ message.chat.id,
+ message_id,
+ section_text,
+ section_keyboard,
+ )
+ await message.answer(texts.t("ADMIN_PRICING_EDIT_CANCELLED", "Изменения отменены."))
+ return
+
+ try:
+ price_kopeks = _parse_price_input(raw_value)
+ except ValueError:
+ await message.answer(
+ texts.t(
+ "ADMIN_PRICING_EDIT_INVALID",
+ "Не удалось распознать цену. Укажите число в рублях (например 990 или 990.50).",
+ )
+ )
+ return
+
+ await bot_configuration_service.set_value(db, key, price_kopeks)
+ await db.commit()
+
+ label = _resolve_label(section, key, db_user.language)
+ await message.answer(
+ texts.t("ADMIN_PRICING_EDIT_SUCCESS", "Цена для {item} обновлена: {price}").format(
+ item=label,
+ price=settings.format_price(price_kopeks),
+ )
+ )
+
+ await state.clear()
+
+ if message_id:
+ section_text, section_keyboard = _build_section(section, db_user.language)
+ await _render_message_by_id(
+ message.bot,
+ message.chat.id,
+ message_id,
+ section_text,
+ section_keyboard,
+ )
+
+
+def register_handlers(dp: Dispatcher) -> None:
+ dp.callback_query.register(
+ show_pricing_menu,
+ F.data.in_({"admin_pricing", "admin_subs_pricing"}),
+ )
+ dp.callback_query.register(
+ show_pricing_section,
+ F.data.startswith("admin_pricing_section:"),
+ )
+ dp.callback_query.register(
+ start_price_edit,
+ F.data.startswith("admin_pricing_edit:"),
+ )
+ dp.message.register(
+ process_price_input,
+ PricingStates.waiting_for_value,
+ )
diff --git a/app/handlers/admin/servers.py b/app/handlers/admin/servers.py
index 2a1ba638..ab918955 100644
--- a/app/handlers/admin/servers.py
+++ b/app/handlers/admin/servers.py
@@ -171,7 +171,7 @@ async def show_servers_menu(
types.InlineKeyboardButton(text="📈 Подробная статистика", callback_data="admin_servers_stats")
],
[
- types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")
+ types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
]
diff --git a/app/handlers/admin/subscriptions.py b/app/handlers/admin/subscriptions.py
index 0c341a04..ae2d8f43 100644
--- a/app/handlers/admin/subscriptions.py
+++ b/app/handlers/admin/subscriptions.py
@@ -4,7 +4,6 @@ from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
-from app.config import settings
from app.states import AdminStates
from app.database.models import User
from app.keyboards.admin import get_admin_subscriptions_keyboard
@@ -87,10 +86,6 @@ async def show_subscriptions_menu(
],
[
types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_subs_stats"),
- types.InlineKeyboardButton(text="💰 Настройки цен", callback_data="admin_subs_pricing")
- ],
- [
- types.InlineKeyboardButton(text="🌐 Управление серверами", callback_data="admin_servers"),
types.InlineKeyboardButton(text="🌍 География", callback_data="admin_subs_countries")
],
[
@@ -277,56 +272,6 @@ async def show_subscriptions_stats(
@admin_required
@error_handler
-async def show_pricing_settings(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession
-):
- text = f"""
-⚙️ Настройки цен
-
-Периоды подписки:
-- 14 дней: {settings.format_price(settings.PRICE_14_DAYS)}
-- 30 дней: {settings.format_price(settings.PRICE_30_DAYS)}
-- 60 дней: {settings.format_price(settings.PRICE_60_DAYS)}
-- 90 дней: {settings.format_price(settings.PRICE_90_DAYS)}
-- 180 дней: {settings.format_price(settings.PRICE_180_DAYS)}
-- 360 дней: {settings.format_price(settings.PRICE_360_DAYS)}
-
-Трафик-пакеты:
-- 5 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_5GB)}
-- 10 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_10GB)}
-- 25 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_25GB)}
-- 50 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_50GB)}
-- 100 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_100GB)}
-- 250 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_250GB)}
-
-Дополнительно:
-- За устройство: {settings.format_price(settings.PRICE_PER_DEVICE)}
-"""
-
- keyboard = [
- # [
- # types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"),
- # types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices")
- # ],
- # [
- # types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price")
- # ],
- [
- types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")
- ]
- ]
-
- await callback.message.edit_text(
- text,
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
async def show_countries_management(
callback: types.CallbackQuery,
db_user: User,
@@ -487,7 +432,6 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_subscriptions_list, F.data == "admin_subs_list")
dp.callback_query.register(show_expiring_subscriptions, F.data == "admin_subs_expiring")
dp.callback_query.register(show_subscriptions_stats, F.data == "admin_subs_stats")
- dp.callback_query.register(show_pricing_settings, F.data == "admin_subs_pricing")
dp.callback_query.register(show_countries_management, F.data == "admin_subs_countries")
dp.callback_query.register(send_expiry_reminders, F.data == "admin_send_expiry_reminders")
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index ee7729af..60be21e8 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -13,12 +13,46 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
- [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_USERS_SUBSCRIPTIONS", "👥 Юзеры/Подписки"), callback_data="admin_submenu_users")],
- [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")],
- [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SETTINGS", "⚙️ Настройки"), callback_data="admin_submenu_settings")],
- [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SYSTEM", "🛠️ Система"), callback_data="admin_submenu_system")],
+ [
+ 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",
+ ),
+ ],
+ [
+ InlineKeyboardButton(
+ text=_t(texts, "ADMIN_MAIN_SETTINGS", "⚙️ Настройки"),
+ callback_data="admin_submenu_settings",
+ ),
+ InlineKeyboardButton(
+ text=_t(texts, "ADMIN_MAIN_SYSTEM", "🛠️ Система"),
+ callback_data="admin_submenu_system",
+ ),
+ ],
[InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")]
])
@@ -299,10 +333,6 @@ def get_admin_subscriptions_keyboard(language: str = "ru") -> InlineKeyboardMark
)
],
[
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_SUBSCRIPTIONS_PRICING", "⚙️ Настройки цен"),
- callback_data="admin_subs_pricing"
- ),
InlineKeyboardButton(
text=_t(texts, "ADMIN_SUBSCRIPTIONS_COUNTRIES", "🌍 Управление странами"),
callback_data="admin_subs_countries"
diff --git a/app/states.py b/app/states.py
index 61015d48..25ceccd1 100644
--- a/app/states.py
+++ b/app/states.py
@@ -135,6 +135,10 @@ 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()
diff --git a/locales/en.json b/locales/en.json
index d970b566..63c0b1cd 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -543,6 +543,8 @@
"NOTIFY_PROMPT_THIRD_HOURS": "Enter the number of hours the late discount is active (1-168):",
"NOTIFY_PROMPT_THIRD_DAYS": "After how many days without a subscription should we send the offer? (minimum 2):",
"ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Users / Subscriptions",
+ "ADMIN_MAIN_SERVERS": "🌐 Servers",
+ "ADMIN_MAIN_PRICING": "💰 Pricing",
"ADMIN_MAIN_PROMO_STATS": "💰 Promo codes / Stats",
"ADMIN_MAIN_SUPPORT": "🛟 Support",
"ADMIN_MAIN_MESSAGES": "📨 Messages",
@@ -815,5 +817,30 @@
"ADMIN_BROADCAST_BUTTON_CONNECT": "🔗 Connect",
"ADMIN_BROADCAST_BUTTON_SUBSCRIPTION": "📱 Subscription",
"ADMIN_BROADCAST_BUTTON_SUPPORT": "🛠️ Support",
- "ADMIN_BROADCAST_BUTTON_HOME": "🏠 Main menu"
+ "ADMIN_BROADCAST_BUTTON_HOME": "🏠 Main menu",
+ "ADMIN_PRICING_SUMMARY_EMPTY": "—",
+ "ADMIN_PRICING_MENU_TITLE": "Pricing management",
+ "ADMIN_PRICING_MENU_DESCRIPTION": "Quick access to subscription plans, traffic bundles and extra services.",
+ "ADMIN_PRICING_MENU_SUMMARY": "Quick summary:",
+ "ADMIN_PRICING_MENU_SUMMARY_PERIODS": "• Periods: {summary}",
+ "ADMIN_PRICING_MENU_SUMMARY_TRAFFIC": "• Traffic: {summary}",
+ "ADMIN_PRICING_MENU_SUMMARY_EXTRA": "• Extras: {summary}",
+ "ADMIN_PRICING_MENU_PROMPT": "Choose a section to edit:",
+ "ADMIN_PRICING_BUTTON_PERIODS": "🗓 Subscription periods",
+ "ADMIN_PRICING_BUTTON_TRAFFIC": "📦 Traffic packages",
+ "ADMIN_PRICING_BUTTON_EXTRA": "➕ Extras",
+ "ADMIN_PRICING_SECTION_PERIODS_TITLE": "🗓 Subscription periods",
+ "ADMIN_PRICING_SECTION_TRAFFIC_TITLE": "📦 Traffic packages",
+ "ADMIN_PRICING_SECTION_EXTRA_TITLE": "➕ Extra options",
+ "ADMIN_PRICING_SECTION_PROMPT": "Select what to update:",
+ "ADMIN_PRICING_SECTION_EMPTY": "No values available.",
+ "ADMIN_PRICING_EDIT_TITLE": "Update price",
+ "ADMIN_PRICING_EDIT_TARGET": "Current item",
+ "ADMIN_PRICING_EDIT_CURRENT": "Current value",
+ "ADMIN_PRICING_EDIT_PROMPT": "Enter a new price in RUB (e.g. 990 or 990.50). Use 0 for a free plan.",
+ "ADMIN_PRICING_EDIT_CANCEL": "❌ Cancel",
+ "ADMIN_PRICING_EDIT_EXPIRED": "Editing session expired.",
+ "ADMIN_PRICING_EDIT_CANCELLED": "Changes cancelled.",
+ "ADMIN_PRICING_EDIT_INVALID": "Could not parse the price. Please enter a number in RUB (e.g. 990 or 990.50).",
+ "ADMIN_PRICING_EDIT_SUCCESS": "Price for {item} updated: {price}"
}
diff --git a/locales/ru.json b/locales/ru.json
index 13c5c434..6c7c9422 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -543,6 +543,8 @@
"NOTIFY_PROMPT_THIRD_HOURS": "Введите количество часов действия скидки (1-168):",
"NOTIFY_PROMPT_THIRD_DAYS": "Через сколько дней после истечения отправлять предложение? (минимум 2):",
"ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Юзеры/Подписки",
+ "ADMIN_MAIN_SERVERS": "🌐 Серверы",
+ "ADMIN_MAIN_PRICING": "💰 Цены",
"ADMIN_MAIN_PROMO_STATS": "💰 Промокоды/Статистика",
"ADMIN_MAIN_SUPPORT": "🛟 Поддержка",
"ADMIN_MAIN_MESSAGES": "📨 Сообщения",
@@ -815,5 +817,30 @@
"ADMIN_BROADCAST_BUTTON_CONNECT": "🔗 Подключиться",
"ADMIN_BROADCAST_BUTTON_SUBSCRIPTION": "📱 Подписка",
"ADMIN_BROADCAST_BUTTON_SUPPORT": "🛠️ Техподдержка",
- "ADMIN_BROADCAST_BUTTON_HOME": "🏠 На главную"
+ "ADMIN_BROADCAST_BUTTON_HOME": "🏠 На главную",
+ "ADMIN_PRICING_SUMMARY_EMPTY": "—",
+ "ADMIN_PRICING_MENU_TITLE": "Управление ценами",
+ "ADMIN_PRICING_MENU_DESCRIPTION": "Быстрый доступ к тарифам подписок, пакетам трафика и дополнительным услугам.",
+ "ADMIN_PRICING_MENU_SUMMARY": "Краткая сводка:",
+ "ADMIN_PRICING_MENU_SUMMARY_PERIODS": "• Периоды: {summary}",
+ "ADMIN_PRICING_MENU_SUMMARY_TRAFFIC": "• Трафик: {summary}",
+ "ADMIN_PRICING_MENU_SUMMARY_EXTRA": "• Дополнительно: {summary}",
+ "ADMIN_PRICING_MENU_PROMPT": "Выберите раздел для редактирования:",
+ "ADMIN_PRICING_BUTTON_PERIODS": "🗓 Периоды подписки",
+ "ADMIN_PRICING_BUTTON_TRAFFIC": "📦 Пакеты трафика",
+ "ADMIN_PRICING_BUTTON_EXTRA": "➕ Дополнительно",
+ "ADMIN_PRICING_SECTION_PERIODS_TITLE": "🗓 Периоды подписки",
+ "ADMIN_PRICING_SECTION_TRAFFIC_TITLE": "📦 Пакеты трафика",
+ "ADMIN_PRICING_SECTION_EXTRA_TITLE": "➕ Дополнительные опции",
+ "ADMIN_PRICING_SECTION_PROMPT": "Выберите что изменить:",
+ "ADMIN_PRICING_SECTION_EMPTY": "Нет доступных значений.",
+ "ADMIN_PRICING_EDIT_TITLE": "Изменение цены",
+ "ADMIN_PRICING_EDIT_TARGET": "Текущий тариф",
+ "ADMIN_PRICING_EDIT_CURRENT": "Текущее значение",
+ "ADMIN_PRICING_EDIT_PROMPT": "Введите новую стоимость в рублях (например 990 или 990.50). Для бесплатного тарифа укажите 0.",
+ "ADMIN_PRICING_EDIT_CANCEL": "❌ Отмена",
+ "ADMIN_PRICING_EDIT_EXPIRED": "Сессия редактирования истекла.",
+ "ADMIN_PRICING_EDIT_CANCELLED": "Изменения отменены.",
+ "ADMIN_PRICING_EDIT_INVALID": "Не удалось распознать цену. Укажите число в рублях (например 990 или 990.50).",
+ "ADMIN_PRICING_EDIT_SUCCESS": "Цена для {item} обновлена: {price}"
}