diff --git a/app/database/crud/discount_offer.py b/app/database/crud/discount_offer.py deleted file mode 100644 index eaa789ae..00000000 --- a/app/database/crud/discount_offer.py +++ /dev/null @@ -1,90 +0,0 @@ -from datetime import datetime, timedelta -from typing import Optional - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.database.models import DiscountOffer - - -async def upsert_discount_offer( - db: AsyncSession, - *, - user_id: int, - subscription_id: Optional[int], - notification_type: str, - discount_percent: int, - bonus_amount_kopeks: int, - valid_hours: int, -) -> DiscountOffer: - """Create or refresh a discount offer for a user.""" - - expires_at = datetime.utcnow() + timedelta(hours=valid_hours) - - result = await db.execute( - select(DiscountOffer) - .where( - DiscountOffer.user_id == user_id, - DiscountOffer.notification_type == notification_type, - DiscountOffer.is_active == True, # noqa: E712 - ) - .order_by(DiscountOffer.created_at.desc()) - ) - offer = result.scalars().first() - - if offer and offer.claimed_at is None: - offer.discount_percent = discount_percent - offer.bonus_amount_kopeks = bonus_amount_kopeks - offer.expires_at = expires_at - offer.subscription_id = subscription_id - else: - offer = DiscountOffer( - user_id=user_id, - subscription_id=subscription_id, - notification_type=notification_type, - discount_percent=discount_percent, - bonus_amount_kopeks=bonus_amount_kopeks, - expires_at=expires_at, - is_active=True, - ) - db.add(offer) - - await db.commit() - await db.refresh(offer) - return offer - - -async def get_offer_by_id(db: AsyncSession, offer_id: int) -> Optional[DiscountOffer]: - result = await db.execute( - select(DiscountOffer).where(DiscountOffer.id == offer_id) - ) - return result.scalar_one_or_none() - - -async def mark_offer_claimed(db: AsyncSession, offer: DiscountOffer) -> DiscountOffer: - offer.claimed_at = datetime.utcnow() - offer.is_active = False - await db.commit() - await db.refresh(offer) - return offer - - -async def deactivate_expired_offers(db: AsyncSession) -> int: - now = datetime.utcnow() - result = await db.execute( - select(DiscountOffer).where( - DiscountOffer.is_active == True, # noqa: E712 - DiscountOffer.expires_at < now, - ) - ) - offers = result.scalars().all() - if not offers: - return 0 - - count = 0 - for offer in offers: - offer.is_active = False - count += 1 - - await db.commit() - return count diff --git a/app/database/models.py b/app/database/models.py index 91a7a360..f9b6d8ab 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -14,7 +14,6 @@ from sqlalchemy import ( JSON, BigInteger, UniqueConstraint, - Index, ) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship, Mapped, mapped_column @@ -359,7 +358,6 @@ class User(Base): subscription = relationship("Subscription", back_populates="user", uselist=False) transactions = relationship("Transaction", back_populates="user") referral_earnings = relationship("ReferralEarning", foreign_keys="ReferralEarning.user_id", back_populates="user") - discount_offers = relationship("DiscountOffer", back_populates="user") lifetime_used_traffic_bytes = Column(BigInteger, default=0) auto_promo_group_assigned = Column(Boolean, nullable=False, default=False) last_remnawave_sync = Column(DateTime, nullable=True) @@ -422,9 +420,8 @@ class Subscription(Base): updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) remnawave_short_uuid = Column(String(255), nullable=True) - + user = relationship("User", back_populates="subscription") - discount_offers = relationship("DiscountOffer", back_populates="subscription") @property def is_active(self) -> bool: @@ -768,28 +765,6 @@ class SentNotification(Base): user = relationship("User", backref="sent_notifications") subscription = relationship("Subscription", backref="sent_notifications") - -class DiscountOffer(Base): - __tablename__ = "discount_offers" - __table_args__ = ( - Index("ix_discount_offers_user_type", "user_id", "notification_type"), - ) - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) - subscription_id = Column(Integer, ForeignKey("subscriptions.id", ondelete="SET NULL"), nullable=True) - notification_type = Column(String(50), nullable=False) - discount_percent = Column(Integer, nullable=False, default=0) - bonus_amount_kopeks = Column(Integer, nullable=False, default=0) - expires_at = Column(DateTime, nullable=False) - claimed_at = Column(DateTime, nullable=True) - is_active = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - - user = relationship("User", back_populates="discount_offers") - subscription = relationship("Subscription", back_populates="discount_offers") - class BroadcastHistory(Base): __tablename__ = "broadcast_history" diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index 522747f0..40273ff4 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -520,94 +520,6 @@ async def create_pal24_payments_table(): logger.error(f"Ошибка создания таблицы pal24_payments: {e}") return False - -async def create_discount_offers_table(): - table_exists = await check_table_exists('discount_offers') - if table_exists: - logger.info("Таблица discount_offers уже существует") - return True - - try: - async with engine.begin() as conn: - db_type = await get_database_type() - - if db_type == 'sqlite': - await conn.execute(text(""" - CREATE TABLE discount_offers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - subscription_id INTEGER NULL, - notification_type VARCHAR(50) NOT NULL, - discount_percent INTEGER NOT NULL DEFAULT 0, - bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0, - expires_at DATETIME NOT NULL, - claimed_at DATETIME NULL, - is_active BOOLEAN NOT NULL DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY(subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL - ) - """)) - await conn.execute(text(""" - CREATE INDEX IF NOT EXISTS ix_discount_offers_user_type - ON discount_offers (user_id, notification_type) - """)) - - elif db_type == 'postgresql': - await conn.execute(text(""" - CREATE TABLE IF NOT EXISTS discount_offers ( - id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - subscription_id INTEGER NULL REFERENCES subscriptions(id) ON DELETE SET NULL, - notification_type VARCHAR(50) NOT NULL, - discount_percent INTEGER NOT NULL DEFAULT 0, - bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0, - expires_at TIMESTAMP NOT NULL, - claimed_at TIMESTAMP NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """)) - await conn.execute(text(""" - CREATE INDEX IF NOT EXISTS ix_discount_offers_user_type - ON discount_offers (user_id, notification_type) - """)) - - elif db_type == 'mysql': - await conn.execute(text(""" - CREATE TABLE IF NOT EXISTS discount_offers ( - id INTEGER PRIMARY KEY AUTO_INCREMENT, - user_id INTEGER NOT NULL, - subscription_id INTEGER NULL, - notification_type VARCHAR(50) NOT NULL, - discount_percent INTEGER NOT NULL DEFAULT 0, - bonus_amount_kopeks INTEGER NOT NULL DEFAULT 0, - expires_at DATETIME NOT NULL, - claimed_at DATETIME NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_discount_offers_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, - CONSTRAINT fk_discount_offers_subscription FOREIGN KEY(subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL - ) - """)) - await conn.execute(text(""" - CREATE INDEX ix_discount_offers_user_type - ON discount_offers (user_id, notification_type) - """)) - - else: - raise ValueError(f"Unsupported database type: {db_type}") - - logger.info("✅ Таблица discount_offers успешно создана") - return True - - except Exception as e: - logger.error(f"Ошибка создания таблицы discount_offers: {e}") - return False - async def create_user_messages_table(): table_exists = await check_table_exists('user_messages') if table_exists: @@ -1555,13 +1467,6 @@ async def run_universal_migration(): else: logger.warning("⚠️ Проблемы с таблицей Pal24 payments") - logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ DISCOUNT_OFFERS ===") - discount_created = await create_discount_offers_table() - if discount_created: - logger.info("✅ Таблица discount_offers готова") - else: - logger.warning("⚠️ Проблемы с таблицей discount_offers") - logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ USER_MESSAGES ===") user_messages_created = await create_user_messages_table() if user_messages_created: diff --git a/app/handlers/admin/monitoring.py b/app/handlers/admin/monitoring.py index 2c1066b4..be876876 100644 --- a/app/handlers/admin/monitoring.py +++ b/app/handlers/admin/monitoring.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.filters import Command -from aiogram.fsm.context import FSMContext from app.config import settings from app.database.database import get_db @@ -13,77 +12,11 @@ from app.utils.decorators import admin_required from app.utils.pagination import paginate_list from app.keyboards.admin import get_monitoring_keyboard, get_admin_main_keyboard from app.localization.texts import get_texts -from app.services.notification_settings_service import NotificationSettingsService -from app.states import AdminStates logger = logging.getLogger(__name__) router = Router() -def _format_toggle(enabled: bool) -> str: - return "🟢 Вкл" if enabled else "🔴 Выкл" - - -def _build_notification_settings_view(language: str): - texts = get_texts(language) - config = NotificationSettingsService.get_config() - - second_percent = NotificationSettingsService.get_second_wave_discount_percent() - second_hours = NotificationSettingsService.get_second_wave_valid_hours() - third_percent = NotificationSettingsService.get_third_wave_discount_percent() - third_hours = NotificationSettingsService.get_third_wave_valid_hours() - third_days = NotificationSettingsService.get_third_wave_trigger_days() - - trial_1h_status = _format_toggle(config["trial_inactive_1h"].get("enabled", True)) - trial_24h_status = _format_toggle(config["trial_inactive_24h"].get("enabled", True)) - expired_1d_status = _format_toggle(config["expired_1d"].get("enabled", True)) - second_wave_status = _format_toggle(config["expired_second_wave"].get("enabled", True)) - third_wave_status = _format_toggle(config["expired_third_wave"].get("enabled", True)) - - summary_text = ( - "🔔 Уведомления пользователям\n\n" - f"• 1 час после триала: {trial_1h_status}\n" - f"• 24 часа после триала: {trial_24h_status}\n" - f"• 1 день после истечения: {expired_1d_status}\n" - f"• 2-3 дня (скидка {second_percent}% / {second_hours} ч): {second_wave_status}\n" - f"• {third_days} дней (скидка {third_percent}% / {third_hours} ч): {third_wave_status}" - ) - - from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton - - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text=f"{trial_1h_status} • 1 час после триала", callback_data="admin_mon_notify_toggle_trial_1h")], - [InlineKeyboardButton(text=f"{trial_24h_status} • 24 часа после триала", callback_data="admin_mon_notify_toggle_trial_24h")], - [InlineKeyboardButton(text=f"{expired_1d_status} • 1 день после истечения", callback_data="admin_mon_notify_toggle_expired_1d")], - [InlineKeyboardButton(text=f"{second_wave_status} • 2-3 дня со скидкой", callback_data="admin_mon_notify_toggle_expired_2d")], - [InlineKeyboardButton(text=f"✏️ Скидка 2-3 дня: {second_percent}%", callback_data="admin_mon_notify_edit_2d_percent")], - [InlineKeyboardButton(text=f"⏱️ Срок скидки 2-3 дня: {second_hours} ч", callback_data="admin_mon_notify_edit_2d_hours")], - [InlineKeyboardButton(text=f"{third_wave_status} • {third_days} дней со скидкой", callback_data="admin_mon_notify_toggle_expired_nd")], - [InlineKeyboardButton(text=f"✏️ Скидка {third_days} дней: {third_percent}%", callback_data="admin_mon_notify_edit_nd_percent")], - [InlineKeyboardButton(text=f"⏱️ Срок скидки {third_days} дней: {third_hours} ч", callback_data="admin_mon_notify_edit_nd_hours")], - [InlineKeyboardButton(text=f"📆 Порог уведомления: {third_days} дн.", callback_data="admin_mon_notify_edit_nd_threshold")], - [InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_mon_settings")], - ]) - - return summary_text, keyboard - - -async def _render_notification_settings(callback: CallbackQuery) -> None: - language = (callback.from_user.language_code or settings.DEFAULT_LANGUAGE) - text, keyboard = _build_notification_settings_view(language) - await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard) - - -async def _render_notification_settings_for_state(bot, chat_id: int, message_id: int, language: str) -> None: - text, keyboard = _build_notification_settings_view(language) - await bot.edit_message_text( - text, - chat_id, - message_id, - parse_mode="HTML", - reply_markup=keyboard, - ) - @router.callback_query(F.data == "admin_monitoring") @admin_required async def admin_monitoring_menu(callback: CallbackQuery): @@ -119,180 +52,6 @@ async def admin_monitoring_menu(callback: CallbackQuery): await callback.answer("❌ Ошибка получения данных", show_alert=True) -@router.callback_query(F.data == "admin_mon_settings") -@admin_required -async def admin_monitoring_settings(callback: CallbackQuery): - try: - language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE - global_status = "🟢 Включены" if NotificationSettingsService.are_notifications_globally_enabled() else "🔴 Отключены" - second_percent = NotificationSettingsService.get_second_wave_discount_percent() - third_percent = NotificationSettingsService.get_third_wave_discount_percent() - third_days = NotificationSettingsService.get_third_wave_trigger_days() - - text = ( - "⚙️ Настройки мониторинга\n\n" - f"🔔 Уведомления пользователям: {global_status}\n" - f"• Скидка 2-3 дня: {second_percent}%\n" - f"• Скидка после {third_days} дней: {third_percent}%\n\n" - "Выберите раздел для настройки." - ) - - from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton - - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🔔 Уведомления пользователям", callback_data="admin_mon_notify_settings")], - [InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")], - ]) - - await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard) - - except Exception as e: - logger.error(f"Ошибка отображения настроек мониторинга: {e}") - await callback.answer("❌ Не удалось открыть настройки", show_alert=True) - - -@router.callback_query(F.data == "admin_mon_notify_settings") -@admin_required -async def admin_notify_settings(callback: CallbackQuery): - try: - await _render_notification_settings(callback) - except Exception as e: - logger.error(f"Ошибка отображения настроек уведомлений: {e}") - await callback.answer("❌ Не удалось загрузить настройки", show_alert=True) - - -@router.callback_query(F.data == "admin_mon_notify_toggle_trial_1h") -@admin_required -async def toggle_trial_1h_notification(callback: CallbackQuery): - enabled = NotificationSettingsService.is_trial_inactive_1h_enabled() - NotificationSettingsService.set_trial_inactive_1h_enabled(not enabled) - await callback.answer("✅ Включено" if not enabled else "⏸️ Отключено") - await _render_notification_settings(callback) - - -@router.callback_query(F.data == "admin_mon_notify_toggle_trial_24h") -@admin_required -async def toggle_trial_24h_notification(callback: CallbackQuery): - enabled = NotificationSettingsService.is_trial_inactive_24h_enabled() - NotificationSettingsService.set_trial_inactive_24h_enabled(not enabled) - await callback.answer("✅ Включено" if not enabled else "⏸️ Отключено") - await _render_notification_settings(callback) - - -@router.callback_query(F.data == "admin_mon_notify_toggle_expired_1d") -@admin_required -async def toggle_expired_1d_notification(callback: CallbackQuery): - enabled = NotificationSettingsService.is_expired_1d_enabled() - NotificationSettingsService.set_expired_1d_enabled(not enabled) - await callback.answer("✅ Включено" if not enabled else "⏸️ Отключено") - await _render_notification_settings(callback) - - -@router.callback_query(F.data == "admin_mon_notify_toggle_expired_2d") -@admin_required -async def toggle_second_wave_notification(callback: CallbackQuery): - enabled = NotificationSettingsService.is_second_wave_enabled() - NotificationSettingsService.set_second_wave_enabled(not enabled) - await callback.answer("✅ Включено" if not enabled else "⏸️ Отключено") - await _render_notification_settings(callback) - - -@router.callback_query(F.data == "admin_mon_notify_toggle_expired_nd") -@admin_required -async def toggle_third_wave_notification(callback: CallbackQuery): - enabled = NotificationSettingsService.is_third_wave_enabled() - NotificationSettingsService.set_third_wave_enabled(not enabled) - await callback.answer("✅ Включено" if not enabled else "⏸️ Отключено") - await _render_notification_settings(callback) - - -async def _start_notification_value_edit( - callback: CallbackQuery, - state: FSMContext, - setting_key: str, - field: str, - prompt_key: str, - default_prompt: str, -): - language = callback.from_user.language_code or settings.DEFAULT_LANGUAGE - await state.set_state(AdminStates.editing_notification_value) - await state.update_data( - notification_setting_key=setting_key, - notification_setting_field=field, - settings_message_chat=callback.message.chat.id, - settings_message_id=callback.message.message_id, - settings_language=language, - ) - texts = get_texts(language) - await callback.answer() - await callback.message.answer(texts.get(prompt_key, default_prompt)) - - -@router.callback_query(F.data == "admin_mon_notify_edit_2d_percent") -@admin_required -async def edit_second_wave_percent(callback: CallbackQuery, state: FSMContext): - await _start_notification_value_edit( - callback, - state, - "expired_second_wave", - "percent", - "NOTIFY_PROMPT_SECOND_PERCENT", - "Введите новый процент скидки для уведомления через 2-3 дня (0-100):", - ) - - -@router.callback_query(F.data == "admin_mon_notify_edit_2d_hours") -@admin_required -async def edit_second_wave_hours(callback: CallbackQuery, state: FSMContext): - await _start_notification_value_edit( - callback, - state, - "expired_second_wave", - "hours", - "NOTIFY_PROMPT_SECOND_HOURS", - "Введите количество часов действия скидки (1-168):", - ) - - -@router.callback_query(F.data == "admin_mon_notify_edit_nd_percent") -@admin_required -async def edit_third_wave_percent(callback: CallbackQuery, state: FSMContext): - await _start_notification_value_edit( - callback, - state, - "expired_third_wave", - "percent", - "NOTIFY_PROMPT_THIRD_PERCENT", - "Введите новый процент скидки для позднего предложения (0-100):", - ) - - -@router.callback_query(F.data == "admin_mon_notify_edit_nd_hours") -@admin_required -async def edit_third_wave_hours(callback: CallbackQuery, state: FSMContext): - await _start_notification_value_edit( - callback, - state, - "expired_third_wave", - "hours", - "NOTIFY_PROMPT_THIRD_HOURS", - "Введите количество часов действия скидки (1-168):", - ) - - -@router.callback_query(F.data == "admin_mon_notify_edit_nd_threshold") -@admin_required -async def edit_third_wave_threshold(callback: CallbackQuery, state: FSMContext): - await _start_notification_value_edit( - callback, - state, - "expired_third_wave", - "trigger", - "NOTIFY_PROMPT_THIRD_DAYS", - "Через сколько дней после истечения отправлять предложение? (минимум 2):", - ) - - @router.callback_query(F.data == "admin_mon_start") @admin_required async def start_monitoring_callback(callback: CallbackQuery): @@ -607,53 +366,5 @@ async def monitoring_command(message: Message): await message.answer(f"❌ Ошибка: {str(e)}") -@router.message(AdminStates.editing_notification_value) -async def process_notification_value_input(message: Message, state: FSMContext): - data = await state.get_data() - if not data: - await state.clear() - await message.answer("ℹ️ Контекст утерян, попробуйте снова из меню настроек.") - return - - raw_value = (message.text or "").strip() - try: - value = int(raw_value) - except (TypeError, ValueError): - language = data.get("settings_language") or message.from_user.language_code or settings.DEFAULT_LANGUAGE - texts = get_texts(language) - await message.answer(texts.get("NOTIFICATION_VALUE_INVALID", "❌ Введите целое число.")) - return - - key = data.get("notification_setting_key") - field = data.get("notification_setting_field") - language = data.get("settings_language") or message.from_user.language_code or settings.DEFAULT_LANGUAGE - texts = get_texts(language) - - success = False - if key == "expired_second_wave" and field == "percent": - success = NotificationSettingsService.set_second_wave_discount_percent(value) - elif key == "expired_second_wave" and field == "hours": - success = NotificationSettingsService.set_second_wave_valid_hours(value) - elif key == "expired_third_wave" and field == "percent": - success = NotificationSettingsService.set_third_wave_discount_percent(value) - elif key == "expired_third_wave" and field == "hours": - success = NotificationSettingsService.set_third_wave_valid_hours(value) - elif key == "expired_third_wave" and field == "trigger": - success = NotificationSettingsService.set_third_wave_trigger_days(value) - - if not success: - await message.answer(texts.get("NOTIFICATION_VALUE_INVALID", "❌ Некорректное значение, попробуйте снова.")) - return - - await message.answer(texts.get("NOTIFICATION_VALUE_UPDATED", "✅ Настройки обновлены.")) - - chat_id = data.get("settings_message_chat") - message_id = data.get("settings_message_id") - if chat_id and message_id: - await _render_notification_settings_for_state(message.bot, chat_id, message_id, language) - - await state.clear() - - def register_handlers(dp): dp.include_router(router) \ No newline at end of file diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py index 3eeee497..3f0c182a 100644 --- a/app/handlers/subscription.py +++ b/app/handlers/subscription.py @@ -17,13 +17,12 @@ from app.database.crud.subscription import ( add_subscription_squad, update_subscription_autopay, add_subscription_servers ) -from app.database.crud.user import subtract_user_balance, add_user_balance +from app.database.crud.user import subtract_user_balance from app.database.crud.transaction import create_transaction, get_user_transactions from app.database.models import ( - User, TransactionType, SubscriptionStatus, - SubscriptionServer, Subscription + User, TransactionType, SubscriptionStatus, + SubscriptionServer, Subscription ) -from app.database.crud.discount_offer import get_offer_by_id, mark_offer_claimed from app.keyboards.inline import ( get_subscription_keyboard, get_trial_keyboard, get_subscription_period_keyboard, get_traffic_packages_keyboard, @@ -4069,76 +4068,6 @@ async def handle_connect_subscription( await callback.answer() -async def claim_discount_offer( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession, -): - texts = get_texts(db_user.language) - - try: - offer_id = int(callback.data.split("_")[-1]) - except (ValueError, AttributeError): - await callback.answer( - texts.get("DISCOUNT_CLAIM_NOT_FOUND", "❌ Предложение не найдено"), - show_alert=True, - ) - return - - offer = await get_offer_by_id(db, offer_id) - if not offer or offer.user_id != db_user.id: - await callback.answer( - texts.get("DISCOUNT_CLAIM_NOT_FOUND", "❌ Предложение не найдено"), - show_alert=True, - ) - return - - now = datetime.utcnow() - if offer.claimed_at is not None: - await callback.answer( - texts.get("DISCOUNT_CLAIM_ALREADY", "ℹ️ Скидка уже была активирована"), - show_alert=True, - ) - return - - if not offer.is_active or offer.expires_at <= now: - offer.is_active = False - await db.commit() - await callback.answer( - texts.get("DISCOUNT_CLAIM_EXPIRED", "⚠️ Время действия предложения истекло"), - show_alert=True, - ) - return - - bonus_amount = offer.bonus_amount_kopeks or 0 - if bonus_amount > 0: - success = await add_user_balance( - db, - db_user, - bonus_amount, - texts.get("DISCOUNT_BONUS_DESCRIPTION", "Скидка за продление подписки"), - ) - if not success: - await callback.answer( - texts.get("DISCOUNT_CLAIM_ERROR", "❌ Не удалось начислить скидку. Попробуйте позже."), - show_alert=True, - ) - return - - await mark_offer_claimed(db, offer) - - success_message = texts.get( - "DISCOUNT_CLAIM_SUCCESS", - "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.", - ).format( - percent=offer.discount_percent, - amount=settings.format_price(bonus_amount), - ) - - await callback.answer("✅ Скидка активирована!", show_alert=True) - await callback.message.answer(success_message) - - async def handle_device_guide( callback: types.CallbackQuery, db_user: User, @@ -5034,11 +4963,6 @@ def register_handlers(dp: Dispatcher): F.data == "countries_apply" ) - dp.callback_query.register( - claim_discount_offer, - F.data.startswith("claim_discount_") - ) - dp.callback_query.register( handle_connect_subscription, F.data == "subscription_connect" diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index c6dba961..8219147b 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -93,17 +93,14 @@ def get_admin_support_submenu_keyboard(language: str = "ru") -> InlineKeyboardMa def get_admin_settings_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup: texts = get_texts(language) - + return InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton(text=texts.ADMIN_REMNAWAVE, callback_data="admin_remnawave"), InlineKeyboardButton(text=texts.ADMIN_MONITORING, callback_data="admin_monitoring") ], [ - InlineKeyboardButton(text=texts.t("ADMIN_MONITORING_SETTINGS", "🔔 Настройки уведомлений"), callback_data="admin_mon_settings"), - InlineKeyboardButton(text=texts.ADMIN_RULES, callback_data="admin_rules") - ], - [ + InlineKeyboardButton(text=texts.ADMIN_RULES, callback_data="admin_rules"), InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel") ], [ @@ -785,9 +782,6 @@ def get_monitoring_keyboard() -> InlineKeyboardMarkup: InlineKeyboardButton(text="🧪 Тест уведомлений", callback_data="admin_mon_test_notifications"), InlineKeyboardButton(text="📊 Статистика", callback_data="admin_mon_statistics") ], - [ - InlineKeyboardButton(text="⚙️ Настройки уведомлений", callback_data="admin_mon_settings") - ], [ InlineKeyboardButton(text="⬅️ Назад в админку", callback_data="admin_panel") ] diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index 337e18f8..a190aec4 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -21,15 +21,10 @@ from app.database.crud.notification import ( notification_sent, record_notification, ) -from app.database.crud.discount_offer import ( - upsert_discount_offer, - deactivate_expired_offers, -) from app.database.models import MonitoringLog, SubscriptionStatus, Subscription, User, Ticket, TicketStatus from app.services.subscription_service import SubscriptionService from app.services.payment_service import PaymentService from app.localization.texts import get_texts -from app.services.notification_settings_service import NotificationSettingsService from app.external.remnawave_api import ( RemnaWaveUser, UserStatus, TrafficLimitStrategy, RemnaWaveAPIError @@ -85,16 +80,10 @@ class MonitoringService: async for db in get_db(): try: await self._cleanup_notification_cache() - - expired_offers = await deactivate_expired_offers(db) - if expired_offers: - logger.info(f"🧹 Деактивировано {expired_offers} просроченных скидочных предложений") - + await self._check_expired_subscriptions(db) await self._check_expiring_subscriptions(db) - await self._check_trial_expiring_soon(db) - await self._check_trial_inactivity_notifications(db) - await self._check_expired_subscription_followups(db) + await self._check_trial_expiring_soon(db) await self._process_autopayments(db) await self._cleanup_inactive_users(db) await self._sync_with_remnawave(db) @@ -261,7 +250,7 @@ class MonitoringService: async def _check_trial_expiring_soon(self, db: AsyncSession): try: threshold_time = datetime.utcnow() + timedelta(hours=2) - + result = await db.execute( select(Subscription) .options(selectinload(Subscription.user)) @@ -299,202 +288,7 @@ class MonitoringService: except Exception as e: logger.error(f"Ошибка проверки истекающих тестовых подписок: {e}") - - async def _check_trial_inactivity_notifications(self, db: AsyncSession): - if not NotificationSettingsService.are_notifications_globally_enabled(): - return - if not self.bot: - return - - try: - now = datetime.utcnow() - one_hour_ago = now - timedelta(hours=1) - - result = await db.execute( - select(Subscription) - .options(selectinload(Subscription.user)) - .where( - and_( - Subscription.status == SubscriptionStatus.ACTIVE.value, - Subscription.is_trial == True, - Subscription.start_date.isnot(None), - Subscription.start_date <= one_hour_ago, - Subscription.end_date > now, - ) - ) - ) - - subscriptions = result.scalars().all() - sent_1h = 0 - sent_24h = 0 - - for subscription in subscriptions: - user = subscription.user - if not user: - continue - - if (subscription.traffic_used_gb or 0) > 0: - continue - - start_date = subscription.start_date - if not start_date: - continue - - time_since_start = now - start_date - - if (NotificationSettingsService.is_trial_inactive_1h_enabled() - and timedelta(hours=1) <= time_since_start < timedelta(hours=24)): - if not await notification_sent(db, user.id, subscription.id, "trial_inactive_1h"): - success = await self._send_trial_inactive_notification(user, subscription, 1) - if success: - await record_notification(db, user.id, subscription.id, "trial_inactive_1h") - sent_1h += 1 - - if NotificationSettingsService.is_trial_inactive_24h_enabled() and time_since_start >= timedelta(hours=24): - if not await notification_sent(db, user.id, subscription.id, "trial_inactive_24h"): - success = await self._send_trial_inactive_notification(user, subscription, 24) - if success: - await record_notification(db, user.id, subscription.id, "trial_inactive_24h") - sent_24h += 1 - - if sent_1h or sent_24h: - await self._log_monitoring_event( - db, - "trial_inactivity_notifications", - f"Отправлено {sent_1h} уведомлений спустя 1 час и {sent_24h} спустя 24 часа", - {"sent_1h": sent_1h, "sent_24h": sent_24h}, - ) - - except Exception as e: - logger.error(f"Ошибка проверки неактивных тестовых подписок: {e}") - - async def _check_expired_subscription_followups(self, db: AsyncSession): - if not NotificationSettingsService.are_notifications_globally_enabled(): - return - if not self.bot: - return - - try: - now = datetime.utcnow() - - result = await db.execute( - select(Subscription) - .options(selectinload(Subscription.user)) - .where( - and_( - Subscription.is_trial == False, - Subscription.end_date <= now, - ) - ) - ) - - subscriptions = result.scalars().all() - sent_day1 = 0 - sent_wave2 = 0 - sent_wave3 = 0 - - for subscription in subscriptions: - user = subscription.user - if not user: - continue - - if subscription.end_date is None: - continue - - time_since_end = now - subscription.end_date - if time_since_end.total_seconds() < 0: - continue - - days_since = time_since_end.total_seconds() / 86400 - - # Day 1 reminder - if NotificationSettingsService.is_expired_1d_enabled() and 1 <= days_since < 2: - if not await notification_sent(db, user.id, subscription.id, "expired_1d"): - success = await self._send_expired_day1_notification(user, subscription) - if success: - await record_notification(db, user.id, subscription.id, "expired_1d") - sent_day1 += 1 - - # Second wave (2-3 days) discount - if NotificationSettingsService.is_second_wave_enabled() and 2 <= days_since < 4: - if not await notification_sent(db, user.id, subscription.id, "expired_discount_wave2"): - percent = NotificationSettingsService.get_second_wave_discount_percent() - valid_hours = NotificationSettingsService.get_second_wave_valid_hours() - bonus_amount = settings.PRICE_30_DAYS * percent // 100 - offer = await upsert_discount_offer( - db, - user_id=user.id, - subscription_id=subscription.id, - notification_type="expired_discount_wave2", - discount_percent=percent, - bonus_amount_kopeks=bonus_amount, - valid_hours=valid_hours, - ) - success = await self._send_expired_discount_notification( - user, - subscription, - percent, - offer.expires_at, - offer.id, - "second", - bonus_amount, - ) - if success: - await record_notification(db, user.id, subscription.id, "expired_discount_wave2") - sent_wave2 += 1 - - # Third wave (N days) discount - if NotificationSettingsService.is_third_wave_enabled(): - trigger_days = NotificationSettingsService.get_third_wave_trigger_days() - if trigger_days <= days_since < trigger_days + 1: - if not await notification_sent(db, user.id, subscription.id, "expired_discount_wave3"): - percent = NotificationSettingsService.get_third_wave_discount_percent() - valid_hours = NotificationSettingsService.get_third_wave_valid_hours() - bonus_amount = settings.PRICE_30_DAYS * percent // 100 - offer = await upsert_discount_offer( - db, - user_id=user.id, - subscription_id=subscription.id, - notification_type="expired_discount_wave3", - discount_percent=percent, - bonus_amount_kopeks=bonus_amount, - valid_hours=valid_hours, - ) - success = await self._send_expired_discount_notification( - user, - subscription, - percent, - offer.expires_at, - offer.id, - "third", - bonus_amount, - trigger_days=trigger_days, - ) - if success: - await record_notification(db, user.id, subscription.id, "expired_discount_wave3") - sent_wave3 += 1 - - if sent_day1 or sent_wave2 or sent_wave3: - await self._log_monitoring_event( - db, - "expired_followups_sent", - ( - "Follow-ups: 1д={0}, скидка 2-3д={1}, скидка N={2}".format( - sent_day1, - sent_wave2, - sent_wave3, - ) - ), - { - "day1": sent_day1, - "wave2": sent_wave2, - "wave3": sent_wave3, - }, - ) - - except Exception as e: - logger.error(f"Ошибка проверки напоминаний об истекшей подписке: {e}") - + async def _get_expiring_paid_subscriptions(self, db: AsyncSession, days_before: int) -> List[Subscription]: current_time = datetime.utcnow() threshold_date = current_time + timedelta(days=days_before) @@ -671,7 +465,7 @@ class MonitoringService: async def _send_trial_ending_notification(self, user: User, subscription: Subscription) -> bool: try: texts = get_texts(user.language) - + message = f""" 🎁 Тестовая подписка скоро закончится! @@ -707,149 +501,7 @@ class MonitoringService: except Exception as e: logger.error(f"Ошибка отправки уведомления об окончании тестовой подписки пользователю {user.telegram_id}: {e}") return False - - async def _send_trial_inactive_notification(self, user: User, subscription: Subscription, hours: int) -> bool: - try: - texts = get_texts(user.language) - if hours >= 24: - template = texts.get( - "TRIAL_INACTIVE_24H", - ( - "⏳ Вы ещё не подключились к VPN\n\n" - "Прошли сутки с активации тестового периода, но трафик не зафиксирован." - "\n\nНажмите кнопку ниже, чтобы подключиться." - ), - ) - else: - template = texts.get( - "TRIAL_INACTIVE_1H", - ( - "⏳ Прошёл час, а подключения нет\n\n" - "Если возникли сложности с запуском — воспользуйтесь инструкциями." - ), - ) - - message = template.format( - price=settings.format_price(settings.PRICE_30_DAYS), - end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"), - ) - - from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton - - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")], - [InlineKeyboardButton(text=texts.t("MY_SUBSCRIPTION_BUTTON", "📱 Моя подписка"), callback_data="menu_subscription")], - [InlineKeyboardButton(text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), callback_data="menu_support")], - ]) - - await self.bot.send_message( - user.telegram_id, - message, - parse_mode="HTML", - reply_markup=keyboard, - ) - return True - - except Exception as e: - logger.error(f"Ошибка отправки уведомления об отсутствии подключения пользователю {user.telegram_id}: {e}") - return False - - async def _send_expired_day1_notification(self, user: User, subscription: Subscription) -> bool: - try: - texts = get_texts(user.language) - template = texts.get( - "SUBSCRIPTION_EXPIRED_1D", - ( - "⛔ Подписка закончилась\n\n" - "Доступ был отключён {end_date}. Продлите подписку, чтобы вернуться в сервис." - ), - ) - message = template.format( - end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"), - price=settings.format_price(settings.PRICE_30_DAYS), - ) - - from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton - - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), callback_data="subscription_extend")], - [InlineKeyboardButton(text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), callback_data="balance_topup")], - [InlineKeyboardButton(text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), callback_data="menu_support")], - ]) - - await self.bot.send_message( - user.telegram_id, - message, - parse_mode="HTML", - reply_markup=keyboard, - ) - return True - - except Exception as e: - logger.error(f"Ошибка отправки напоминания об истекшей подписке пользователю {user.telegram_id}: {e}") - return False - - async def _send_expired_discount_notification( - self, - user: User, - subscription: Subscription, - percent: int, - expires_at: datetime, - offer_id: int, - wave: str, - bonus_amount: int, - trigger_days: int = None, - ) -> bool: - try: - texts = get_texts(user.language) - - if wave == "second": - template = texts.get( - "SUBSCRIPTION_EXPIRED_SECOND_WAVE", - ( - "🔥 Скидка {percent}% на продление\n\n" - "Нажмите «Получить скидку», и мы начислим {bonus} на баланс. " - "Предложение действует до {expires_at}." - ), - ) - else: - template = texts.get( - "SUBSCRIPTION_EXPIRED_THIRD_WAVE", - ( - "🎁 Индивидуальная скидка {percent}%\n\n" - "Прошло {trigger_days} дней без подписки — возвращайтесь, и мы добавим {bonus} на баланс. " - "Скидка действует до {expires_at}." - ), - ) - - message = template.format( - percent=percent, - bonus=settings.format_price(bonus_amount), - expires_at=expires_at.strftime("%d.%m.%Y %H:%M"), - trigger_days=trigger_days or "", - ) - - from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton - - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🎁 Получить скидку", callback_data=f"claim_discount_{offer_id}")], - [InlineKeyboardButton(text=texts.t("SUBSCRIPTION_EXTEND", "💎 Продлить подписку"), callback_data="subscription_extend")], - [InlineKeyboardButton(text=texts.t("BALANCE_TOPUP", "💳 Пополнить баланс"), callback_data="balance_topup")], - [InlineKeyboardButton(text=texts.t("SUPPORT_BUTTON", "🆘 Поддержка"), callback_data="menu_support")], - ]) - - await self.bot.send_message( - user.telegram_id, - message, - parse_mode="HTML", - reply_markup=keyboard, - ) - return True - - except Exception as e: - logger.error(f"Ошибка отправки скидочного уведомления пользователю {user.telegram_id}: {e}") - return False - + async def _send_autopay_success_notification(self, user: User, amount: int, days: int): try: texts = get_texts(user.language) diff --git a/app/services/notification_settings_service.py b/app/services/notification_settings_service.py deleted file mode 100644 index a19edffd..00000000 --- a/app/services/notification_settings_service.py +++ /dev/null @@ -1,249 +0,0 @@ -import json -import json -import logging -from copy import deepcopy -from pathlib import Path -from typing import Any, Dict - -from app.config import settings - - -logger = logging.getLogger(__name__) - - -class NotificationSettingsService: - """Runtime-editable notification settings stored on disk.""" - - _storage_path: Path = Path("data/notification_settings.json") - _data: Dict[str, Dict[str, Any]] = {} - _loaded: bool = False - - _DEFAULTS: Dict[str, Dict[str, Any]] = { - "trial_inactive_1h": {"enabled": True}, - "trial_inactive_24h": {"enabled": True}, - "expired_1d": {"enabled": True}, - "expired_second_wave": { - "enabled": True, - "discount_percent": 10, - "valid_hours": 24, - }, - "expired_third_wave": { - "enabled": True, - "discount_percent": 20, - "valid_hours": 24, - "trigger_days": 5, - }, - } - - @classmethod - def _ensure_dir(cls) -> None: - try: - cls._storage_path.parent.mkdir(parents=True, exist_ok=True) - except Exception as exc: # pragma: no cover - filesystem guard - logger.error("Failed to create notification settings dir: %s", exc) - - @classmethod - def _load(cls) -> None: - if cls._loaded: - return - - cls._ensure_dir() - try: - if cls._storage_path.exists(): - raw = cls._storage_path.read_text(encoding="utf-8") - cls._data = json.loads(raw) if raw.strip() else {} - else: - cls._data = {} - except Exception as exc: - logger.error("Failed to load notification settings: %s", exc) - cls._data = {} - - changed = cls._apply_defaults() - if changed: - cls._save() - cls._loaded = True - - @classmethod - def _apply_defaults(cls) -> bool: - changed = False - for key, defaults in cls._DEFAULTS.items(): - current = cls._data.get(key) - if not isinstance(current, dict): - cls._data[key] = deepcopy(defaults) - changed = True - continue - - for def_key, def_value in defaults.items(): - if def_key not in current: - current[def_key] = def_value - changed = True - return changed - - @classmethod - def _save(cls) -> bool: - cls._ensure_dir() - try: - cls._storage_path.write_text( - json.dumps(cls._data, ensure_ascii=False, indent=2), - encoding="utf-8", - ) - return True - except Exception as exc: - logger.error("Failed to save notification settings: %s", exc) - return False - - @classmethod - def _get(cls, key: str) -> Dict[str, Any]: - cls._load() - value = cls._data.get(key) - if not isinstance(value, dict): - value = deepcopy(cls._DEFAULTS.get(key, {})) - cls._data[key] = value - return value - - @classmethod - def get_config(cls) -> Dict[str, Dict[str, Any]]: - cls._load() - return deepcopy(cls._data) - - @classmethod - def _set_field(cls, key: str, field: str, value: Any) -> bool: - cls._load() - section = cls._get(key) - section[field] = value - cls._data[key] = section - return cls._save() - - @classmethod - def set_enabled(cls, key: str, enabled: bool) -> bool: - return cls._set_field(key, "enabled", bool(enabled)) - - @classmethod - def is_enabled(cls, key: str) -> bool: - return bool(cls._get(key).get("enabled", True)) - - # Trial inactivity helpers - @classmethod - def is_trial_inactive_1h_enabled(cls) -> bool: - return cls.is_enabled("trial_inactive_1h") - - @classmethod - def set_trial_inactive_1h_enabled(cls, enabled: bool) -> bool: - return cls.set_enabled("trial_inactive_1h", enabled) - - @classmethod - def is_trial_inactive_24h_enabled(cls) -> bool: - return cls.is_enabled("trial_inactive_24h") - - @classmethod - def set_trial_inactive_24h_enabled(cls, enabled: bool) -> bool: - return cls.set_enabled("trial_inactive_24h", enabled) - - # Expired subscription notifications - @classmethod - def is_expired_1d_enabled(cls) -> bool: - return cls.is_enabled("expired_1d") - - @classmethod - def set_expired_1d_enabled(cls, enabled: bool) -> bool: - return cls.set_enabled("expired_1d", enabled) - - @classmethod - def is_second_wave_enabled(cls) -> bool: - return cls.is_enabled("expired_second_wave") - - @classmethod - def set_second_wave_enabled(cls, enabled: bool) -> bool: - return cls.set_enabled("expired_second_wave", enabled) - - @classmethod - def get_second_wave_discount_percent(cls) -> int: - value = cls._get("expired_second_wave").get("discount_percent", 10) - try: - return max(0, min(100, int(value))) - except (TypeError, ValueError): - return 10 - - @classmethod - def set_second_wave_discount_percent(cls, percent: int) -> bool: - try: - percent_int = max(0, min(100, int(percent))) - except (TypeError, ValueError): - return False - return cls._set_field("expired_second_wave", "discount_percent", percent_int) - - @classmethod - def get_second_wave_valid_hours(cls) -> int: - value = cls._get("expired_second_wave").get("valid_hours", 24) - try: - return max(1, min(168, int(value))) - except (TypeError, ValueError): - return 24 - - @classmethod - def set_second_wave_valid_hours(cls, hours: int) -> bool: - try: - hours_int = max(1, min(168, int(hours))) - except (TypeError, ValueError): - return False - return cls._set_field("expired_second_wave", "valid_hours", hours_int) - - @classmethod - def is_third_wave_enabled(cls) -> bool: - return cls.is_enabled("expired_third_wave") - - @classmethod - def set_third_wave_enabled(cls, enabled: bool) -> bool: - return cls.set_enabled("expired_third_wave", enabled) - - @classmethod - def get_third_wave_discount_percent(cls) -> int: - value = cls._get("expired_third_wave").get("discount_percent", 20) - try: - return max(0, min(100, int(value))) - except (TypeError, ValueError): - return 20 - - @classmethod - def set_third_wave_discount_percent(cls, percent: int) -> bool: - try: - percent_int = max(0, min(100, int(percent))) - except (TypeError, ValueError): - return False - return cls._set_field("expired_third_wave", "discount_percent", percent_int) - - @classmethod - def get_third_wave_valid_hours(cls) -> int: - value = cls._get("expired_third_wave").get("valid_hours", 24) - try: - return max(1, min(168, int(value))) - except (TypeError, ValueError): - return 24 - - @classmethod - def set_third_wave_valid_hours(cls, hours: int) -> bool: - try: - hours_int = max(1, min(168, int(hours))) - except (TypeError, ValueError): - return False - return cls._set_field("expired_third_wave", "valid_hours", hours_int) - - @classmethod - def get_third_wave_trigger_days(cls) -> int: - value = cls._get("expired_third_wave").get("trigger_days", 5) - try: - return max(2, min(60, int(value))) - except (TypeError, ValueError): - return 5 - - @classmethod - def set_third_wave_trigger_days(cls, days: int) -> bool: - try: - days_int = max(2, min(60, int(days))) - except (TypeError, ValueError): - return False - return cls._set_field("expired_third_wave", "trigger_days", days_int) - - @classmethod - def are_notifications_globally_enabled(cls) -> bool: - return bool(getattr(settings, "ENABLE_NOTIFICATIONS", True)) diff --git a/app/states.py b/app/states.py index 782fae7d..45e87e21 100644 --- a/app/states.py +++ b/app/states.py @@ -84,10 +84,9 @@ class AdminStates(StatesGroup): editing_device_price = State() editing_user_devices = State() editing_user_traffic = State() - + editing_rules_page = State() - editing_notification_value = State() - + confirming_sync = State() editing_server_name = State() diff --git a/locales/en.json b/locales/en.json index 68c3adc1..1b416564 100644 --- a/locales/en.json +++ b/locales/en.json @@ -129,7 +129,6 @@ "ACCESS_DENIED": "❌ Access denied", "ADMIN_MESSAGES": "📨 Broadcasts", "ADMIN_MONITORING": "🔍 Monitoring", - "ADMIN_MONITORING_SETTINGS": "🔔 Notification settings", "ADMIN_PANEL": "\n⚙️ Administration panel\n\nSelect a section to manage:\n", "ADMIN_PROMOCODES": "🎫 Promo codes", "ADMIN_REFERRALS": "🤝 Referral program", @@ -485,23 +484,5 @@ "PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "via CryptoBot", "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Support team", "PAYMENT_METHOD_SUPPORT_DESCRIPTION": "other options", - "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ Automated payment methods are temporarily unavailable. Contact support to top up your balance.", - "TRIAL_INACTIVE_1H": "⏳ An hour has passed and we haven't seen any traffic yet\n\nOpen the connection guide and follow the steps. We're always ready to help!", - "TRIAL_INACTIVE_24H": "⏳ A full day passed without activity\n\nWe still don't see traffic from your test subscription. Use the guide or message support and we'll help you connect!", - "SUBSCRIPTION_EXPIRED_1D": "⛔ Your subscription expired\n\nAccess was disabled on {end_date}. Renew to return to the service.\n\n💎 Renewal price: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 {percent}% discount on renewal\n\nTap “Get discount” and we'll add {bonus} to your balance. The offer is valid until {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Personal {percent}% discount\n\nIt's been {trigger_days} days without a subscription. Come back — tap “Get discount” and {bonus} will be credited. Offer valid until {expires_at}.", - "DISCOUNT_CLAIM_SUCCESS": "🎉 Discount of {percent}% activated! {amount} credited to your balance.", - "DISCOUNT_CLAIM_ALREADY": "ℹ️ This discount has already been activated.", - "DISCOUNT_CLAIM_EXPIRED": "⚠️ The offer has expired.", - "DISCOUNT_CLAIM_NOT_FOUND": "❌ Offer not found.", - "DISCOUNT_CLAIM_ERROR": "❌ Failed to credit the discount. Please try again later.", - "DISCOUNT_BONUS_DESCRIPTION": "Renewal discount bonus", - "NOTIFICATION_VALUE_INVALID": "❌ Invalid value, please enter a number.", - "NOTIFICATION_VALUE_UPDATED": "✅ Settings updated.", - "NOTIFY_PROMPT_SECOND_PERCENT": "Enter a new discount percentage for the 2-3 day reminder (0-100):", - "NOTIFY_PROMPT_SECOND_HOURS": "Enter the number of hours the discount is active (1-168):", - "NOTIFY_PROMPT_THIRD_PERCENT": "Enter a new discount percentage for the late offer (0-100):", - "NOTIFY_PROMPT_THIRD_HOURS": "Enter the number of hours the late discount is active (1-168):", - "NOTIFY_PROMPT_THIRD_DAYS": "After how many days without a subscription should we send the offer? (minimum 2):" + "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ Automated payment methods are temporarily unavailable. Contact support to top up your balance." } diff --git a/locales/ru.json b/locales/ru.json index 9f9d87f1..736d38e1 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -5,7 +5,6 @@ "ADMIN_CAMPAIGNS": "📣 Рекламные кампании", "ADMIN_MESSAGES": "📨 Рассылки", "ADMIN_MONITORING": "🔍 Мониторинг", - "ADMIN_MONITORING_SETTINGS": "🔔 Настройки уведомлений", "ADMIN_REPORTS": "📊 Отчеты", "ADMIN_PANEL": "\n⚙️ Административная панель\n\nВыберите раздел для управления:\n", "ADMIN_PROMOCODES": "🎫 Промокоды", @@ -485,23 +484,5 @@ "PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot", "PAYMENT_METHOD_SUPPORT_NAME": "🛠️ Через поддержку", "PAYMENT_METHOD_SUPPORT_DESCRIPTION": "другие способы", - "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.", - "TRIAL_INACTIVE_1H": "⏳ Прошёл час, а подключение не выполнено\n\nЕсли возникли сложности — откройте инструкцию и следуйте шагам. Мы всегда готовы помочь!", - "TRIAL_INACTIVE_24H": "⏳ Прошли сутки с начала теста\n\nМы не видим трафика по вашей подписке. Загляните в инструкцию или напишите в поддержку — поможем подключиться!", - "SUBSCRIPTION_EXPIRED_1D": "⛔ Подписка закончилась\n\nДоступ был отключён {end_date}. Продлите подписку, чтобы вернуть полный доступ.\n\n💎 Стоимость продления: {price}", - "SUBSCRIPTION_EXPIRED_SECOND_WAVE": "🔥 Скидка {percent}% на продление\n\nНажмите «Получить скидку», и мы начислим {bonus} на ваш баланс. Предложение действительно до {expires_at}.", - "SUBSCRIPTION_EXPIRED_THIRD_WAVE": "🎁 Индивидуальная скидка {percent}%\n\nПрошло {trigger_days} дней без подписки. Вернитесь — нажмите «Получить скидку», и {bonus} поступит на баланс. Предложение действительно до {expires_at}.", - "DISCOUNT_CLAIM_SUCCESS": "🎉 Скидка {percent}% активирована! На баланс начислено {amount}.", - "DISCOUNT_CLAIM_ALREADY": "ℹ️ Скидка уже была активирована ранее.", - "DISCOUNT_CLAIM_EXPIRED": "⚠️ Время действия предложения истекло.", - "DISCOUNT_CLAIM_NOT_FOUND": "❌ Предложение не найдено.", - "DISCOUNT_CLAIM_ERROR": "❌ Не удалось начислить скидку. Попробуйте позже.", - "DISCOUNT_BONUS_DESCRIPTION": "Скидка за продление подписки", - "NOTIFICATION_VALUE_INVALID": "❌ Некорректное значение, укажите число.", - "NOTIFICATION_VALUE_UPDATED": "✅ Настройки обновлены.", - "NOTIFY_PROMPT_SECOND_PERCENT": "Введите новый процент скидки для уведомления через 2-3 дня (0-100):", - "NOTIFY_PROMPT_SECOND_HOURS": "Введите количество часов действия скидки (1-168):", - "NOTIFY_PROMPT_THIRD_PERCENT": "Введите новый процент скидки для позднего предложения (0-100):", - "NOTIFY_PROMPT_THIRD_HOURS": "Введите количество часов действия скидки (1-168):", - "NOTIFY_PROMPT_THIRD_DAYS": "Через сколько дней после истечения отправлять предложение? (минимум 2):" + "PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку." }