From e3e204d7af0e72422ef85ada2cf348b6338614c6 Mon Sep 17 00:00:00 2001 From: hexdev Date: Wed, 22 Apr 2026 17:41:44 +0700 Subject: [PATCH] feat: enhance subscription management with discount calculations and update support messages --- handlers/buy.py | 45 +++------ handlers/menus.py | 23 ++++- handlers/support.py | 65 +------------ keyboards/client.py | 7 +- misc/utils.py | 10 +- services/user_service.py | 204 ++------------------------------------- texts.py | 69 +++++++------ 7 files changed, 95 insertions(+), 328 deletions(-) diff --git a/handlers/buy.py b/handlers/buy.py index 2b54b5a..359bc28 100644 --- a/handlers/buy.py +++ b/handlers/buy.py @@ -1,9 +1,9 @@ import logging +import math from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery -from sqlalchemy.ext.asyncio import AsyncSession from config import settings from keyboards.client import ( @@ -14,22 +14,20 @@ from keyboards.client import ( ) from misc.utils import ( calculate_price, + convert_duration_to_int, convert_int_to_duration, + get_discount, ) -from schemas.di import DependenciesDTO from schemas.subscriptions import SubscriptionPlan -from services.user_service import UserServiceError from states.buy import SubscriptionStorage from texts import ( CALLBACK_FALLBACK, CHECKOUT_PAYMENT_CHOICE, CONTEXT_REINITIATED, - GENERAL_PROCESSING, NOTHING_PLACEHOLDER, STANDARD_FALLBACK, SUBSCRIPTION_DEVICE_SELECTOR, SUBSCRIPTION_DURATION_SELECTOR, - SUPPORT_MSG_SENT, ) router = Router() @@ -103,8 +101,12 @@ async def select_duration_init(cb: CallbackQuery, state: FSMContext): return try: + discount = get_discount(convert_duration_to_int(ctx.duration)) await cb.message.edit_text( - SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)), + SUBSCRIPTION_DURATION_SELECTOR.format( + price=math.ceil(calculate_price(ctx) * (1 - discount / 100)), + discount=math.floor(discount), + ), reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"), ) except Exception as exc: @@ -132,8 +134,12 @@ async def select_duration(cb: CallbackQuery, state: FSMContext): duration = int(cb_data[1]) if cb_data[1].isdigit() else 1 ctx.duration = convert_int_to_duration(duration) try: + discount = get_discount(convert_duration_to_int(ctx.duration)) await cb.message.edit_text( - SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)), + SUBSCRIPTION_DURATION_SELECTOR.format( + price=math.ceil(calculate_price(ctx) * (1 - discount / 100)), + discount=math.floor(discount), + ), reply_markup=duration_selector(current=ctx.duration, back_cb=f"devices:{ctx.devices}"), ) except Exception as exc: @@ -164,28 +170,3 @@ async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext): await state.set_state(SubscriptionStorage.checkout) await state.set_data({"ctx": ctx}) - - -@router.callback_query(F.data == "checkout") -async def checkout_support( - cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO -): - data = await state.get_data() - await state.clear() - - ctx: SubscriptionPlan | None = data.get("ctx") - if not ctx: - await cb.message.edit_text(STANDARD_FALLBACK, reply_markup=return_to_menu) - return - - await cb.message.edit_text(GENERAL_PROCESSING) - try: - await deps.user_service.create_subscription_ticket(session, cb.from_user, ctx, cb.bot) - await cb.message.edit_text(SUPPORT_MSG_SENT) - except UserServiceError: - await cb.message.edit_text(STANDARD_FALLBACK) - raise - except Exception: - await cb.message.edit_text(STANDARD_FALLBACK) - logger.exception("Exception occured while trying to forward a msg to support") - raise diff --git a/handlers/menus.py b/handlers/menus.py index 635e290..74dc2f0 100644 --- a/handlers/menus.py +++ b/handlers/menus.py @@ -4,9 +4,10 @@ from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from sqlalchemy.ext.asyncio import AsyncSession -from keyboards.client import main_menu, return_to_menu +from keyboards.client import close_kb, main_menu, return_to_menu from misc.utils import build_main_menu_text, format_subscription_link from schemas.di import DependenciesDTO +from services.rw import get_user_by_telegram_id from texts import FAQ_TEXT, GENERAL_PROCESSING router = Router() @@ -32,7 +33,7 @@ async def fetch_referal( session, user_id=msg.from_user.id, referal=referal, rw_sdk=deps.rw_sdk ) - await msg.reply( + await msg.answer( build_main_menu_text(format_subscription_link(user.subscription_link)), reply_markup=main_menu, ) @@ -49,7 +50,7 @@ async def standard_start( user = await deps.user_service.add_user(session, user_id=msg.from_user.id, rw_sdk=deps.rw_sdk) - await msg.reply( + await msg.answer( build_main_menu_text(format_subscription_link(user.subscription_link)), reply_markup=main_menu, ) @@ -88,3 +89,19 @@ async def faq(cb: CallbackQuery, state: FSMContext): await state.clear() await cb.message.edit_text(FAQ_TEXT, reply_markup=return_to_menu) + + +@router.callback_query(F.data == "sub_info") +async def sub_info( + cb: CallbackQuery, state: FSMContext, deps: DependenciesDTO, session: AsyncSession +): + await state.clear() + + await cb.message.edit_text(GENERAL_PROCESSING, reply_markup=close_kb) + rw_user = await get_user_by_telegram_id(deps.rw_sdk, cb.from_user.id) + user = await deps.user_service.update_user_link(session, cb.from_user.id, deps.rw_sdk) + + await cb.message.edit_text( + deps.user_service.format_subscription_message(rw_user, link=user.subscription_link), + reply_markup=return_to_menu, + ) diff --git a/handlers/support.py b/handlers/support.py index 37e388e..4a4e1c2 100644 --- a/handlers/support.py +++ b/handlers/support.py @@ -4,14 +4,9 @@ from aiogram import F, Router from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message -from sqlalchemy.ext.asyncio import AsyncSession -from config import settings from keyboards.client import return_to_menu -from schemas.di import DependenciesDTO -from services.user_service import UserServiceError -from states.support import SupportStorage -from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT +from texts import SUPPORT_INIT router = Router() logger = logging.getLogger(__name__) @@ -22,7 +17,6 @@ async def support_init(cb: CallbackQuery, state: FSMContext): await state.clear() await cb.message.edit_text(text=SUPPORT_INIT, reply_markup=return_to_menu) - await state.set_state(SupportStorage.prompt) @router.message(Command("support")) @@ -31,60 +25,3 @@ async def support_init_cmd(msg: Message, state: FSMContext): await msg.delete() await msg.answer(text=SUPPORT_INIT, reply_markup=return_to_menu) - await state.set_state(SupportStorage.prompt) - - -@router.message(SupportStorage.prompt) -async def received_message( - msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO -): - await state.clear() - - notification_msg = await msg.reply(GENERAL_PROCESSING) - - try: - await deps.user_service.support_forward_message(session, msg, deps.rw_sdk) - except UserServiceError: - await msg.reply(STANDARD_FALLBACK) - raise - except Exception: - await msg.reply(STANDARD_FALLBACK) - logger.exception("Exception occured while trying to forward a msg to support") - raise - - await notification_msg.edit_text(SUPPORT_MSG_SENT, reply_markup=return_to_menu) - await state.set_state(SupportStorage.prompt) - - -@router.message( - F.chat.id == settings.admin_group_id, - F.message_thread_id.is_not(None), - F.from_user.is_bot == False, # noqa: E712 - ~Command("close"), - ~Command("refpay"), -) -async def handle_admin_message( - msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO -): - await state.clear() - print(str(msg)) - - await deps.user_service.support_answer(session, msg) - - -@router.message(F.chat.id == settings.admin_group_id, Command("close")) -async def close_ticket( - msg: Message, state: FSMContext, session: AsyncSession, deps: DependenciesDTO -): - await state.clear() - - await deps.user_service.close_ticket(session, msg) - - -@router.callback_query(F.message.chat.id == settings.admin_group_id, F.data == "close") -async def close_ticket_cb( - cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO -): - await state.clear() - - await deps.user_service.close_ticket(session, cb.message) diff --git a/keyboards/client.py b/keyboards/client.py index 4d1fc88..cd73874 100644 --- a/keyboards/client.py +++ b/keyboards/client.py @@ -7,13 +7,14 @@ from schemas.subscriptions import SubscriptionDuration main_menu = InlineKeyboardBuilder( [ + [InlineKeyboardButton(text="📊 Подписка", callback_data="sub_info")], [ InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"), - InlineKeyboardButton(text="FAQ", callback_data="faq"), + InlineKeyboardButton(text="⁉️ FAQ", callback_data="faq"), ], [ - InlineKeyboardButton(text="Наш Канал", url="https://goo.gle"), - InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"), + InlineKeyboardButton(text="📢 Наш Канал", url="https://t.me/maleniavpn"), + InlineKeyboardButton(text="🎧 Тех. Поддержка", url="https://t.me/maleniasupportbot"), ], [InlineKeyboardButton(text="🔗 Обновить ссылку", callback_data="update_link")], [InlineKeyboardButton(text="👤 Реферальная система", callback_data="referal")], diff --git a/misc/utils.py b/misc/utils.py index 69c237f..3f3c8af 100644 --- a/misc/utils.py +++ b/misc/utils.py @@ -1,3 +1,4 @@ +import math import urllib.parse from datetime import UTC, datetime, timedelta from random import choice @@ -179,8 +180,15 @@ def calculate_price(subscription: SubscriptionPlan): ) * convert_duration_to_int(subscription.duration) +def get_discount(months) -> float: + if months <= 0: + return 0 + calculated = 5 * (math.log2(months / 3) + 1) + return max(0, calculated) + + def format_subscription_link(link: str | None = None): - return f"{link or '...'}" + return f"{link or texts.NOTHING_PLACEHOLDER}" def format_share_deep_link(link: str): diff --git a/services/user_service.py b/services/user_service.py index f7cc1fe..c03bde0 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -1,26 +1,20 @@ import logging -from aiogram import Bot -from aiogram.types import Message, ReactionTypeEmoji, User as TelegramUser +from aiogram.types import Message from remnawave import RemnawaveSDK from sqlalchemy.ext.asyncio import AsyncSession -from config import settings from db.models.tickets import TicketStatus from db.models.users import User -from keyboards.admins import ticket_actions from misc import utils from repositories import UserRepository from repositories.tickets import TicketRepository -from schemas.subscriptions import SubscriptionPlan -from schemas.tickets import NewTicketDTO from schemas.users import NewUserDTO, TicketCreator from services.rw import RWUserInfo, get_user_by_telegram_id from texts import ( - ADMIN_STANDARD_FALLBACK, - ADMIN_TICKET_WITH_RW, - ADMIN_TICKET_WITHOUT_RW, - SUPPORT_SIGNATURE, + NO_SUBSCRIPTION, + NOTHING_PLACEHOLDER, + SUBSCRIPTION_INFORMATION, TICKET_STATUS_EMOJIS, ) @@ -51,49 +45,29 @@ class UserService: ) @staticmethod - async def format_admin_support_message( - ticket_number: int, - user: TelegramUser, + def format_subscription_message( rw_user: RWUserInfo | None, - balance: int, - referal_id: int | None = None, + link: str | None = None, ): - username_display = utils.format_username(user.username) - referal_display = utils.format_referal_id(referal_id) - if rw_user is None: # Пользователь не найден в Remnawave - return ADMIN_TICKET_WITHOUT_RW.format( - ticket_number=ticket_number, - full_name=user.full_name, - username_display=username_display, - telegram_id=user.id, - referal_id=referal_display, - balance=balance, - ) + return NO_SUBSCRIPTION status_icon, status_text = utils.format_status(rw_user.status) expire_date = rw_user.expire_at.strftime("%d.%m.%Y") days_left = utils.format_days_left(rw_user.expire_at) used_traffic = utils.format_traffic(rw_user.used_traffic_bytes, rw_user.traffic_limit_bytes) hwid_limit = utils.format_hwid_limit(rw_user.hwid_device_limit) - squads = utils.format_squads(rw_user.active_squads) + link = link or NOTHING_PLACEHOLDER - return ADMIN_TICKET_WITH_RW.format( - ticket_number=ticket_number, - full_name=user.full_name, - username_display=username_display, - telegram_id=user.id, - balance=balance, + return SUBSCRIPTION_INFORMATION.format( status_icon=status_icon, status=status_text, expire_date=expire_date, days_left=days_left, used_traffic=used_traffic, hwid_limit=hwid_limit, - squads=squads, - remnawave_uuid=rw_user.uuid, - referal_id=referal_display, + link=link, ) async def add_user( @@ -114,154 +88,6 @@ class UserService: model = NewUserDTO(id=user_id, referal=referal, link=link) return await self.user_repository.create_user(session, model) - async def create_subscription_ticket( - self, - session: AsyncSession, - user: TelegramUser, - subscription: SubscriptionPlan, - bot: Bot, - ): - ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id) - db_user = await self.user_repository.get_user_by_id(session, user.id) - - if ticket: - try: - await bot.send_message( - chat_id=settings.admin_group_id, - message_thread_id=ticket.thread_id, - text=utils.format_support_subscription( - subscription=subscription, - full_name=user.full_name, - user_id=user.id, - username=user.username, - balance=db_user.balance, - referal_id=db_user.referal_id, - ), - reply_markup=ticket_actions, - ) - return - except Exception: - await self.ticket_repository.update_ticket_status( - session, ticket, TicketStatus.CLOSED - ) - raise - creator = TicketCreator(id=user.id, username=user.username) - - ticket_dto = NewTicketDTO( - creator_id=user.id, - ) - ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto) - - thread = await bot.create_forum_topic( - settings.admin_group_id, - self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING), - ) - ticket = await self.ticket_repository.update_ticket_thread_id( - session, ticket, thread.message_thread_id - ) - db_user = await self.user_repository.get_user_by_id(session, ticket.creator_id) - - await bot.send_message( - chat_id=settings.admin_group_id, - message_thread_id=ticket.thread_id, - text=utils.format_support_subscription( - subscription=subscription, - full_name=user.full_name, - user_id=user.id, - username=user.username, - balance=db_user.balance, - referal_id=db_user.referal_id, - ), - reply_markup=ticket_actions, - ) - - async def support_forward_message(self, session: AsyncSession, msg: Message, rw: RemnawaveSDK): - user = await self.user_repository.get_user_by_id(session, msg.from_user.id) - if not user: - raise UserServiceError("User %d is not found.", msg.from_user.id) - - ticket = await self.ticket_repository.get_available_ticket_by_user_id(session, user.id) - - if ticket: - try: - await msg.copy_to( - chat_id=settings.admin_group_id, message_thread_id=ticket.thread_id - ) - return - except Exception: - await self.ticket_repository.update_ticket_status( - session, ticket, TicketStatus.CLOSED - ) - raise - - rw_user = await get_user_by_telegram_id(rw, user.id) - creator = TicketCreator(id=user.id, username=msg.from_user.username) - - ticket_dto = NewTicketDTO( - creator_id=user.id, - ) - ticket = await self.ticket_repository.create_ticket(session, ticket_data=ticket_dto) - - thread = await msg.bot.create_forum_topic( - settings.admin_group_id, - self.format_ticket_name(creator, ticket_id=ticket.id, status=TicketStatus.PENDING), - ) - ticket = await self.ticket_repository.update_ticket_thread_id( - session, ticket, thread.message_thread_id - ) - - await msg.bot.send_message( - settings.admin_group_id, - message_thread_id=thread.message_thread_id, - text=await self.format_admin_support_message( - ticket_number=ticket.id, - user=msg.from_user, - rw_user=rw_user, - balance=user.balance, - referal_id=user.referal_id, - ), - reply_markup=ticket_actions, - ) - await msg.copy_to(settings.admin_group_id, message_thread_id=thread.message_thread_id) - - async def support_answer(self, session: AsyncSession, msg: Message): - thread_id = msg.message_thread_id - ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id) - - if ticket.status == TicketStatus.PENDING: - await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.OPEN) - await msg.bot.edit_forum_topic( - settings.admin_group_id, - ticket.thread_id, - name=self.format_ticket_name(TicketCreator(ticket.creator_id), ticket_id=ticket.id), - ) - - try: - await msg.bot.send_message(ticket.creator_id, SUPPORT_SIGNATURE) - await msg.copy_to(ticket.creator_id) - await msg.react([ReactionTypeEmoji(emoji="⚡")]) - except Exception: - await msg.reply(ADMIN_STANDARD_FALLBACK) - - async def close_ticket(self, session: AsyncSession, msg: Message): - thread_id = msg.message_thread_id - if not thread_id: - return - ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id) - - await msg.bot.edit_forum_topic( - settings.admin_group_id, - ticket.thread_id, - name=self.format_ticket_name( - TicketCreator(ticket.creator_id), - ticket_id=ticket.id, - status=TicketStatus.CLOSED, - ), - ) - - await self.ticket_repository.update_ticket_status(session, ticket, TicketStatus.CLOSED) - await msg.bot.close_forum_topic(settings.admin_group_id, message_thread_id=thread_id) - async def update_user_link( self, session: AsyncSession, user_id: int, rw_sdk: RemnawaveSDK ) -> User: @@ -278,16 +104,6 @@ class UserService: return user - async def fetch_user_by_thread(self, session: AsyncSession, thread_id: int) -> User | None: - ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id) - - return await self.user_repository.get_user_by_id(session, ticket.creator_id) - - async def fetch_referal_by_thread(self, session: AsyncSession, thread_id: int) -> User | None: - ticket = await self.ticket_repository.get_ticket_by_thread_id(session, thread_id=thread_id) - - return await self.user_repository.get_referal_by_user_id(session, ticket.creator_id) - async def send_promotion_to_users(self, session: AsyncSession, msg: Message) -> list[User]: users = await self.user_repository.get_users(session) diff --git a/texts.py b/texts.py index e010c21..103f9f4 100644 --- a/texts.py +++ b/texts.py @@ -23,6 +23,21 @@ MALENIA_LOCKED = PremiumEmoji(5276346642868508688, "🔒") MALENIA_SUPPORT = PremiumEmoji(5275968088746006467, "🎧") +# ============================================================ +# ШАБЛОНЫ — ОБЩИЕ БЛОКИ +# ============================================================ + +_SUPPORT_USER_DESCRIPTION = ( + "👤 Профиль: {full_name}\n" + "🔗 Alias: {username_display}\n" + "🆔 UID: {telegram_id}\n\n" + "❤️ Referal UID: {referal_id}\n\n" + "💰 Баланс: {balance}" +) + +_DIVIDER = "────────────────────────────\n\n" + + # ============================================================ # МАППИНГИ # ============================================================ @@ -46,7 +61,7 @@ STATUS_ICON_LIMITED = "🟠" STATUS_ICON_UNKNOWN = "❓" # — Тексты статусов -STATUS_ACTIVE = "работает" +STATUS_ACTIVE = "активна" STATUS_DISABLED = "отключена" STATUS_EXPIRED = "истёк" STATUS_LIMITED = "трафик ограничен" @@ -109,7 +124,9 @@ FAQ_TEXT = ( GENERAL_PROCESSING = "⏳ Подождите..." -STANDARD_FALLBACK = "❌ Ошибка. Прожмите /start. Если не поможет — значит, мы где-то не доглядели." +STANDARD_FALLBACK = ( + "❌ Ошибка. Прожмите /start. Если не поможет — значит, мы где-то не доглядели." +) CALLBACK_FALLBACK = "❌ Что-то пошло не так. Повторите попытку или обратитесь в поддержку." @@ -117,6 +134,18 @@ CONTEXT_REINITIATED = "🌑 Сессия сброшена. Данные могл UNDEFINED_MESSAGE = "❌ Таких команд у нас нет.\n\nИщете техподдержку? Вам в /support\nПотерялись? Вам в /start." +SUBSCRIPTION_INFORMATION = ( + "📊 Данные о подписке\n\n" + f"{MALENIA_LINK} Ссылка на подписку: {{link}}\n\n" + "{status_icon} Статус: {status}\n" + + "📅 Истекает: {expire_date}\n" + + "⏳ Осталось дней: {days_left}\n" + + "📊 Израсходовано трафика: {used_traffic}\n" + + "📱 Количество устройств: {hwid_limit}\n" +) + +NO_SUBSCRIPTION = "⚠️ Подписка не найдена." + # ============================================================ # РЕФЕРАЛЬНАЯ СИСТЕМА # ============================================================ @@ -134,50 +163,28 @@ REFERALS_MENU = ( # ОФОРМЛЕНИЕ ПОДПИСКИ # ============================================================ -SUBSCRIPTION_DEVICE_SELECTOR = ( - "Сколько устройств вы хотите подключить?\n\nЦена: {price}₽" -) +SUBSCRIPTION_DEVICE_SELECTOR = "Сколько устройств вы хотите подключить?\n\nЦена: {price}₽" -SUBSCRIPTION_DURATION_SELECTOR = "На какой срок хотите оформить подписку?\n\nИтого: {price}₽" +SUBSCRIPTION_DURATION_SELECTOR = ( + "На какой срок хотите оформить подписку?\n\nИтого: {price}₽ (скидка: {discount}%)" +) WHITELISTS_ALREADY_ON = "Белые списки уже активны." -CHECKOUT_PAYMENT_CHOICE = ( - "Пока что без автоплатежа. Нажмите кнопку ниже и обратитесь в поддержку для оплаты и активации подписки." -) +CHECKOUT_PAYMENT_CHOICE = "Пока что без автоплатежа. Нажмите кнопку ниже и обратитесь в поддержку для оплаты и активации подписки." # ============================================================ # ПОДДЕРЖКА # ============================================================ -SUPPORT_INIT = ( - f"{MALENIA_SUPPORT} Технический отдел\n" - "— если интернет превратился в тыкву или есть идеи по конфигам.\n" - "Пишите по делу, скриншоты приветствуются.\n\n" - "🌑 /cancel — вернуться в меню" -) +SUPPORT_INIT = "🎧 Тех. Поддержка Malenia — не здесь. Кнопка ниже." SUPPORT_MSG_SENT = "⏳ Приняли ваше сообщение. Мы постараемся ответить на него в ближайшее время.\nВернуться в меню: /cancel" SUPPORT_SIGNATURE = "📡 Ответ от Malenia:" -# ============================================================ -# ШАБЛОНЫ — ОБЩИЕ БЛОКИ -# ============================================================ - -_SUPPORT_USER_DESCRIPTION = ( - "👤 Профиль: {full_name}\n" - "🔗 Alias: {username_display}\n" - "🆔 UID: {telegram_id}\n\n" - "❤️ Referal UID: {referal_id}\n\n" - "💰 Баланс: {balance}" -) - -_DIVIDER = "────────────────────────────\n\n" - - # ============================================================ # ШАБЛОНЫ — ЗАЯВКИ И ТИКЕТЫ # ============================================================ @@ -214,7 +221,7 @@ ADMIN_TICKET_WITHOUT_RW = ( + _SUPPORT_USER_DESCRIPTION + "\n" + _DIVIDER - + "⚠️ Remnawave-профиль отсутствует" + + NO_SUBSCRIPTION ) ADMIN_STANDARD_FALLBACK = "❌ Ошибка выполнения. Проверь логи, что-то пошло не по чертежу."