diff --git a/app/bot.py b/app/bot.py
index d5815987..997b6ecb 100644
--- a/app/bot.py
+++ b/app/bot.py
@@ -44,7 +44,6 @@ from app.handlers.admin import (
bot_configuration as admin_bot_configuration,
pricing as admin_pricing,
privacy_policy as admin_privacy_policy,
- public_offer as admin_public_offer,
)
from app.handlers.stars_payments import register_stars_handlers
@@ -152,7 +151,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
admin_bot_configuration.register_handlers(dp)
admin_pricing.register_handlers(dp)
admin_privacy_policy.register_handlers(dp)
- admin_public_offer.register_handlers(dp)
common.register_handlers(dp)
register_stars_handlers(dp)
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
diff --git a/app/database/crud/public_offer.py b/app/database/crud/public_offer.py
deleted file mode 100644
index 78d5716e..00000000
--- a/app/database/crud/public_offer.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import logging
-from datetime import datetime
-from typing import Optional
-
-from sqlalchemy import select
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.database.models import PublicOffer
-
-logger = logging.getLogger(__name__)
-
-
-async def get_public_offer(db: AsyncSession, language: str) -> Optional[PublicOffer]:
- result = await db.execute(
- select(PublicOffer).where(PublicOffer.language == language)
- )
- return result.scalar_one_or_none()
-
-
-async def upsert_public_offer(
- db: AsyncSession,
- language: str,
- content: str,
- *,
- enable_if_new: bool = True,
-) -> PublicOffer:
- offer = await get_public_offer(db, language)
-
- if offer:
- offer.content = content or ""
- offer.updated_at = datetime.utcnow()
- else:
- offer = PublicOffer(
- language=language,
- content=content or "",
- is_enabled=True if enable_if_new else False,
- )
- db.add(offer)
-
- await db.commit()
- await db.refresh(offer)
-
- logger.info(
- "✅ Публичная оферта для языка %s обновлена (ID: %s)",
- language,
- offer.id,
- )
-
- return offer
-
-
-async def set_public_offer_enabled(
- db: AsyncSession,
- language: str,
- enabled: bool,
-) -> PublicOffer:
- offer = await get_public_offer(db, language)
-
- if offer:
- offer.is_enabled = bool(enabled)
- offer.updated_at = datetime.utcnow()
- else:
- offer = PublicOffer(
- language=language,
- content="",
- is_enabled=bool(enabled),
- )
- db.add(offer)
-
- await db.commit()
- await db.refresh(offer)
-
- logger.info(
- "✅ Статус публичной оферты для языка %s обновлен: %s",
- language,
- "enabled" if offer.is_enabled else "disabled",
- )
-
- return offer
diff --git a/app/database/models.py b/app/database/models.py
index 3536fecb..f48f2f47 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -771,17 +771,6 @@ class PrivacyPolicy(Base):
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
-class PublicOffer(Base):
- __tablename__ = "public_offers"
-
- id = Column(Integer, primary_key=True, index=True)
- language = Column(String(10), nullable=False, unique=True)
- content = Column(Text, nullable=False)
- is_enabled = Column(Boolean, default=True, nullable=False)
- created_at = Column(DateTime, default=func.now())
- updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
-
-
class SystemSetting(Base):
__tablename__ = "system_settings"
diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py
index 791d5d4d..a29a748f 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -2553,59 +2553,6 @@ async def create_privacy_policies_table() -> bool:
return False
-async def create_public_offers_table() -> bool:
- table_exists = await check_table_exists("public_offers")
- if table_exists:
- logger.info("ℹ️ Таблица public_offers уже существует")
- return True
-
- try:
- async with engine.begin() as conn:
- db_type = await get_database_type()
-
- if db_type == "sqlite":
- create_sql = """
- CREATE TABLE public_offers (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- language VARCHAR(10) NOT NULL UNIQUE,
- content TEXT NOT NULL,
- is_enabled BOOLEAN NOT NULL DEFAULT 1,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- """
- elif db_type == "postgresql":
- create_sql = """
- CREATE TABLE public_offers (
- id SERIAL PRIMARY KEY,
- language VARCHAR(10) NOT NULL UNIQUE,
- content TEXT NOT NULL,
- is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
- created_at TIMESTAMP DEFAULT NOW(),
- updated_at TIMESTAMP DEFAULT NOW()
- );
- """
- else:
- create_sql = """
- CREATE TABLE public_offers (
- id INT AUTO_INCREMENT PRIMARY KEY,
- language VARCHAR(10) NOT NULL UNIQUE,
- content TEXT NOT NULL,
- is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB;
- """
-
- await conn.execute(text(create_sql))
- logger.info("✅ Таблица public_offers создана")
- return True
-
- except Exception as error:
- logger.error(f"❌ Ошибка создания таблицы public_offers: {error}")
- return False
-
-
async def ensure_default_web_api_token() -> bool:
default_token = (settings.WEB_API_DEFAULT_TOKEN or "").strip()
if not default_token:
@@ -2695,13 +2642,6 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей privacy_policies")
- logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PUBLIC_OFFERS ===")
- public_offers_ready = await create_public_offers_table()
- if public_offers_ready:
- logger.info("✅ Таблица public_offers готова")
- else:
- logger.warning("⚠️ Проблемы с таблицей public_offers")
-
logger.info("=== ПРОВЕРКА БАЗОВЫХ ТОКЕНОВ ВЕБ-API ===")
default_token_ready = await ensure_default_web_api_token()
if default_token_ready:
@@ -2989,7 +2929,6 @@ async def check_migration_status():
"promo_groups_table": False,
"server_promo_groups_table": False,
"privacy_policies_table": False,
- "public_offers_table": False,
"users_promo_group_column": False,
"promo_groups_period_discounts_column": False,
"promo_groups_auto_assign_column": False,
@@ -3015,7 +2954,6 @@ async def check_migration_status():
status["user_messages_table"] = await check_table_exists('user_messages')
status["welcome_texts_table"] = await check_table_exists('welcome_texts')
status["privacy_policies_table"] = await check_table_exists('privacy_policies')
- status["public_offers_table"] = await check_table_exists('public_offers')
status["subscription_conversions_table"] = await check_table_exists('subscription_conversions')
status["promo_groups_table"] = await check_table_exists('promo_groups')
status["server_promo_groups_table"] = await check_table_exists('server_squad_promo_groups')
@@ -3066,7 +3004,6 @@ async def check_migration_status():
"user_messages_table": "Таблица пользовательских сообщений",
"welcome_texts_table": "Таблица приветственных текстов",
"privacy_policies_table": "Таблица политик конфиденциальности",
- "public_offers_table": "Таблица публичных оферт",
"welcome_texts_is_enabled_column": "Поле is_enabled в welcome_texts",
"broadcast_history_media_fields": "Медиа поля в broadcast_history",
"subscription_conversions_table": "Таблица конверсий подписок",
diff --git a/app/handlers/admin/public_offer.py b/app/handlers/admin/public_offer.py
deleted file mode 100644
index 4dbd3705..00000000
--- a/app/handlers/admin/public_offer.py
+++ /dev/null
@@ -1,517 +0,0 @@
-import html
-import logging
-from datetime import datetime
-
-from aiogram import Dispatcher, types, F
-from aiogram.fsm.context import FSMContext
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.database.models import User
-from app.localization.texts import get_texts
-from app.services.public_offer_service import PublicOfferService
-from app.states import AdminStates
-from app.utils.decorators import admin_required, error_handler
-from app.utils.validators import validate_html_tags, get_html_help_text
-
-logger = logging.getLogger(__name__)
-
-
-def _format_timestamp(value: datetime | None) -> str:
- if not value:
- return ""
- try:
- return value.strftime("%d.%m.%Y %H:%M")
- except Exception:
- return ""
-
-
-async def _build_overview(
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- offer = await PublicOfferService.get_offer(
- db,
- db_user.language,
- fallback=False,
- )
-
- normalized_language = PublicOfferService.normalize_language(db_user.language)
- has_content = bool(offer and offer.content and offer.content.strip())
-
- description = texts.t(
- "ADMIN_PUBLIC_OFFER_DESCRIPTION",
- "Публичная оферта отображается в разделе «Инфо».",
- )
-
- status_text = texts.t(
- "ADMIN_PUBLIC_OFFER_STATUS_DISABLED",
- "⚠️ Показ оферты выключен или текст отсутствует.",
- )
- if offer and offer.is_enabled and has_content:
- status_text = texts.t(
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED",
- "✅ Оферта активна и показывается пользователям.",
- )
- elif offer and offer.is_enabled:
- status_text = texts.t(
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED_EMPTY",
- "⚠️ Оферта включена, но текст пуст — пользователи её не увидят.",
- )
-
- updated_at = _format_timestamp(getattr(offer, "updated_at", None))
- updated_block = ""
- if updated_at:
- updated_block = texts.t(
- "ADMIN_PUBLIC_OFFER_UPDATED_AT",
- "Последнее обновление: {timestamp}",
- ).format(timestamp=updated_at)
-
- preview_block = texts.t(
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY",
- "Текст ещё не задан.",
- )
- if has_content:
- preview_title = texts.t(
- "ADMIN_PUBLIC_OFFER_PREVIEW_TITLE",
- "Превью текста:",
- )
- preview_raw = offer.content.strip()
- preview_trimmed = preview_raw[:400]
- if len(preview_raw) > 400:
- preview_trimmed += "..."
- preview_block = (
- f"{preview_title}\n"
- f"{html.escape(preview_trimmed)}"
- )
-
- language_block = texts.t(
- "ADMIN_PUBLIC_OFFER_LANGUAGE",
- "Язык: {lang}",
- ).format(lang=normalized_language)
-
- header = texts.t(
- "ADMIN_PUBLIC_OFFER_HEADER",
- "📄 Публичная оферта",
- )
- actions_prompt = texts.t(
- "ADMIN_PUBLIC_OFFER_ACTION_PROMPT",
- "Выберите действие:",
- )
-
- message_parts = [
- header,
- description,
- language_block,
- status_text,
- ]
-
- if updated_block:
- message_parts.append(updated_block)
-
- message_parts.append(preview_block)
- message_parts.append(actions_prompt)
-
- overview_text = "\n\n".join(part for part in message_parts if part)
-
- buttons: list[list[types.InlineKeyboardButton]] = []
-
- buttons.append([
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_EDIT_BUTTON",
- "✏️ Изменить текст",
- ),
- callback_data="admin_public_offer_edit",
- )
- ])
-
- if has_content:
- buttons.append([
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_VIEW_BUTTON",
- "👀 Просмотреть текущий текст",
- ),
- callback_data="admin_public_offer_view",
- )
- ])
-
- toggle_text = texts.t(
- "ADMIN_PUBLIC_OFFER_ENABLE_BUTTON",
- "✅ Включить показ",
- )
- if offer and offer.is_enabled:
- toggle_text = texts.t(
- "ADMIN_PUBLIC_OFFER_DISABLE_BUTTON",
- "🚫 Отключить показ",
- )
-
- buttons.append([
- types.InlineKeyboardButton(
- text=toggle_text,
- callback_data="admin_public_offer_toggle",
- )
- ])
-
- buttons.append([
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_HTML_HELP",
- "ℹ️ HTML помощь",
- ),
- callback_data="admin_public_offer_help",
- )
- ])
-
- buttons.append([
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data="admin_submenu_settings",
- )
- ])
-
- return overview_text, types.InlineKeyboardMarkup(inline_keyboard=buttons), offer
-
-
-@admin_required
-@error_handler
-async def show_public_offer_management(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- overview_text, markup, _ = await _build_overview(db_user, db)
-
- await callback.message.edit_text(
- overview_text,
- reply_markup=markup,
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def toggle_public_offer(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- updated_offer = await PublicOfferService.toggle_enabled(db, db_user.language)
- logger.info(
- "Админ %s переключил показ публичной оферты: %s",
- db_user.telegram_id,
- "enabled" if updated_offer.is_enabled else "disabled",
- )
- status_message = (
- texts.t("ADMIN_PUBLIC_OFFER_ENABLED", "✅ Оферта включена")
- if updated_offer.is_enabled
- else texts.t("ADMIN_PUBLIC_OFFER_DISABLED", "🚫 Оферта отключена")
- )
-
- overview_text, markup, _ = await _build_overview(db_user, db)
- await callback.message.edit_text(
- overview_text,
- reply_markup=markup,
- )
- await callback.answer(status_message)
-
-
-@admin_required
-@error_handler
-async def start_edit_public_offer(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
-
- offer = await PublicOfferService.get_offer(
- db,
- db_user.language,
- fallback=False,
- )
-
- current_preview = ""
- if offer and offer.content:
- preview = offer.content.strip()[:400]
- if len(offer.content.strip()) > 400:
- preview += "..."
- current_preview = (
- texts.t(
- "ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW",
- "Текущий текст (превью):",
- )
- + f"\n{html.escape(preview)}\n\n"
- )
-
- prompt = texts.t(
- "ADMIN_PUBLIC_OFFER_EDIT_PROMPT",
- "Отправьте новый текст публичной оферты. Допускается HTML-разметка.",
- )
-
- hint = texts.t(
- "ADMIN_PUBLIC_OFFER_EDIT_HINT",
- "Используйте /html_help для справки по тегам.",
- )
-
- message_text = (
- f"📝 {texts.t('ADMIN_PUBLIC_OFFER_EDIT_TITLE', 'Редактирование оферты')}\n\n"
- f"{current_preview}{prompt}\n\n{hint}"
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_HTML_HELP",
- "ℹ️ HTML помощь",
- ),
- callback_data="admin_public_offer_help",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_PUBLIC_OFFER_CANCEL", "❌ Отмена"),
- callback_data="admin_public_offer_cancel",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(message_text, reply_markup=keyboard)
- await state.set_state(AdminStates.editing_public_offer)
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def cancel_edit_public_offer(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
- db: AsyncSession,
-):
- await state.clear()
- overview_text, markup, _ = await _build_overview(db_user, db)
- await callback.message.edit_text(
- overview_text,
- reply_markup=markup,
- )
- await callback.answer(
- get_texts(db_user.language).t(
- "ADMIN_PUBLIC_OFFER_EDIT_CANCELLED",
- "Редактирование оферты отменено.",
- )
- )
-
-
-@admin_required
-@error_handler
-async def process_public_offer_edit(
- message: types.Message,
- db_user: User,
- state: FSMContext,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- new_text = message.text or ""
-
- if len(new_text) > 4000:
- await message.answer(
- texts.t(
- "ADMIN_PUBLIC_OFFER_TOO_LONG",
- "❌ Текст оферты слишком длинный. Максимум 4000 символов.",
- )
- )
- return
-
- is_valid, error_message = validate_html_tags(new_text)
- if not is_valid:
- await message.answer(
- texts.t(
- "ADMIN_PUBLIC_OFFER_HTML_ERROR",
- "❌ Ошибка в HTML: {error}",
- ).format(error=error_message)
- )
- return
-
- await PublicOfferService.save_offer(db, db_user.language, new_text)
- logger.info(
- "Админ %s обновил текст публичной оферты (%d символов)",
- db_user.telegram_id,
- len(new_text),
- )
- await state.clear()
-
- success_text = texts.t(
- "ADMIN_PUBLIC_OFFER_SAVED",
- "✅ Публичная оферта обновлена.",
- )
-
- reply_markup = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON",
- "⬅️ К настройкам оферты",
- ),
- callback_data="admin_public_offer",
- )
- ]
- ]
- )
-
- await message.answer(success_text, reply_markup=reply_markup)
-
-
-@admin_required
-@error_handler
-async def view_public_offer(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- offer = await PublicOfferService.get_offer(
- db,
- db_user.language,
- fallback=False,
- )
-
- if not offer or not offer.content or not offer.content.strip():
- await callback.answer(
- texts.t(
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY_ALERT",
- "Текст оферты пока не задан.",
- ),
- show_alert=True,
- )
- return
-
- content = offer.content.strip()
- truncated = False
- max_length = 3800
- if len(content) > max_length:
- content = content[: max_length - 3] + "..."
- truncated = True
-
- header = texts.t(
- "ADMIN_PUBLIC_OFFER_VIEW_TITLE",
- "👀 Текущий текст оферты",
- )
-
- note = ""
- if truncated:
- note = texts.t(
- "ADMIN_PUBLIC_OFFER_VIEW_TRUNCATED",
- "\n\n⚠️ Текст сокращён для отображения. Полную версию увидят пользователи в меню.",
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON",
- "⬅️ К настройкам оферты",
- ),
- callback_data="admin_public_offer",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_EDIT_BUTTON",
- "✏️ Изменить текст",
- ),
- callback_data="admin_public_offer_edit",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(
- f"{header}\n\n{content}{note}",
- reply_markup=keyboard,
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def show_public_offer_html_help(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- help_text = get_html_help_text()
-
- current_state = await state.get_state()
-
- buttons: list[list[types.InlineKeyboardButton]] = []
-
- if current_state == AdminStates.editing_public_offer.state:
- buttons.append([
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_RETURN_TO_EDIT",
- "⬅️ Назад к редактированию",
- ),
- callback_data="admin_public_offer_edit",
- )
- ])
-
- buttons.append([
- types.InlineKeyboardButton(
- text=texts.t(
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON",
- "⬅️ К настройкам оферты",
- ),
- callback_data="admin_public_offer",
- )
- ])
-
- await callback.message.edit_text(
- help_text,
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=buttons),
- )
- await callback.answer()
-
-
-def register_handlers(dp: Dispatcher) -> None:
- dp.callback_query.register(
- show_public_offer_management,
- F.data == "admin_public_offer",
- )
- dp.callback_query.register(
- toggle_public_offer,
- F.data == "admin_public_offer_toggle",
- )
- dp.callback_query.register(
- start_edit_public_offer,
- F.data == "admin_public_offer_edit",
- )
- dp.callback_query.register(
- cancel_edit_public_offer,
- F.data == "admin_public_offer_cancel",
- )
- dp.callback_query.register(
- view_public_offer,
- F.data == "admin_public_offer_view",
- )
- dp.callback_query.register(
- show_public_offer_html_help,
- F.data == "admin_public_offer_help",
- )
-
- dp.message.register(
- process_public_offer_edit,
- AdminStates.editing_public_offer,
- )
diff --git a/app/handlers/menu.py b/app/handlers/menu.py
index a15d42c2..fab18d5b 100644
--- a/app/handlers/menu.py
+++ b/app/handlers/menu.py
@@ -26,7 +26,6 @@ from app.utils.promo_offer import (
build_test_access_hint,
)
from app.services.privacy_policy_service import PrivacyPolicyService
-from app.services.public_offer_service import PublicOfferService
logger = logging.getLogger(__name__)
@@ -113,7 +112,6 @@ async def show_info_menu(
caption = f"{header}\n\n{prompt}" if prompt else header
privacy_enabled = await PrivacyPolicyService.is_policy_enabled(db, db_user.language)
- public_offer_enabled = await PublicOfferService.is_offer_enabled(db, db_user.language)
await edit_or_answer_photo(
callback=callback,
@@ -121,7 +119,6 @@ async def show_info_menu(
keyboard=get_info_menu_keyboard(
language=db_user.language,
show_privacy_policy=privacy_enabled,
- show_public_offer=public_offer_enabled,
),
parse_mode="HTML",
)
@@ -235,113 +232,6 @@ async def show_privacy_policy(
await callback.answer()
-async def show_public_offer(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
-
- raw_page = 1
- if callback.data and ":" in callback.data:
- try:
- raw_page = int(callback.data.split(":", 1)[1])
- except ValueError:
- raw_page = 1
-
- if raw_page < 1:
- raw_page = 1
-
- offer = await PublicOfferService.get_active_offer(db, db_user.language)
-
- if not offer:
- await callback.answer(
- texts.t(
- "PUBLIC_OFFER_NOT_AVAILABLE",
- "Публичная оферта временно недоступна.",
- ),
- show_alert=True,
- )
- return
-
- pages = PublicOfferService.split_content_into_pages(offer.content)
-
- if not pages:
- await callback.answer(
- texts.t(
- "PUBLIC_OFFER_EMPTY_ALERT",
- "Публичная оферта ещё не заполнена.",
- ),
- show_alert=True,
- )
- return
-
- total_pages = len(pages)
- current_page = raw_page if raw_page <= total_pages else total_pages
-
- header = texts.t(
- "PUBLIC_OFFER_HEADER",
- "📄 Публичная оферта",
- )
- body = pages[current_page - 1]
-
- footer_template = texts.t(
- "PUBLIC_OFFER_PAGE_INFO",
- "Страница {current} из {total}",
- )
- footer = ""
- if total_pages > 1 and footer_template:
- try:
- footer = footer_template.format(current=current_page, total=total_pages)
- except Exception:
- footer = f"{current_page}/{total_pages}"
-
- message_text = header
- if body:
- message_text += f"\n\n{body}"
- if footer:
- message_text += f"\n\n{footer}"
-
- keyboard_rows: list[list[types.InlineKeyboardButton]] = []
-
- if total_pages > 1:
- nav_row: list[types.InlineKeyboardButton] = []
- if current_page > 1:
- nav_row.append(
- types.InlineKeyboardButton(
- text=texts.t("PAGINATION_PREV", "⬅️"),
- callback_data=f"menu_public_offer:{current_page - 1}",
- )
- )
-
- nav_row.append(
- types.InlineKeyboardButton(
- text=f"{current_page}/{total_pages}",
- callback_data="noop",
- )
- )
-
- if current_page < total_pages:
- nav_row.append(
- types.InlineKeyboardButton(
- text=texts.t("PAGINATION_NEXT", "➡️"),
- callback_data=f"menu_public_offer:{current_page + 1}",
- )
- )
-
- keyboard_rows.append(nav_row)
-
- keyboard_rows.append(
- [types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_info")]
- )
-
- await callback.message.edit_text(
- message_text,
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
- )
- await callback.answer()
-
-
async def show_language_menu(
callback: types.CallbackQuery,
db_user: User,
@@ -621,16 +511,6 @@ def register_handlers(dp: Dispatcher):
F.data.startswith("menu_privacy_policy:"),
)
- dp.callback_query.register(
- show_public_offer,
- F.data == "menu_public_offer",
- )
-
- dp.callback_query.register(
- show_public_offer,
- F.data.startswith("menu_public_offer:"),
- )
-
dp.callback_query.register(
show_language_menu,
F.data == "menu_language"
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index 2a59970b..72a66fbd 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -184,12 +184,6 @@ def get_admin_settings_submenu_keyboard(language: str = "ru") -> InlineKeyboardM
callback_data="admin_privacy_policy",
)
],
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_SETTINGS_PUBLIC_OFFER", "📄 Публичная оферта"),
- callback_data="admin_public_offer",
- )
- ],
[
InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")
]
diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py
index 15624917..5bfa995b 100644
--- a/app/keyboards/inline.py
+++ b/app/keyboards/inline.py
@@ -283,7 +283,6 @@ def get_main_menu_keyboard(
def get_info_menu_keyboard(
language: str = DEFAULT_LANGUAGE,
show_privacy_policy: bool = False,
- show_public_offer: bool = False,
) -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -297,14 +296,6 @@ def get_info_menu_keyboard(
)
])
- if show_public_offer:
- buttons.append([
- InlineKeyboardButton(
- text=texts.t("MENU_PUBLIC_OFFER", "📄 Оферта"),
- callback_data="menu_public_offer",
- )
- ])
-
buttons.append([
InlineKeyboardButton(text=texts.MENU_RULES, callback_data="menu_rules")
])
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index ffa15770..35328586 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -720,42 +720,5 @@
"ADMIN_SUPPORT_EDIT_DESCRIPTION_CONTACT_HINT": "Add to the description if needed.",
"ADMIN_SUPPORT_DESCRIPTION_UPDATED": "✅ Description updated.",
"ADMIN_SUPPORT_DESCRIPTION_SENT": "Description sent below",
- "ADMIN_SUPPORT_MESSAGE_DELETED": "Message deleted",
- "MENU_PUBLIC_OFFER": "📄 Offer",
- "PUBLIC_OFFER_NOT_AVAILABLE": "Public offer is temporarily unavailable.",
- "PUBLIC_OFFER_EMPTY_ALERT": "Public offer content is not provided yet.",
- "PUBLIC_OFFER_HEADER": "📄 Public Offer",
- "PUBLIC_OFFER_PAGE_INFO": "Page {current} of {total}",
- "ADMIN_SETTINGS_PUBLIC_OFFER": "📄 Public offer",
- "ADMIN_PUBLIC_OFFER_HEADER": "📄 Public offer",
- "ADMIN_PUBLIC_OFFER_DESCRIPTION": "The public offer is shown in the “Info” section.",
- "ADMIN_PUBLIC_OFFER_LANGUAGE": "Language: {lang}",
- "ADMIN_PUBLIC_OFFER_STATUS_DISABLED": "⚠️ Offer display is disabled or empty.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED": "✅ Offer is active and visible to users.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED_EMPTY": "⚠️ Offer is enabled but text is empty — users will not see it.",
- "ADMIN_PUBLIC_OFFER_UPDATED_AT": "Last updated: {timestamp}",
- "ADMIN_PUBLIC_OFFER_PREVIEW_TITLE": "Text preview:",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY": "Text is not set yet.",
- "ADMIN_PUBLIC_OFFER_ACTION_PROMPT": "Choose an action:",
- "ADMIN_PUBLIC_OFFER_EDIT_BUTTON": "✏️ Edit text",
- "ADMIN_PUBLIC_OFFER_VIEW_BUTTON": "👀 View current text",
- "ADMIN_PUBLIC_OFFER_ENABLE_BUTTON": "✅ Enable display",
- "ADMIN_PUBLIC_OFFER_DISABLE_BUTTON": "🚫 Disable display",
- "ADMIN_PUBLIC_OFFER_HTML_HELP": "ℹ️ HTML help",
- "ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW": "Current text (preview):",
- "ADMIN_PUBLIC_OFFER_EDIT_PROMPT": "Send a new public offer text. HTML markup is allowed.",
- "ADMIN_PUBLIC_OFFER_EDIT_HINT": "Use /html_help for the list of allowed tags.",
- "ADMIN_PUBLIC_OFFER_EDIT_TITLE": "Public offer editing",
- "ADMIN_PUBLIC_OFFER_CANCEL": "❌ Cancel",
- "ADMIN_PUBLIC_OFFER_TOO_LONG": "❌ Offer text is too long. Maximum 4000 characters.",
- "ADMIN_PUBLIC_OFFER_HTML_ERROR": "❌ HTML error: {error}",
- "ADMIN_PUBLIC_OFFER_SAVED": "✅ Public offer updated.",
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON": "⬅️ Back to offer settings",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY_ALERT": "Offer text is not set yet.",
- "ADMIN_PUBLIC_OFFER_VIEW_TITLE": "👀 Current offer text",
- "ADMIN_PUBLIC_OFFER_VIEW_TRUNCATED": "\n\n⚠️ Text shortened for display. Users will see the full version in the menu.",
- "ADMIN_PUBLIC_OFFER_ENABLED": "✅ Offer enabled",
- "ADMIN_PUBLIC_OFFER_DISABLED": "🚫 Offer disabled",
- "ADMIN_PUBLIC_OFFER_RETURN_TO_EDIT": "⬅️ Back to editing",
- "ADMIN_PUBLIC_OFFER_EDIT_CANCELLED": "Offer editing cancelled."
+ "ADMIN_SUPPORT_MESSAGE_DELETED": "Message deleted"
}
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index 06d7a051..f66baaec 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -235,15 +235,10 @@
"MENU_INFO_HEADER": "ℹ️ Инфо",
"MENU_INFO_PROMPT": "Выберите раздел:",
"MENU_PRIVACY_POLICY": "🛡️ Политика конф.",
- "MENU_PUBLIC_OFFER": "📄 Оферта",
"PRIVACY_POLICY_HEADER": "🛡️ Политика конфиденциальности",
"PRIVACY_POLICY_NOT_AVAILABLE": "Политика конфиденциальности временно недоступна.",
"PRIVACY_POLICY_EMPTY_ALERT": "Политика конфиденциальности ещё не заполнена.",
"PRIVACY_POLICY_PAGE_INFO": "Страница {current} из {total}",
- "PUBLIC_OFFER_NOT_AVAILABLE": "Публичная оферта временно недоступна.",
- "PUBLIC_OFFER_EMPTY_ALERT": "Публичная оферта ещё не заполнена.",
- "PUBLIC_OFFER_HEADER": "📄 Публичная оферта",
- "PUBLIC_OFFER_PAGE_INFO": "Страница {current} из {total}",
"MENU_LANGUAGE": "🌐 Язык",
"MENU_PROMOCODE": "🎫 Промокод",
"MENU_REFERRALS": "🤝 Партнерка",
@@ -724,38 +719,6 @@
"ADMIN_PRIVACY_POLICY_ENABLED": "✅ Политика включена",
"ADMIN_PRIVACY_POLICY_DISABLED": "🚫 Политика отключена",
"ADMIN_PRIVACY_POLICY_RETURN_TO_EDIT": "⬅️ Назад к редактированию",
- "ADMIN_SETTINGS_PUBLIC_OFFER": "📄 Публичная оферта",
- "ADMIN_PUBLIC_OFFER_HEADER": "📄 Публичная оферта",
- "ADMIN_PUBLIC_OFFER_DESCRIPTION": "Публичная оферта отображается в разделе «Инфо».",
- "ADMIN_PUBLIC_OFFER_LANGUAGE": "Язык: {lang}",
- "ADMIN_PUBLIC_OFFER_STATUS_DISABLED": "⚠️ Показ оферты выключен или текст отсутствует.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED": "✅ Оферта активна и показывается пользователям.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED_EMPTY": "⚠️ Оферта включена, но текст пуст — пользователи её не увидят.",
- "ADMIN_PUBLIC_OFFER_UPDATED_AT": "Последнее обновление: {timestamp}",
- "ADMIN_PUBLIC_OFFER_PREVIEW_TITLE": "Превью текста:",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY": "Текст ещё не задан.",
- "ADMIN_PUBLIC_OFFER_ACTION_PROMPT": "Выберите действие:",
- "ADMIN_PUBLIC_OFFER_EDIT_BUTTON": "✏️ Изменить текст",
- "ADMIN_PUBLIC_OFFER_VIEW_BUTTON": "👀 Просмотреть текущий текст",
- "ADMIN_PUBLIC_OFFER_ENABLE_BUTTON": "✅ Включить показ",
- "ADMIN_PUBLIC_OFFER_DISABLE_BUTTON": "🚫 Отключить показ",
- "ADMIN_PUBLIC_OFFER_HTML_HELP": "ℹ️ HTML помощь",
- "ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW": "Текущий текст (превью):",
- "ADMIN_PUBLIC_OFFER_EDIT_PROMPT": "Отправьте новый текст публичной оферты. Допускается HTML-разметка.",
- "ADMIN_PUBLIC_OFFER_EDIT_HINT": "Используйте /html_help для справки по тегам.",
- "ADMIN_PUBLIC_OFFER_EDIT_TITLE": "Редактирование оферты",
- "ADMIN_PUBLIC_OFFER_CANCEL": "❌ Отмена",
- "ADMIN_PUBLIC_OFFER_TOO_LONG": "❌ Текст оферты слишком длинный. Максимум 4000 символов.",
- "ADMIN_PUBLIC_OFFER_HTML_ERROR": "❌ Ошибка в HTML: {error}",
- "ADMIN_PUBLIC_OFFER_SAVED": "✅ Публичная оферта обновлена.",
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON": "⬅️ К настройкам оферты",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY_ALERT": "Текст оферты пока не задан.",
- "ADMIN_PUBLIC_OFFER_VIEW_TITLE": "👀 Текущий текст оферты",
- "ADMIN_PUBLIC_OFFER_VIEW_TRUNCATED": "\n\n⚠️ Текст сокращён для отображения. Полную версию увидят пользователи в меню.",
- "ADMIN_PUBLIC_OFFER_ENABLED": "✅ Оферта включена",
- "ADMIN_PUBLIC_OFFER_DISABLED": "🚫 Оферта отключена",
- "ADMIN_PUBLIC_OFFER_RETURN_TO_EDIT": "⬅️ Назад к редактированию",
- "ADMIN_PUBLIC_OFFER_EDIT_CANCELLED": "Редактирование оферты отменено.",
"ADMIN_SYSTEM_SUBMENU_TITLE": "🛠️ **Системные функции**\n\n",
"ADMIN_SYSTEM_SUBMENU_DESCRIPTION": "Отчеты, обновления, логи, резервные копии и системные операции:",
"ADMIN_SUPPORT_SETTINGS_STATUS_ENABLED": "Включены",
diff --git a/app/services/public_offer_service.py b/app/services/public_offer_service.py
deleted file mode 100644
index 5b5fdc45..00000000
--- a/app/services/public_offer_service.py
+++ /dev/null
@@ -1,359 +0,0 @@
-import logging
-from html.parser import HTMLParser
-from typing import List, Optional, Tuple
-
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.config import settings
-from app.database.crud.public_offer import (
- get_public_offer,
- set_public_offer_enabled,
- upsert_public_offer,
-)
-from app.database.models import PublicOffer
-
-logger = logging.getLogger(__name__)
-
-
-class PublicOfferService:
- """Helpers for managing the public offer text and visibility."""
-
- MAX_PAGE_LENGTH = 3500
-
- @staticmethod
- def _normalize_language(language: str) -> str:
- base_language = language or settings.DEFAULT_LANGUAGE or "ru"
- return base_language.split("-")[0].lower()
-
- @staticmethod
- def normalize_language(language: str) -> str:
- return PublicOfferService._normalize_language(language)
-
- @classmethod
- async def get_offer(
- cls,
- db: AsyncSession,
- language: str,
- *,
- fallback: bool = False,
- ) -> Optional[PublicOffer]:
- lang = cls._normalize_language(language)
- offer = await get_public_offer(db, lang)
-
- if offer or not fallback:
- return offer
-
- default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
- if lang != default_lang:
- return await get_public_offer(db, default_lang)
-
- return offer
-
- @classmethod
- async def get_active_offer(
- cls,
- db: AsyncSession,
- language: str,
- ) -> Optional[PublicOffer]:
- lang = cls._normalize_language(language)
- offer = await get_public_offer(db, lang)
-
- if offer:
- if offer.is_enabled and offer.content.strip():
- return offer
-
- if not offer.is_enabled:
- return None
-
- default_lang = cls._normalize_language(settings.DEFAULT_LANGUAGE)
- if lang != default_lang:
- fallback_offer = await get_public_offer(db, default_lang)
- if fallback_offer and fallback_offer.is_enabled and fallback_offer.content.strip():
- return fallback_offer
-
- return None
-
- @classmethod
- async def is_offer_enabled(cls, db: AsyncSession, language: str) -> bool:
- offer = await cls.get_active_offer(db, language)
- return offer is not None
-
- @classmethod
- async def save_offer(
- cls,
- db: AsyncSession,
- language: str,
- content: str,
- ) -> PublicOffer:
- lang = cls._normalize_language(language)
- enable_if_new = True
- offer = await upsert_public_offer(
- db,
- lang,
- content,
- enable_if_new=enable_if_new,
- )
- logger.info("✅ Публичная оферта обновлена для языка %s", lang)
- return offer
-
- @classmethod
- async def set_enabled(
- cls,
- db: AsyncSession,
- language: str,
- enabled: bool,
- ) -> PublicOffer:
- lang = cls._normalize_language(language)
- return await set_public_offer_enabled(db, lang, enabled)
-
- @classmethod
- async def toggle_enabled(
- cls,
- db: AsyncSession,
- language: str,
- ) -> PublicOffer:
- lang = cls._normalize_language(language)
- offer = await get_public_offer(db, lang)
-
- if offer:
- new_status = not offer.is_enabled
- else:
- new_status = True
-
- return await set_public_offer_enabled(db, lang, new_status)
-
- class _RichTextPaginator(HTMLParser):
- """Split HTML-like text into Telegram-safe chunks."""
-
- SELF_CLOSING_TAGS = {"br", "hr", "img", "input", "meta", "link"}
-
- def __init__(self, max_len: int) -> None:
- super().__init__()
- self.max_len = max_len
- self.pages: List[str] = []
- self.current_parts: List[str] = []
- self.current_length = 0
- self.open_stack: List[Tuple[str, str, int]] = []
- self.closing_length = 0
- self.needs_prefix = False
- self.prefix_length = 0
-
- def _closing_sequence(self) -> str:
- return "".join(f"{name}>" for name, _, _ in reversed(self.open_stack))
-
- def _ensure_prefix(self) -> None:
- if not self.needs_prefix:
- return
-
- prefix = "".join(token for _, token, _ in self.open_stack)
- if prefix:
- self.current_parts.append(prefix)
- self.current_length += len(prefix)
- self.needs_prefix = False
-
- def _flush(self) -> None:
- if not self.current_parts and not self.closing_length:
- return
-
- content = "".join(self.current_parts)
- closing_tags = self._closing_sequence()
- page = (content + closing_tags).strip()
- if page:
- self.pages.append(page)
-
- self.current_parts = []
- self.current_length = 0
- if self.prefix_length + self.closing_length > self.max_len:
- self.open_stack = []
- self.closing_length = 0
- self.prefix_length = 0
- self.needs_prefix = False
- else:
- self.needs_prefix = bool(self.open_stack)
-
- @staticmethod
- def _format_attrs(attrs: List[Tuple[str, Optional[str]]]) -> str:
- parts = []
- for name, value in attrs:
- if value is None:
- parts.append(f" {name}")
- else:
- parts.append(f" {name}=\"{value}\"")
- return "".join(parts)
-
- def _append_token(self, token: str) -> None:
- while True:
- self._ensure_prefix()
- if self.current_length + len(token) + self.closing_length <= self.max_len:
- self.current_parts.append(token)
- self.current_length += len(token)
- break
-
- self._flush()
-
- if len(token) > self.max_len and not self.current_parts:
- self.current_parts.append(token)
- self.current_length += len(token)
- break
-
- def handle_data(self, data: str) -> None:
- if not data:
- return
-
- remaining = data
- while remaining:
- self._ensure_prefix()
- available = self.max_len - (self.current_length + self.closing_length)
- if available <= 0:
- self._flush()
- continue
-
- piece = remaining[:available]
- self.current_parts.append(piece)
- self.current_length += len(piece)
- remaining = remaining[available:]
-
- if remaining:
- self._flush()
-
- def handle_entityref(self, name: str) -> None:
- self.handle_data(f"&{name};")
-
- def handle_charref(self, name: str) -> None:
- self.handle_data(f"{name};")
-
- def _handle_self_closing(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
- token = f"<{tag}{self._format_attrs(attrs)}/>"
- self._append_token(token)
-
- def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
- if tag in self.SELF_CLOSING_TAGS:
- self._handle_self_closing(tag, attrs)
- return
-
- token = f"<{tag}{self._format_attrs(attrs)}>"
- closing_token = f"{tag}>"
- closing_len = len(closing_token)
-
- while True:
- self._ensure_prefix()
- projected_length = self.current_length + len(token) + self.closing_length + closing_len
- if projected_length <= self.max_len:
- self.current_parts.append(token)
- self.current_length += len(token)
- self.open_stack.append((tag, token, closing_len))
- self.closing_length += closing_len
- self.prefix_length += len(token)
- break
-
- self._flush()
-
- if len(token) + closing_len > self.max_len and not self.current_parts:
- self.current_parts.append(token)
- self.current_length += len(token)
- self.open_stack.append((tag, token, closing_len))
- self.closing_length += closing_len
- self.prefix_length += len(token)
- break
-
- def handle_endtag(self, tag: str) -> None:
- token = f"{tag}>"
- closing_len_reduction = 0
- index_to_remove = None
- for index in range(len(self.open_stack) - 1, -1, -1):
- if self.open_stack[index][0] == tag:
- closing_len_reduction = self.open_stack[index][2]
- index_to_remove = index
- break
-
- while True:
- self._ensure_prefix()
- projected_closing_length = self.closing_length - closing_len_reduction
- if projected_closing_length < 0:
- projected_closing_length = 0
-
- projected_total = self.current_length + len(token) + projected_closing_length
- if projected_total <= self.max_len or not self.current_parts:
- self.current_parts.append(token)
- self.current_length += len(token)
- if index_to_remove is not None:
- removed_tag = self.open_stack.pop(index_to_remove)
- self.closing_length -= closing_len_reduction
- self.prefix_length -= len(removed_tag[1])
- break
-
- self._flush()
-
- def handle_startendtag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
- self._handle_self_closing(tag, attrs)
-
- def finalize(self) -> List[str]:
- self._flush()
- return self.pages
-
- @classmethod
- def _split_rich_paragraph(cls, paragraph: str, max_len: int) -> List[str]:
- if len(paragraph) <= max_len:
- return [paragraph]
-
- paginator = cls._RichTextPaginator(max_len)
- paginator.feed(paragraph)
- paginator.close()
- pages = paginator.finalize()
- return pages or [paragraph]
-
- @classmethod
- def split_content_into_pages(
- cls,
- content: str,
- *,
- max_length: int = None,
- ) -> List[str]:
- if not content:
- return []
-
- normalized = content.replace("\r\n", "\n").strip()
- if not normalized:
- return []
-
- max_len = max_length or cls.MAX_PAGE_LENGTH
-
- if len(normalized) <= max_len:
- return [normalized]
-
- paragraphs = [
- paragraph.strip()
- for paragraph in normalized.split("\n\n")
- if paragraph.strip()
- ]
-
- pages: List[str] = []
- current = ""
-
- def flush_current() -> None:
- nonlocal current
- if current:
- pages.append(current.strip())
- current = ""
-
- for paragraph in paragraphs:
- segments = cls._split_rich_paragraph(paragraph, max_len)
- for segment in segments:
- segment = segment.strip()
- if not segment:
- continue
-
- candidate = f"{current}\n\n{segment}".strip() if current else segment
- if len(candidate) <= max_len:
- current = candidate
- continue
-
- flush_current()
- current = segment
-
- flush_current()
-
- if not pages:
- return [normalized[:max_len]]
-
- return pages
diff --git a/app/states.py b/app/states.py
index 7527313b..1e42df99 100644
--- a/app/states.py
+++ b/app/states.py
@@ -88,7 +88,6 @@ class AdminStates(StatesGroup):
editing_rules_page = State()
editing_privacy_policy = State()
- editing_public_offer = State()
editing_notification_value = State()
confirming_sync = State()
diff --git a/locales/en.json b/locales/en.json
index 8869e22e..54cf63dd 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1052,42 +1052,5 @@
"ADMIN_PRICING_SETTING_PROMPT": "Send a new value or type \"Cancel\". Use none to clear.",
"ADMIN_PRICING_SETTING_SUCCESS": "Parameter {label} updated: {value}",
"ADMIN_PRICING_SETTING_TOGGLE_STATEFUL": "{icon} {label}",
- "ADMIN_PRICING_SETTING_WARNING": "Important",
- "MENU_PUBLIC_OFFER": "📄 Offer",
- "PUBLIC_OFFER_NOT_AVAILABLE": "Public offer is temporarily unavailable.",
- "PUBLIC_OFFER_EMPTY_ALERT": "Public offer content is not provided yet.",
- "PUBLIC_OFFER_HEADER": "📄 Public Offer",
- "PUBLIC_OFFER_PAGE_INFO": "Page {current} of {total}",
- "ADMIN_SETTINGS_PUBLIC_OFFER": "📄 Public offer",
- "ADMIN_PUBLIC_OFFER_HEADER": "📄 Public offer",
- "ADMIN_PUBLIC_OFFER_DESCRIPTION": "The public offer is shown in the “Info” section.",
- "ADMIN_PUBLIC_OFFER_LANGUAGE": "Language: {lang}",
- "ADMIN_PUBLIC_OFFER_STATUS_DISABLED": "⚠️ Offer display is disabled or empty.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED": "✅ Offer is active and visible to users.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED_EMPTY": "⚠️ Offer is enabled but text is empty — users will not see it.",
- "ADMIN_PUBLIC_OFFER_UPDATED_AT": "Last updated: {timestamp}",
- "ADMIN_PUBLIC_OFFER_PREVIEW_TITLE": "Text preview:",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY": "Text is not set yet.",
- "ADMIN_PUBLIC_OFFER_ACTION_PROMPT": "Choose an action:",
- "ADMIN_PUBLIC_OFFER_EDIT_BUTTON": "✏️ Edit text",
- "ADMIN_PUBLIC_OFFER_VIEW_BUTTON": "👀 View current text",
- "ADMIN_PUBLIC_OFFER_ENABLE_BUTTON": "✅ Enable display",
- "ADMIN_PUBLIC_OFFER_DISABLE_BUTTON": "🚫 Disable display",
- "ADMIN_PUBLIC_OFFER_HTML_HELP": "ℹ️ HTML help",
- "ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW": "Current text (preview):",
- "ADMIN_PUBLIC_OFFER_EDIT_PROMPT": "Send a new public offer text. HTML markup is allowed.",
- "ADMIN_PUBLIC_OFFER_EDIT_HINT": "Use /html_help for the list of allowed tags.",
- "ADMIN_PUBLIC_OFFER_EDIT_TITLE": "Public offer editing",
- "ADMIN_PUBLIC_OFFER_CANCEL": "❌ Cancel",
- "ADMIN_PUBLIC_OFFER_TOO_LONG": "❌ Offer text is too long. Maximum 4000 characters.",
- "ADMIN_PUBLIC_OFFER_HTML_ERROR": "❌ HTML error: {error}",
- "ADMIN_PUBLIC_OFFER_SAVED": "✅ Public offer updated.",
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON": "⬅️ Back to offer settings",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY_ALERT": "Offer text is not set yet.",
- "ADMIN_PUBLIC_OFFER_VIEW_TITLE": "👀 Current offer text",
- "ADMIN_PUBLIC_OFFER_VIEW_TRUNCATED": "\n\n⚠️ Text shortened for display. Users will see the full version in the menu.",
- "ADMIN_PUBLIC_OFFER_ENABLED": "✅ Offer enabled",
- "ADMIN_PUBLIC_OFFER_DISABLED": "🚫 Offer disabled",
- "ADMIN_PUBLIC_OFFER_RETURN_TO_EDIT": "⬅️ Back to editing",
- "ADMIN_PUBLIC_OFFER_EDIT_CANCELLED": "Offer editing cancelled."
+ "ADMIN_PRICING_SETTING_WARNING": "Important"
}
diff --git a/locales/ru.json b/locales/ru.json
index 868f9e4f..32eba83d 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -1016,42 +1016,5 @@
"ADMIN_PRICING_SETTING_PROMPT": "Отправьте новое значение или напишите «Отмена». Для очистки используйте none.",
"ADMIN_PRICING_SETTING_SUCCESS": "Параметр {label} обновлен: {value}",
"ADMIN_PRICING_SETTING_TOGGLE_STATEFUL": "{icon} {label}",
- "ADMIN_PRICING_SETTING_WARNING": "Важно",
- "MENU_PUBLIC_OFFER": "📄 Оферта",
- "PUBLIC_OFFER_NOT_AVAILABLE": "Публичная оферта временно недоступна.",
- "PUBLIC_OFFER_EMPTY_ALERT": "Публичная оферта ещё не заполнена.",
- "PUBLIC_OFFER_HEADER": "📄 Публичная оферта",
- "PUBLIC_OFFER_PAGE_INFO": "Страница {current} из {total}",
- "ADMIN_SETTINGS_PUBLIC_OFFER": "📄 Публичная оферта",
- "ADMIN_PUBLIC_OFFER_HEADER": "📄 Публичная оферта",
- "ADMIN_PUBLIC_OFFER_DESCRIPTION": "Публичная оферта отображается в разделе «Инфо».",
- "ADMIN_PUBLIC_OFFER_LANGUAGE": "Язык: {lang}",
- "ADMIN_PUBLIC_OFFER_STATUS_DISABLED": "⚠️ Показ оферты выключен или текст отсутствует.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED": "✅ Оферта активна и показывается пользователям.",
- "ADMIN_PUBLIC_OFFER_STATUS_ENABLED_EMPTY": "⚠️ Оферта включена, но текст пуст — пользователи её не увидят.",
- "ADMIN_PUBLIC_OFFER_UPDATED_AT": "Последнее обновление: {timestamp}",
- "ADMIN_PUBLIC_OFFER_PREVIEW_TITLE": "Превью текста:",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY": "Текст ещё не задан.",
- "ADMIN_PUBLIC_OFFER_ACTION_PROMPT": "Выберите действие:",
- "ADMIN_PUBLIC_OFFER_EDIT_BUTTON": "✏️ Изменить текст",
- "ADMIN_PUBLIC_OFFER_VIEW_BUTTON": "👀 Просмотреть текущий текст",
- "ADMIN_PUBLIC_OFFER_ENABLE_BUTTON": "✅ Включить показ",
- "ADMIN_PUBLIC_OFFER_DISABLE_BUTTON": "🚫 Отключить показ",
- "ADMIN_PUBLIC_OFFER_HTML_HELP": "ℹ️ HTML помощь",
- "ADMIN_PUBLIC_OFFER_CURRENT_PREVIEW": "Текущий текст (превью):",
- "ADMIN_PUBLIC_OFFER_EDIT_PROMPT": "Отправьте новый текст публичной оферты. Допускается HTML-разметка.",
- "ADMIN_PUBLIC_OFFER_EDIT_HINT": "Используйте /html_help для справки по тегам.",
- "ADMIN_PUBLIC_OFFER_EDIT_TITLE": "Редактирование оферты",
- "ADMIN_PUBLIC_OFFER_CANCEL": "❌ Отмена",
- "ADMIN_PUBLIC_OFFER_TOO_LONG": "❌ Текст оферты слишком длинный. Максимум 4000 символов.",
- "ADMIN_PUBLIC_OFFER_HTML_ERROR": "❌ Ошибка в HTML: {error}",
- "ADMIN_PUBLIC_OFFER_SAVED": "✅ Публичная оферта обновлена.",
- "ADMIN_PUBLIC_OFFER_BACK_BUTTON": "⬅️ К настройкам оферты",
- "ADMIN_PUBLIC_OFFER_PREVIEW_EMPTY_ALERT": "Текст оферты пока не задан.",
- "ADMIN_PUBLIC_OFFER_VIEW_TITLE": "👀 Текущий текст оферты",
- "ADMIN_PUBLIC_OFFER_VIEW_TRUNCATED": "\n\n⚠️ Текст сокращён для отображения. Полную версию увидят пользователи в меню.",
- "ADMIN_PUBLIC_OFFER_ENABLED": "✅ Оферта включена",
- "ADMIN_PUBLIC_OFFER_DISABLED": "🚫 Оферта отключена",
- "ADMIN_PUBLIC_OFFER_RETURN_TO_EDIT": "⬅️ Назад к редактированию",
- "ADMIN_PUBLIC_OFFER_EDIT_CANCELLED": "Редактирование оферты отменено."
+ "ADMIN_PRICING_SETTING_WARNING": "Важно"
}