diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py
index 1cb146f7..d7516549 100644
--- a/app/handlers/admin/users.py
+++ b/app/handlers/admin/users.py
@@ -1,39 +1,60 @@
import logging
from datetime import datetime, timedelta
-from typing import Optional
+from typing import Optional, List, Tuple
+
from aiogram import Dispatcher, types, F
-from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.fsm.context import FSMContext
+from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
-from app.states import AdminStates
-from app.database.models import User, UserStatus, Subscription, SubscriptionStatus, TransactionType
-from app.database.crud.user import get_user_by_id
from app.database.crud.campaign import (
get_campaign_registration_by_user,
get_campaign_statistics,
)
-from app.keyboards.admin import (
- get_admin_users_keyboard, get_user_management_keyboard,
- get_admin_pagination_keyboard, get_confirmation_keyboard,
- get_admin_users_filters_keyboard, get_user_promo_group_keyboard
-)
-from app.localization.texts import get_texts
-from app.services.user_service import UserService
-from app.services.admin_notification_service import AdminNotificationService
+from app.database.crud.discount_offer import upsert_discount_offer
from app.database.crud.promo_group import get_promo_groups_with_counts
-from app.utils.decorators import admin_required, error_handler
-from app.utils.formatters import format_datetime, format_time_ago
-from app.services.remnawave_service import RemnaWaveService
-from app.external.remnawave_api import TrafficLimitStrategy
+from app.database.crud.promo_offer_template import (
+ ensure_default_templates,
+ get_promo_offer_template_by_id,
+ list_promo_offer_templates,
+)
from app.database.crud.server_squad import (
get_all_server_squads,
get_server_squad_by_uuid,
get_server_squad_by_id,
get_server_ids_by_uuids,
)
+from app.database.crud.user import get_user_by_id
+from app.database.models import (
+ Subscription,
+ SubscriptionStatus,
+ TransactionType,
+ User,
+ UserStatus,
+)
+from app.external.remnawave_api import TrafficLimitStrategy
+from app.keyboards.admin import (
+ get_admin_pagination_keyboard,
+ get_admin_users_filters_keyboard,
+ get_admin_users_keyboard,
+ get_confirmation_keyboard,
+ get_user_management_keyboard,
+ get_user_promo_group_keyboard,
+)
+from app.localization.texts import get_texts
+from app.services.admin_notification_service import AdminNotificationService
+from app.services.remnawave_service import RemnaWaveService
from app.services.subscription_service import SubscriptionService
+from app.services.user_service import UserService
+from app.states import AdminStates
+from app.utils.decorators import admin_required, error_handler
+from app.utils.formatters import format_datetime, format_time_ago
+from app.utils.promo_offer import (
+ build_promo_offer_timer_line,
+ get_user_active_promo_discount_percent,
+)
logger = logging.getLogger(__name__)
@@ -997,6 +1018,12 @@ async def _render_user_subscription_overview(
user = profile["user"]
subscription = profile["subscription"]
+ if subscription is not None:
+ try:
+ setattr(user, "subscription", subscription)
+ except Exception:
+ pass
+
text = "📱 Подписка и настройки пользователя\n\n"
text += f"👤 {user.full_name} (ID: {user.telegram_id})\n\n"
@@ -1448,6 +1475,44 @@ async def show_user_management(
else:
sections.append(texts.ADMIN_USER_MANAGEMENT_PROMO_GROUP_NONE)
+ promo_percent = get_user_active_promo_discount_percent(user)
+ promo_expires_at = getattr(user, "promo_offer_discount_expires_at", None)
+ promo_source = getattr(user, "promo_offer_discount_source", None)
+
+ if promo_percent > 0:
+ expires_text = (
+ format_datetime(promo_expires_at)
+ if promo_expires_at
+ else texts.get("ADMIN_USER_PROMO_OFFER_NO_EXPIRY", "∞")
+ )
+ source_text = (
+ promo_source
+ if promo_source
+ else texts.get("ADMIN_USER_PROMO_OFFER_SOURCE_UNKNOWN", "—")
+ )
+ promo_section = texts.get(
+ "ADMIN_USER_MANAGEMENT_PROMO_OFFER_ACTIVE",
+ "🎯 Активное промо: {percent}% (источник: {source})\nДействует до: {expires_at}",
+ ).format(
+ percent=promo_percent,
+ source=source_text,
+ expires_at=expires_text,
+ )
+ try:
+ timer_line = await build_promo_offer_timer_line(db, user, texts)
+ except Exception:
+ timer_line = None
+ if timer_line:
+ promo_section += f"\n{timer_line}"
+ sections.append(promo_section)
+ else:
+ sections.append(
+ texts.get(
+ "ADMIN_USER_MANAGEMENT_PROMO_OFFER_NONE",
+ "🎯 Активное промо: отсутствует",
+ )
+ )
+
text = "\n\n".join(sections)
# Проверяем состояние, чтобы определить, откуда пришел пользователь
@@ -1485,6 +1550,76 @@ async def show_user_management(
await callback.answer()
+PROMO_TEMPLATE_ICONS = {
+ "test_access": "🧪",
+ "extend_discount": "💎",
+ "purchase_discount": "🎯",
+}
+
+PROMO_TEMPLATE_EFFECT_TYPES = {
+ "test_access": "test_access",
+ "extend_discount": "percent_discount",
+ "purchase_discount": "percent_discount",
+}
+
+
+def _build_user_template_button_text(template) -> str:
+ icon = PROMO_TEMPLATE_ICONS.get(template.offer_type, "📨")
+ suffix = ""
+ try:
+ percent = int(template.discount_percent or 0)
+ except (TypeError, ValueError):
+ percent = 0
+ if template.offer_type != "test_access" and percent > 0:
+ suffix = f" ({percent}%)"
+ return f"{icon} {template.name}{suffix}"
+
+
+def _render_promo_template_text(template, language: str, *, server_name: Optional[str] = None) -> str:
+ replacements = {
+ "discount_percent": template.discount_percent,
+ "valid_hours": template.valid_hours,
+ "test_duration_hours": template.test_duration_hours or 0,
+ "active_discount_hours": template.active_discount_hours or template.valid_hours,
+ }
+
+ if server_name is not None:
+ replacements.setdefault("server_name", server_name)
+ else:
+ replacements.setdefault("server_name", "???")
+
+ try:
+ return template.message_text.format(**replacements)
+ except Exception:
+ logger.warning(
+ "Не удалось форматировать текст промо-предложения %s для языка %s",
+ template.id,
+ language,
+ )
+ return template.message_text
+
+
+async def _resolve_template_squad_info(
+ db: AsyncSession,
+ template,
+) -> Tuple[Optional[str], Optional[str]]:
+ if template.offer_type != "test_access":
+ return None, None
+
+ squad_uuids = template.test_squad_uuids or []
+ if not squad_uuids:
+ return None, None
+
+ squad_uuid = str(squad_uuids[0])
+ server = await get_server_squad_by_uuid(db, squad_uuid)
+ server_name = server.display_name if server else None
+ return squad_uuid, server_name
+
+
+def _get_template_effect_type(template) -> str:
+ return PROMO_TEMPLATE_EFFECT_TYPES.get(template.offer_type, "percent_discount")
+
+
async def _render_user_promo_group(
message: types.Message,
language: str,
@@ -1615,6 +1750,313 @@ async def set_user_promo_group(
)
+@admin_required
+@error_handler
+async def show_user_promo_templates(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+):
+ try:
+ user_id = int(callback.data.split("_")[-1])
+ except (ValueError, AttributeError):
+ await callback.answer("❌ Некорректные данные", show_alert=True)
+ return
+
+ user_service = UserService()
+ profile = await user_service.get_user_profile(db, user_id)
+ if not profile:
+ await callback.answer("❌ Пользователь не найден", show_alert=True)
+ return
+
+ target_user: User = profile["user"]
+ subscription: Optional[Subscription] = profile.get("subscription")
+ if subscription is not None:
+ try:
+ setattr(target_user, "subscription", subscription)
+ except Exception:
+ pass
+
+ await ensure_default_templates(db, created_by=db_user.id)
+ templates = await list_promo_offer_templates(db)
+
+ texts = get_texts(db_user.language)
+ active_templates = [template for template in templates if getattr(template, "is_active", True)]
+
+ if not active_templates:
+ await callback.answer(
+ texts.t("ADMIN_USER_PROMO_TEMPLATES_EMPTY", "Нет активных шаблонов промо-предложений."),
+ show_alert=True,
+ )
+ return
+
+ promo_percent = get_user_active_promo_discount_percent(target_user)
+ promo_expires_at = getattr(target_user, "promo_offer_discount_expires_at", None)
+ expires_text = (
+ format_datetime(promo_expires_at)
+ if promo_expires_at
+ else texts.get("ADMIN_USER_PROMO_OFFER_NO_EXPIRY", "∞")
+ )
+
+ message_lines = [
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATES_TITLE",
+ "🎯 Промо-предложения для {name}",
+ ).format(name=target_user.full_name),
+ "",
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATES_HINT",
+ "Выберите шаблон, чтобы отправить пользователю промо-предложение.",
+ ),
+ ]
+
+ if promo_percent > 0:
+ message_lines.extend(
+ [
+ "",
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATES_ACTIVE_INFO",
+ "Активная скидка: {percent}% (до {expires_at})",
+ ).format(percent=promo_percent, expires_at=expires_text),
+ ]
+ )
+
+ keyboard_rows: List[List[InlineKeyboardButton]] = [
+ [
+ InlineKeyboardButton(
+ text=_build_user_template_button_text(template),
+ callback_data=f"admin_user_promo_send_{target_user.id}_{template.id}",
+ )
+ ]
+ for template in active_templates
+ ]
+
+ keyboard_rows.append([
+ InlineKeyboardButton(
+ text=texts.BACK,
+ callback_data=f"admin_user_manage_{target_user.id}",
+ )
+ ])
+
+ await state.set_state(None)
+
+ await callback.message.edit_text(
+ "\n".join(message_lines),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
+ parse_mode="HTML",
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def send_user_promo_template(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+):
+ prefix = "admin_user_promo_send_"
+ if not callback.data.startswith(prefix):
+ await callback.answer("❌ Некорректные данные", show_alert=True)
+ return
+
+ payload = callback.data[len(prefix):]
+ try:
+ user_part, template_part = payload.split("_", 1)
+ user_id = int(user_part)
+ template_id = int(template_part)
+ except (ValueError, AttributeError):
+ await callback.answer("❌ Некорректные данные", show_alert=True)
+ return
+
+ user_service = UserService()
+ profile = await user_service.get_user_profile(db, user_id)
+ if not profile:
+ await callback.answer("❌ Пользователь не найден", show_alert=True)
+ return
+
+ target_user: User = profile["user"]
+ subscription: Optional[Subscription] = profile.get("subscription")
+ if subscription is not None:
+ try:
+ setattr(target_user, "subscription", subscription)
+ except Exception:
+ pass
+
+ template = await get_promo_offer_template_by_id(db, template_id)
+ if not template or not getattr(template, "is_active", True):
+ await callback.answer("❌ Шаблон недоступен", show_alert=True)
+ return
+
+ squad_uuid, squad_name = await _resolve_template_squad_info(db, template)
+
+ if template.offer_type == "test_access" and squad_uuid:
+ connected = set(subscription.connected_squads or []) if subscription else set()
+ if squad_uuid in connected:
+ texts = get_texts(db_user.language)
+ await callback.answer(
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_ALREADY_HAS_ACCESS",
+ "У пользователя уже есть доступ к этому серверу.",
+ ),
+ show_alert=True,
+ )
+ return
+
+ effect_type = _get_template_effect_type(template)
+
+ try:
+ offer_record = await upsert_discount_offer(
+ db,
+ user_id=target_user.id,
+ subscription_id=subscription.id if subscription else None,
+ notification_type=f"promo_template_{template.id}",
+ discount_percent=template.discount_percent,
+ bonus_amount_kopeks=0,
+ valid_hours=template.valid_hours,
+ effect_type=effect_type,
+ extra_data={
+ "template_id": template.id,
+ "offer_type": template.offer_type,
+ "test_duration_hours": template.test_duration_hours,
+ "test_squad_uuids": template.test_squad_uuids,
+ "active_discount_hours": template.active_discount_hours,
+ },
+ )
+ except Exception as exc:
+ logger.error(
+ "Failed to upsert discount offer for template %s and user %s: %s",
+ template_id,
+ user_id,
+ exc,
+ )
+ texts = get_texts(db_user.language)
+ await callback.answer(
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_ERROR",
+ "Не удалось подготовить промо-предложение.",
+ ),
+ show_alert=True,
+ )
+ return
+
+ user_language = target_user.language or db_user.language
+ user_texts = get_texts(user_language)
+ keyboard_rows: List[List[InlineKeyboardButton]] = [
+ [
+ InlineKeyboardButton(
+ text=template.button_text,
+ callback_data=f"claim_discount_{offer_record.id}",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text=user_texts.t("PROMO_OFFER_CLOSE", "❌ Закрыть"),
+ callback_data="promo_offer_close",
+ )
+ ],
+ ]
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
+ message_text = _render_promo_template_text(
+ template,
+ user_language,
+ server_name=squad_name,
+ )
+
+ try:
+ await callback.bot.send_message(
+ chat_id=target_user.telegram_id,
+ text=message_text,
+ reply_markup=keyboard,
+ parse_mode="HTML",
+ )
+ except TelegramForbiddenError:
+ texts = get_texts(db_user.language)
+ await callback.answer(
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_DELIVERY_FAILED",
+ "Пользователь заблокировал бота. Сообщение не отправлено.",
+ ),
+ show_alert=True,
+ )
+ return
+ except TelegramBadRequest as exc:
+ logger.warning(
+ "Telegram API error while sending promo template %s to %s: %s",
+ template_id,
+ target_user.telegram_id,
+ exc,
+ )
+ texts = get_texts(db_user.language)
+ await callback.answer(
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_DELIVERY_FAILED",
+ "Не удалось отправить сообщение пользователю.",
+ ),
+ show_alert=True,
+ )
+ return
+ except Exception as exc:
+ logger.error(
+ "Unexpected error while sending promo template %s to %s: %s",
+ template_id,
+ target_user.telegram_id,
+ exc,
+ )
+ texts = get_texts(db_user.language)
+ await callback.answer(
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_ERROR",
+ "Не удалось отправить промо-предложение пользователю.",
+ ),
+ show_alert=True,
+ )
+ return
+
+ texts = get_texts(db_user.language)
+ success_text = texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_SUCCESS",
+ "✅ Промо-предложение «{name}» отправлено пользователю {user}.",
+ ).format(name=template.name, user=target_user.full_name)
+
+ reply_markup = InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text=texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_AGAIN",
+ "📨 Отправить другое",
+ ),
+ callback_data=f"admin_user_promo_templates_{target_user.id}",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text=texts.BACK,
+ callback_data=f"admin_user_manage_{target_user.id}",
+ )
+ ],
+ ]
+ )
+
+ await state.set_state(None)
+
+ await callback.message.edit_text(
+ success_text,
+ reply_markup=reply_markup,
+ parse_mode="HTML",
+ )
+ await callback.answer(
+ texts.t(
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_DONE",
+ "Промо-предложение отправлено.",
+ )
+ )
+
+
@admin_required
@error_handler
@@ -3969,6 +4411,16 @@ def register_handlers(dp: Dispatcher):
F.data.startswith("admin_user_promo_group_set_")
)
+ dp.callback_query.register(
+ show_user_promo_templates,
+ F.data.startswith("admin_user_promo_templates_")
+ )
+
+ dp.callback_query.register(
+ send_user_promo_template,
+ F.data.startswith("admin_user_promo_send_")
+ )
+
dp.callback_query.register(
start_balance_edit,
F.data.startswith("admin_user_balance_")
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index 2f5200cf..4a5f45fb 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -724,6 +724,12 @@ def get_user_management_keyboard(user_id: int, user_status: str, language: str =
callback_data=f"admin_user_promo_group_{user_id}"
)
],
+ [
+ InlineKeyboardButton(
+ text=texts.get("ADMIN_USER_PROMO_TEMPLATES_BUTTON", "🎯 Промо-предложения"),
+ callback_data=f"admin_user_promo_templates_{user_id}"
+ )
+ ],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_USER_STATISTICS", "📊 Статистика"),
diff --git a/locales/en.json b/locales/en.json
index c62cfe8d..276352b6 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -240,6 +240,7 @@
"ATTACHMENTS_SENT": "✅ Attachments sent.",
"DELETE_MESSAGE": "🗑 Delete",
"ADMIN_USER_PROMO_GROUP_BUTTON": "👥 Promo group",
+ "ADMIN_USER_PROMO_TEMPLATES_BUTTON": "🎯 Promo offers",
"ADMIN_USER_PROMO_GROUP_TITLE": "👥 User promo group",
"ADMIN_USER_PROMO_GROUP_CURRENT": "Current group: {name}",
"ADMIN_USER_PROMO_GROUP_CURRENT_NONE": "Current group: not assigned",
@@ -784,6 +785,20 @@
"ADMIN_USER_MANAGEMENT_SUBSCRIPTION_NONE": "Subscription: None",
"ADMIN_USER_MANAGEMENT_PROMO_GROUP": "Promo group:\n• Name: {name}\n• Server discount: {server_discount}%\n• Traffic discount: {traffic_discount}%\n• Device discount: {device_discount}%",
"ADMIN_USER_MANAGEMENT_PROMO_GROUP_NONE": "Promo group: Not assigned",
+ "ADMIN_USER_MANAGEMENT_PROMO_OFFER_ACTIVE": "🎯 Active promo: {percent}% (source: {source})\nValid until: {expires_at}",
+ "ADMIN_USER_MANAGEMENT_PROMO_OFFER_NONE": "🎯 Active promo: none",
+ "ADMIN_USER_PROMO_OFFER_SOURCE_UNKNOWN": "unknown",
+ "ADMIN_USER_PROMO_OFFER_NO_EXPIRY": "no expiry",
+ "ADMIN_USER_PROMO_TEMPLATES_TITLE": "🎯 Promo offers for {name}",
+ "ADMIN_USER_PROMO_TEMPLATES_HINT": "Choose a template to send a promo offer to the user.",
+ "ADMIN_USER_PROMO_TEMPLATES_ACTIVE_INFO": "Active discount: {percent}% (until {expires_at})",
+ "ADMIN_USER_PROMO_TEMPLATES_EMPTY": "No active promo offer templates.",
+ "ADMIN_USER_PROMO_TEMPLATE_ALREADY_HAS_ACCESS": "The user already has access to this server.",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_ERROR": "Failed to send the promo offer to the user.",
+ "ADMIN_USER_PROMO_TEMPLATE_DELIVERY_FAILED": "Failed to deliver the message to the user.",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_SUCCESS": "✅ Promo offer “{name}” sent to {user}.",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_AGAIN": "📨 Send another",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_DONE": "Promo offer sent.",
"ADMIN_PROMOCODE_TYPE_BALANCE": "💰 Balance",
"ADMIN_PROMOCODE_TYPE_DAYS": "📅 Subscription days",
"ADMIN_PROMOCODE_TYPE_TRIAL": "🎁 Trial",
diff --git a/locales/ru.json b/locales/ru.json
index 7a075381..e90dbd37 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -96,6 +96,7 @@
"ATTACHMENTS_SENT": "✅ Вложения отправлены.",
"DELETE_MESSAGE": "🗑 Удалить",
"ADMIN_USER_PROMO_GROUP_BUTTON": "👥 Промогруппа",
+ "ADMIN_USER_PROMO_TEMPLATES_BUTTON": "🎯 Промо-предложения",
"ADMIN_USER_PROMO_GROUP_TITLE": "👥 Промогруппа пользователя",
"ADMIN_USER_PROMO_GROUP_CURRENT": "Текущая группа: {name}",
"ADMIN_USER_PROMO_GROUP_CURRENT_NONE": "Текущая группа: не назначена",
@@ -784,6 +785,20 @@
"ADMIN_USER_MANAGEMENT_SUBSCRIPTION_NONE": "Подписка: Отсутствует",
"ADMIN_USER_MANAGEMENT_PROMO_GROUP": "Промогруппа:\n• Название: {name}\n• Скидка на сервера: {server_discount}%\n• Скидка на трафик: {traffic_discount}%\n• Скидка на устройства: {device_discount}%",
"ADMIN_USER_MANAGEMENT_PROMO_GROUP_NONE": "Промогруппа: Не назначена",
+ "ADMIN_USER_MANAGEMENT_PROMO_OFFER_ACTIVE": "🎯 Активное промо: {percent}% (источник: {source})\nДействует до: {expires_at}",
+ "ADMIN_USER_MANAGEMENT_PROMO_OFFER_NONE": "🎯 Активное промо: отсутствует",
+ "ADMIN_USER_PROMO_OFFER_SOURCE_UNKNOWN": "неизвестно",
+ "ADMIN_USER_PROMO_OFFER_NO_EXPIRY": "без ограничения",
+ "ADMIN_USER_PROMO_TEMPLATES_TITLE": "🎯 Промо-предложения для {name}",
+ "ADMIN_USER_PROMO_TEMPLATES_HINT": "Выберите шаблон, чтобы отправить промо-предложение пользователю.",
+ "ADMIN_USER_PROMO_TEMPLATES_ACTIVE_INFO": "Активная скидка: {percent}% (до {expires_at})",
+ "ADMIN_USER_PROMO_TEMPLATES_EMPTY": "Нет активных шаблонов промо-предложений.",
+ "ADMIN_USER_PROMO_TEMPLATE_ALREADY_HAS_ACCESS": "У пользователя уже есть доступ к этому серверу.",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_ERROR": "Не удалось отправить промо-предложение пользователю.",
+ "ADMIN_USER_PROMO_TEMPLATE_DELIVERY_FAILED": "Не удалось отправить сообщение пользователю.",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_SUCCESS": "✅ Промо-предложение «{name}» отправлено пользователю {user}.",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_AGAIN": "📨 Отправить другое",
+ "ADMIN_USER_PROMO_TEMPLATE_SEND_DONE": "Промо-предложение отправлено.",
"ADMIN_PROMOCODE_TYPE_BALANCE": "💰 Баланс",
"ADMIN_PROMOCODE_TYPE_DAYS": "📅 Дни подписки",
"ADMIN_PROMOCODE_TYPE_TRIAL": "🎁 Триал",