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": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку."
}