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"" 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"" - 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"" - 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": "Важно" }