Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edc891c93d | |||
| 2d706a4be0 | |||
| 32cb81ef34 | |||
| 88aa3c4a3d | |||
| e813d3ccb7 | |||
| a24d7a4e24 | |||
| 39987b9717 | |||
| c77a1ea4f8 | |||
| 64b5d90d66 | |||
| 5a1fb32e1d | |||
| c4d8108e8c | |||
| 1350d80083 | |||
| e0cc218007 | |||
| bcaf8f2247 | |||
| d1190ed9dc | |||
| 513d6037d4 | |||
| e86012b2ab | |||
| 214e381dc5 |
@@ -12,7 +12,7 @@
|
||||
## 🚀 Особенности
|
||||
|
||||
### 👤 Для пользователей
|
||||
- 💰 **Управление балансом** - пополнение через поддержку
|
||||
- 💰 **Управление балансом** - Пополнение через Telegram Stars + пополнение через поддержку p2p
|
||||
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
|
||||
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
|
||||
- 🎁 **Система промокодов** - денежные коды
|
||||
@@ -20,6 +20,8 @@
|
||||
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
|
||||
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
|
||||
- 🌐 **Мультиязычность** - русский и английский
|
||||
- 📋 **Правила сервиса**
|
||||
- ♾️ **Автопродление** - С настройкой вкл/выкл и кол-вом дней до автопродления.
|
||||
|
||||
### ⚙️ Для администраторов
|
||||
- 📊 **Полная статистика** - пользователи, платежи, подписки
|
||||
@@ -30,6 +32,8 @@
|
||||
- 🔄 **Синхронизация данных** - импорт из RemnaWave
|
||||
- 📨 **Рассылки** - уведомления пользователям
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
|
||||
- 📋 **Правила сервиса** - Настройка страниц из админ панели
|
||||
- ♾️ **Автопродление** - Статус сервиса, статистика автопродления, принудительное продление
|
||||
|
||||
## 📋 Требования
|
||||
|
||||
@@ -101,6 +105,17 @@ nano .env
|
||||
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
|
||||
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
|
||||
|
||||
#### 🌟 Оплата за звезды
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `STARS_ENABLED=true` | Включить пополнение баланса за звезды | `true/false` |
|
||||
| `STARS_100_RATE` | Курс за 100 звезд | `100` |
|
||||
| `STARS_150_RATE` | Курс за 150 звезд | `150` |
|
||||
| `STARS_250_RATE` | Курс за 250 звезд | `250` |
|
||||
| `И тд` | Курс за XXX звезд | `XXXX` |
|
||||
|
||||
|
||||
#### 💬 Сервис мониторинга сообщений
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
@@ -666,8 +681,10 @@ docker-compose ps
|
||||
| № | Задача | Статус | Версия | Приоритет | Описание |
|
||||
|---|---------|--------|--------|-----------|----------|
|
||||
| 1 | Мониторинг подписок | ✅ Done | v1.1.0 | 🔴 High | Служба оповещения об истечении срока и автоконтроль |
|
||||
| 2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa, Сбербанк, Tinkoff, Crypto, Звезды |
|
||||
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
|
||||
| 2 | Платежные шлюзы | ✅ Done | v1.3.2 | 🔴 High | Звезды |
|
||||
| 2.1 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa |
|
||||
| 2.2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🟡 Medium | Сбербанк, Tinkoff, Crypto, Others |
|
||||
| 3.4 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
|
||||
| 4 | Веб-панель управления | 🎯 Planned | v1.4.0+ | 🟡 Medium | Полнофункциональный веб-интерфейс |
|
||||
| 5 | Управление промокодами | ✅ Done | v1.3.0 | 🟢 Low | Удаление, редактирование |
|
||||
| 6 | Безопасное удаление подписок | 🚧 In Progress | v1.4.0 | 🟡 Medium | Архивирование вместо удаления |
|
||||
@@ -676,6 +693,7 @@ docker-compose ps
|
||||
| 9 | Рулетка | ✅ Done | v1.3.0 | 🟢 Low | Лудочка |
|
||||
| 10 | Блокировка | 🚧 In Progress | v1.3.0+ | 🟢 Low | Бан/разбан юзеров |
|
||||
| 11 | Удаление истекшик подписок из бота и панели | ✅ Done | v1.3.1 | 🔴 High | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
|
||||
| 12 | Правила сервиса | ✅ Done | v1.3.3 | 🟢 Low | Вывод правил сервиса в боте с конфигурацией через админку |
|
||||
|
||||
|
||||
### 🚀 Хотите помочь?
|
||||
|
||||
+897
-7
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta, timezone
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
|
||||
from database import Database, User, ReferralProgram, ReferralEarning
|
||||
from database import Database, User, ReferralProgram, ReferralEarning, ServiceRule
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from keyboards import *
|
||||
from translations import t
|
||||
@@ -430,7 +430,6 @@ async def toggle_subscription(callback: CallbackQuery, user: User, db: Database,
|
||||
status = t('enabled', user.language) if sub.is_active else t('disabled', user.language)
|
||||
await callback.answer(f"✅ Подписка «{sub.name}» {status}")
|
||||
|
||||
# Update the list
|
||||
subs = await db.get_all_subscriptions(include_inactive=True)
|
||||
await callback.message.edit_reply_markup(
|
||||
reply_markup=admin_subscriptions_list_keyboard(subs, user.language)
|
||||
@@ -589,7 +588,6 @@ async def delete_subscription(callback: CallbackQuery, user: User, db: Database,
|
||||
else:
|
||||
await callback.answer("❌ Ошибка удаления")
|
||||
|
||||
# Return to list
|
||||
subs = await db.get_all_subscriptions(include_inactive=True)
|
||||
if subs:
|
||||
await callback.message.edit_text(
|
||||
@@ -681,7 +679,6 @@ async def handle_balance_user_id(message: Message, state: FSMContext, user: User
|
||||
await message.answer("❌ Неверный Telegram ID")
|
||||
return
|
||||
|
||||
# Check if user exists
|
||||
target_user = await db.get_user_by_telegram_id(telegram_id)
|
||||
if not target_user:
|
||||
await message.answer(t('user_not_found', user.language))
|
||||
@@ -1046,7 +1043,7 @@ async def handle_promo_expiry(message: Message, state: FSMContext, user: User, d
|
||||
else:
|
||||
try:
|
||||
days = int(expiry_input)
|
||||
if days <= 0 or days > 3650: # Максимум 10 лет
|
||||
if days <= 0 or days > 3650:
|
||||
await message.answer("❌ Количество дней должно быть от 1 до 3650")
|
||||
return
|
||||
expires_at = datetime.utcnow() + timedelta(days=days)
|
||||
@@ -1733,8 +1730,22 @@ async def confirm_deactivate_all_callback(callback: CallbackQuery, user: User, d
|
||||
BotStates.admin_edit_user_expiry,
|
||||
BotStates.admin_edit_user_traffic,
|
||||
BotStates.admin_test_monitor_user,
|
||||
BotStates.admin_rename_plans_confirm
|
||||
BotStates.admin_rename_plans_confirm,
|
||||
BotStates.waiting_rule_title,
|
||||
BotStates.waiting_rule_content,
|
||||
BotStates.waiting_rule_order,
|
||||
BotStates.waiting_rule_edit_title,
|
||||
BotStates.waiting_rule_edit_content,
|
||||
BotStates.waiting_rule_edit_order
|
||||
))
|
||||
async def cancel_rule_editing(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
|
||||
"""Отмена редактирования правил"""
|
||||
await state.clear()
|
||||
await callback.message.edit_text(
|
||||
t('main_menu', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
async def cancel_admin_action(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
|
||||
await state.clear()
|
||||
await callback.message.edit_text(
|
||||
@@ -5627,7 +5638,7 @@ async def import_all_by_telegram_callback(callback: CallbackQuery, user: User, a
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
expire_dt_naive = datetime.now() + timedelta(days=30) # Дефолт
|
||||
expire_dt_naive = datetime.now() + timedelta(days=30)
|
||||
if expire_at:
|
||||
try:
|
||||
if expire_at.endswith('Z'):
|
||||
@@ -6849,3 +6860,882 @@ async def admin_stars_settings_callback(callback: CallbackQuery, user: User, **k
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_rules")
|
||||
async def admin_rules_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
"""Главное меню управления правилами"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📜 Управление правилами сервиса\n\n"
|
||||
"Здесь вы можете создавать, редактировать и управлять страницами правил сервиса, "
|
||||
"которые видят пользователи в главном меню.",
|
||||
reply_markup=admin_rules_keyboard(user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_rules_list")
|
||||
async def admin_rules_list_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
rules = await db.get_all_service_rules(active_only=False)
|
||||
|
||||
if not rules:
|
||||
await callback.message.edit_text(
|
||||
"📜 Правила сервиса не созданы\n\n"
|
||||
"Создайте первую страницу правил для пользователей.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="➕ Создать первую страницу", callback_data="admin_rules_create")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_rules")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
text = f"📜 Список правил сервиса ({len(rules)} страниц)\n\n"
|
||||
|
||||
for rule in rules:
|
||||
status = "🟢 Активна" if rule.is_active else "🔴 Отключена"
|
||||
text += f"{rule.page_order}. **{rule.title}**\n"
|
||||
text += f" {status}\n"
|
||||
text += f" Создано: {rule.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=admin_rules_list_keyboard(rules, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing service rules: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки правил")
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_rules_create")
|
||||
async def admin_rules_create_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📝 Создание новой страницы правил\n\n"
|
||||
"Введите заголовок страницы (например: 'Общие положения', 'Правила использования'):",
|
||||
reply_markup=cancel_keyboard(user.language)
|
||||
)
|
||||
await state.set_state(BotStates.waiting_rule_title)
|
||||
|
||||
@admin_router.message(StateFilter(BotStates.waiting_rule_title))
|
||||
async def handle_rule_title(message: Message, state: FSMContext, user: User, **kwargs):
|
||||
title = message.text.strip()
|
||||
|
||||
if len(title) < 3 or len(title) > 200:
|
||||
await message.answer("❌ Заголовок должен быть от 3 до 200 символов")
|
||||
return
|
||||
|
||||
await state.update_data(rule_title=title)
|
||||
await message.answer(
|
||||
f"✅ Заголовок установлен: **{title}**\n\n"
|
||||
"📝 Теперь введите содержимое страницы правил:\n\n"
|
||||
"💡 Вы можете использовать форматирование Markdown:\n"
|
||||
"• **жирный текст**\n"
|
||||
"• *курсив*\n"
|
||||
"• `код`\n"
|
||||
"• [ссылка](url)\n\n"
|
||||
"Максимальная длина: 3500 символов",
|
||||
reply_markup=cancel_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
await state.set_state(BotStates.waiting_rule_content)
|
||||
|
||||
@admin_router.message(StateFilter(BotStates.waiting_rule_content))
|
||||
async def handle_rule_content(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||||
content = message.text.strip()
|
||||
|
||||
if len(content) < 10:
|
||||
await message.answer("❌ Содержимое должно быть не менее 10 символов")
|
||||
return
|
||||
|
||||
if len(content) > 3500:
|
||||
await message.answer("❌ Содержимое слишком длинное. Максимум 3500 символов.")
|
||||
return
|
||||
|
||||
try:
|
||||
data = await state.get_data()
|
||||
title = data.get('rule_title')
|
||||
|
||||
rule = await db.create_service_rule(title=title, content=content)
|
||||
|
||||
await message.answer(
|
||||
f"✅ Страница правил создана!\n\n"
|
||||
f"📋 Заголовок: {title}\n"
|
||||
f"📄 Порядок: {rule.page_order}\n"
|
||||
f"📊 Статус: {'🟢 Активна' if rule.is_active else '🔴 Отключена'}\n\n"
|
||||
f"Пользователи смогут увидеть эту страницу в меню 'Правила сервиса'.",
|
||||
reply_markup=admin_menu_keyboard(user.language)
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "service_rule_created", f"Title: {title}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating service rule: {e}")
|
||||
await message.answer(
|
||||
"❌ Ошибка создания страницы правил",
|
||||
reply_markup=admin_menu_keyboard(user.language)
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_view_"))
|
||||
async def admin_rule_view_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
|
||||
if not rule:
|
||||
await callback.answer("❌ Правило не найдено")
|
||||
return
|
||||
|
||||
safe_title = rule.title.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace(']', '\\]').replace('`', '\\`')
|
||||
|
||||
text = f"📜 **{safe_title}**\n\n"
|
||||
text += f"📄 Порядок: {rule.page_order}\n"
|
||||
text += f"📊 Статус: {'🟢 Активна' if rule.is_active else '🔴 Отключена'}\n"
|
||||
|
||||
created_date = rule.created_at.strftime('%d.%m.%Y %H:%M') if rule.created_at else 'N/A'
|
||||
updated_date = rule.updated_at.strftime('%d.%m.%Y %H:%M') if rule.updated_at else 'N/A'
|
||||
|
||||
text += f"📅 Создано: {created_date}\n"
|
||||
text += f"📝 Изменено: {updated_date}\n\n"
|
||||
|
||||
content_preview = rule.content[:200]
|
||||
safe_preview = (content_preview
|
||||
.replace('*', '')
|
||||
.replace('_', '')
|
||||
.replace('[', '')
|
||||
.replace(']', '')
|
||||
.replace('`', '')
|
||||
.replace('#', ''))
|
||||
|
||||
if len(rule.content) > 200:
|
||||
safe_preview += "..."
|
||||
|
||||
text += f"**Превью содержимого:**\n{safe_preview}"
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=admin_rule_edit_keyboard(rule_id, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except Exception as markdown_error:
|
||||
logger.warning(f"Markdown parsing failed, sending without formatting: {markdown_error}")
|
||||
|
||||
simple_text = f"📜 {rule.title}\n\n"
|
||||
simple_text += f"📄 Порядок: {rule.page_order}\n"
|
||||
simple_text += f"📊 Статус: {'🟢 Активна' if rule.is_active else '🔴 Отключена'}\n"
|
||||
simple_text += f"📅 Создано: {created_date}\n"
|
||||
simple_text += f"📝 Изменено: {updated_date}\n\n"
|
||||
simple_text += f"Превью содержимого:\n{safe_preview}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
simple_text,
|
||||
reply_markup=admin_rule_edit_keyboard(rule_id, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error viewing service rule: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки правила")
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_edit_title_"))
|
||||
async def admin_rule_edit_title_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
await state.update_data(edit_rule_id=rule_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"✏️ Редактирование заголовка\n\n"
|
||||
"Введите новый заголовок страницы (3-200 символов):",
|
||||
reply_markup=cancel_keyboard(user.language)
|
||||
)
|
||||
await state.set_state(BotStates.waiting_rule_edit_title)
|
||||
|
||||
@admin_router.message(StateFilter(BotStates.waiting_rule_edit_title))
|
||||
async def handle_rule_edit_title(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||||
new_title = message.text.strip()
|
||||
|
||||
if len(new_title) < 3 or len(new_title) > 200:
|
||||
await message.answer("❌ Заголовок должен быть от 3 до 200 символов")
|
||||
return
|
||||
|
||||
try:
|
||||
data = await state.get_data()
|
||||
rule_id = data.get('edit_rule_id')
|
||||
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
if not rule:
|
||||
await message.answer("❌ Правило не найдено")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
old_title = rule.title
|
||||
rule.title = new_title
|
||||
success = await db.update_service_rule(rule)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Заголовок обновлен!\n\n"
|
||||
f"Было: {old_title}\n"
|
||||
f"Стало: {new_title}",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📜 К правилу", callback_data=f"admin_rule_view_{rule_id}")],
|
||||
[InlineKeyboardButton(text="📋 К списку", callback_data="admin_rules_list")]
|
||||
])
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "service_rule_title_edited",
|
||||
f"ID: {rule_id}, New: {new_title}")
|
||||
else:
|
||||
await message.answer("❌ Ошибка обновления заголовка")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating rule title: {e}")
|
||||
await message.answer("❌ Ошибка обновления")
|
||||
|
||||
await state.clear()
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_edit_content_"))
|
||||
async def admin_rule_edit_content_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
await state.update_data(edit_rule_id=rule_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📝 Редактирование содержимого\n\n"
|
||||
"Введите новое содержимое страницы правил:\n\n"
|
||||
"💡 Поддерживается Markdown форматирование\n"
|
||||
"Максимальная длина: 3500 символов",
|
||||
reply_markup=cancel_keyboard(user.language)
|
||||
)
|
||||
await state.set_state(BotStates.waiting_rule_edit_content)
|
||||
|
||||
@admin_router.message(StateFilter(BotStates.waiting_rule_edit_content))
|
||||
async def handle_rule_edit_content(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||||
new_content = message.text.strip()
|
||||
|
||||
if len(new_content) < 10:
|
||||
await message.answer("❌ Содержимое должно быть не менее 10 символов")
|
||||
return
|
||||
|
||||
if len(new_content) > 3500:
|
||||
await message.answer("❌ Содержимое слишком длинное. Максимум 3500 символов.")
|
||||
return
|
||||
|
||||
try:
|
||||
data = await state.get_data()
|
||||
rule_id = data.get('edit_rule_id')
|
||||
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
if not rule:
|
||||
await message.answer("❌ Правило не найдено")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
rule.content = new_content
|
||||
success = await db.update_service_rule(rule)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Содержимое обновлено!\n\n"
|
||||
f"📜 Правило: {rule.title}\n"
|
||||
f"📝 Новый размер: {len(new_content)} символов",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📜 К правилу", callback_data=f"admin_rule_view_{rule_id}")],
|
||||
[InlineKeyboardButton(text="📋 К списку", callback_data="admin_rules_list")]
|
||||
])
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "service_rule_content_edited",
|
||||
f"ID: {rule_id}, Length: {len(new_content)}")
|
||||
else:
|
||||
await message.answer("❌ Ошибка обновления содержимого")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating rule content: {e}")
|
||||
await message.answer("❌ Ошибка обновления")
|
||||
|
||||
await state.clear()
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_edit_order_"))
|
||||
async def admin_rule_edit_order_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
await state.update_data(edit_rule_id=rule_id)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔄 Изменение порядка страницы\n\n"
|
||||
"Введите новый номер позиции страницы (число от 1 до 100):\n\n"
|
||||
"💡 Страницы с меньшим номером показываются раньше",
|
||||
reply_markup=cancel_keyboard(user.language)
|
||||
)
|
||||
await state.set_state(BotStates.waiting_rule_edit_order)
|
||||
|
||||
@admin_router.message(StateFilter(BotStates.waiting_rule_edit_order))
|
||||
async def handle_rule_edit_order(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||||
try:
|
||||
new_order = int(message.text.strip())
|
||||
|
||||
if new_order < 1 or new_order > 100:
|
||||
await message.answer("❌ Порядок должен быть от 1 до 100")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
rule_id = data.get('edit_rule_id')
|
||||
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
if not rule:
|
||||
await message.answer("❌ Правило не найдено")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
old_order = rule.page_order
|
||||
rule.page_order = new_order
|
||||
success = await db.update_service_rule(rule)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
f"✅ Порядок страницы изменен!\n\n"
|
||||
f"📜 Правило: {rule.title}\n"
|
||||
f"📄 Было: {old_order}\n"
|
||||
f"📄 Стало: {new_order}",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📜 К правилу", callback_data=f"admin_rule_view_{rule_id}")],
|
||||
[InlineKeyboardButton(text="📋 К списку", callback_data="admin_rules_list")]
|
||||
])
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "service_rule_order_changed",
|
||||
f"ID: {rule_id}, Order: {old_order}->{new_order}")
|
||||
else:
|
||||
await message.answer("❌ Ошибка изменения порядка")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите корректное число")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating rule order: {e}")
|
||||
await message.answer("❌ Ошибка обновления")
|
||||
|
||||
await state.clear()
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_toggle_"))
|
||||
async def admin_rule_toggle_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
|
||||
if not rule:
|
||||
await callback.answer("❌ Правило не найдено")
|
||||
return
|
||||
|
||||
rule.is_active = not rule.is_active
|
||||
success = await db.update_service_rule(rule)
|
||||
|
||||
if success:
|
||||
status_text = "активирована" if rule.is_active else "отключена"
|
||||
await callback.answer(f"✅ Страница '{rule.title}' {status_text}")
|
||||
|
||||
await admin_rule_view_callback(callback, user, db, **kwargs)
|
||||
|
||||
log_user_action(user.telegram_id, "service_rule_toggled",
|
||||
f"ID: {rule_id}, Active: {rule.is_active}")
|
||||
else:
|
||||
await callback.answer("❌ Ошибка обновления статуса")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling service rule: {e}")
|
||||
await callback.answer("❌ Ошибка изменения статуса")
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_delete_"))
|
||||
async def admin_rule_delete_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
|
||||
if not rule:
|
||||
await callback.answer("❌ Правило не найдено")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"⚠️ Удаление страницы правил\n\n"
|
||||
f"📜 Заголовок: **{rule.title}**\n"
|
||||
f"📄 Порядок: {rule.page_order}\n\n"
|
||||
f"❗️ Это действие нельзя отменить!\n"
|
||||
f"Пользователи больше не увидят эту страницу.",
|
||||
reply_markup=admin_rule_delete_confirm_keyboard(rule_id, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing delete confirmation: {e}")
|
||||
await callback.answer("❌ Ошибка")
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_rule_confirm_delete_"))
|
||||
async def admin_rule_confirm_delete_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
rule_id = int(callback.data.split("_")[-1])
|
||||
rule = await db.get_service_rule_by_id(rule_id)
|
||||
|
||||
if not rule:
|
||||
await callback.answer("❌ Правило не найдено")
|
||||
return
|
||||
|
||||
rule_title = rule.title
|
||||
success = await db.delete_service_rule(rule_id)
|
||||
|
||||
if success:
|
||||
await callback.message.edit_text(
|
||||
f"✅ Страница правил удалена\n\n"
|
||||
f"📜 Была удалена: {rule_title}",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 К списку правил", callback_data="admin_rules_list")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "service_rule_deleted", f"Title: {rule_title}")
|
||||
else:
|
||||
await callback.answer("❌ Ошибка удаления")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting service rule: {e}")
|
||||
await callback.answer("❌ Ошибка удаления")
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_autopay")
|
||||
async def admin_autopay_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔄 Управление автоплатежами\n\n"
|
||||
"Здесь вы можете просматривать статистику и управлять сервисом автоматических платежей.",
|
||||
reply_markup=admin_autopay_keyboard(user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_status")
|
||||
async def autopay_status_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
autopay_service = kwargs.get('autopay_service')
|
||||
db = kwargs.get('db')
|
||||
|
||||
if not autopay_service:
|
||||
await callback.message.edit_text(
|
||||
"❌ Сервис автоплатежей недоступен",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
status = await autopay_service.get_service_status()
|
||||
|
||||
subscriptions_with_autopay = await db.get_subscriptions_for_autopay()
|
||||
|
||||
text = "🔄 **Статус сервиса автоплатежей**\n\n"
|
||||
|
||||
if status['is_running']:
|
||||
text += "✅ **Статус:** Работает\n"
|
||||
else:
|
||||
text += "❌ **Статус:** Остановлен\n"
|
||||
|
||||
text += f"⚙️ **Настройки:**\n"
|
||||
text += f"• Интервал проверки: {status['check_interval']//60} мин\n"
|
||||
text += f"• API подключен: {'✅' if status['has_api'] else '❌'}\n"
|
||||
text += f"• Бот подключен: {'✅' if status['has_bot'] else '❌'}\n\n"
|
||||
|
||||
text += f"📊 **Статистика:**\n"
|
||||
text += f"• Подписок с автоплатежом: {len(subscriptions_with_autopay)}\n"
|
||||
|
||||
days_stats = {}
|
||||
for sub in subscriptions_with_autopay:
|
||||
days = sub.auto_pay_days_before
|
||||
days_stats[days] = days_stats.get(days, 0) + 1
|
||||
|
||||
if days_stats:
|
||||
text += f"• Распределение по дням:\n"
|
||||
for days in sorted(days_stats.keys()):
|
||||
text += f" - За {days} дн.: {days_stats[days]} подписок\n"
|
||||
|
||||
text += f"\n🕐 Обновлено: {format_datetime(datetime.now(), user.language)}"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay status: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения статуса",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_force_check")
|
||||
async def autopay_force_check_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
autopay_service = kwargs.get('autopay_service')
|
||||
|
||||
if not autopay_service:
|
||||
await callback.answer("❌ Сервис автоплатежей недоступен")
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.answer("⏳ Запускаю проверку автоплатежей...")
|
||||
|
||||
stats = await autopay_service.process_autopayments()
|
||||
|
||||
text = "✅ Принудительная проверка автоплатежей завершена!\n\n"
|
||||
text += f"📊 Результаты:\n"
|
||||
text += f"• Обработано: {stats['processed']}\n"
|
||||
text += f"• Успешно: {stats['successful']}\n"
|
||||
text += f"• Недостаточно средств: {stats['insufficient_balance']}\n"
|
||||
text += f"• Ошибки: {stats['failed']}\n"
|
||||
|
||||
if stats['errors']:
|
||||
text += f"\n❌ Детали ошибок:\n"
|
||||
for error in stats['errors'][:5]:
|
||||
text += f"• {error}\n"
|
||||
if len(stats['errors']) > 5:
|
||||
text += f"... и еще {len(stats['errors']) - 5}\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "autopay_force_check",
|
||||
f"Processed: {stats['processed']}, Successful: {stats['successful']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in force autopay check: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка при выполнении проверки",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
def admin_autopay_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="📈 Статистика автоплатежей", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_statistics")
|
||||
async def autopay_statistics_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.answer("📊 Собираю статистику автоплатежей...")
|
||||
|
||||
stats = await db.get_autopay_statistics()
|
||||
|
||||
insufficient_balance_users = await db.get_users_with_insufficient_autopay_balance()
|
||||
|
||||
autopay_history = await db.get_autopay_history(10)
|
||||
|
||||
text = "📈 **Статистика автоплатежей**\n\n"
|
||||
|
||||
text += "📊 **Общая информация:**\n"
|
||||
text += f"• Всего подписок с автоплатежом: {stats['total_autopay_subscriptions']}\n"
|
||||
text += f"• Активных: {stats['active_autopay_subscriptions']}\n"
|
||||
text += f"• Просроченных: {stats['expired_autopay_subscriptions']}\n\n"
|
||||
|
||||
if stats['ready_for_autopay']:
|
||||
text += "🔄 **Готовы к автоплатежу:**\n"
|
||||
total_ready = 0
|
||||
for ready_info in stats['ready_for_autopay']:
|
||||
count = ready_info['count']
|
||||
days = ready_info['days']
|
||||
total_ready += count
|
||||
if count > 0:
|
||||
text += f"• За {days} дн.: {count} подписок\n"
|
||||
|
||||
if total_ready == 0:
|
||||
text += "• Нет подписок, готовых к продлению\n"
|
||||
text += "\n"
|
||||
|
||||
if insufficient_balance_users:
|
||||
text += f"⚠️ **Недостаточно средств ({len(insufficient_balance_users)}):**\n"
|
||||
for user_info in insufficient_balance_users[:5]:
|
||||
username = user_info.get('username', 'N/A')
|
||||
needed = user_info['needed_amount']
|
||||
days = user_info['expires_in_days']
|
||||
text += f"• @{username}: нужно {needed:.0f}₽ (через {days}д)\n"
|
||||
|
||||
if len(insufficient_balance_users) > 5:
|
||||
text += f"• ... и еще {len(insufficient_balance_users) - 5}\n"
|
||||
text += "\n"
|
||||
|
||||
if autopay_history:
|
||||
text += f"💳 **Последние автоплатежи:**\n"
|
||||
for payment in autopay_history[:5]:
|
||||
username = payment.get('username', 'N/A')
|
||||
amount = abs(payment['amount'])
|
||||
date_str = payment['created_at'].strftime('%d.%m %H:%M')
|
||||
status_emoji = "✅" if payment['status'] == 'completed' else "❌"
|
||||
text += f"• {status_emoji} @{username}: {amount:.0f}₽ ({date_str})\n"
|
||||
text += "\n"
|
||||
|
||||
autopay_service = kwargs.get('autopay_service')
|
||||
if autopay_service:
|
||||
service_status = await autopay_service.get_service_status()
|
||||
status_emoji = "✅" if service_status['is_running'] else "❌"
|
||||
text += f"🔧 **Статус сервиса:** {status_emoji}\n"
|
||||
text += f"• Интервал проверки: {service_status['check_interval']//60} мин\n"
|
||||
else:
|
||||
text += f"🔧 **Статус сервиса:** ❌ Недоступен\n"
|
||||
|
||||
text += f"\n🕐 Обновлено: {format_datetime(datetime.now(), user.language)}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_statistics_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting detailed autopay statistics: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения статистики",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_insufficient_balance_users")
|
||||
async def autopay_insufficient_balance_users_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
insufficient_users = await db.get_users_with_insufficient_autopay_balance()
|
||||
|
||||
if not insufficient_users:
|
||||
text = "✅ **Все пользователи с автоплатежом имеют достаточный баланс**\n\n"
|
||||
text += "Проблемных автоплатежей не обнаружено."
|
||||
else:
|
||||
text = f"⚠️ **Пользователи с недостаточным балансом ({len(insufficient_users)})**\n\n"
|
||||
|
||||
insufficient_users.sort(key=lambda x: x['expires_in_days'])
|
||||
|
||||
for user_info in insufficient_users:
|
||||
username = user_info.get('username', 'N/A')
|
||||
first_name = user_info.get('first_name', 'N/A')
|
||||
current_balance = user_info['current_balance']
|
||||
needed = user_info['needed_amount']
|
||||
price = user_info['subscription_price']
|
||||
days = user_info['expires_in_days']
|
||||
sub_name = user_info['subscription_name']
|
||||
|
||||
display_name = first_name
|
||||
if username != 'N/A':
|
||||
display_name += f" (@{username})"
|
||||
|
||||
urgency_emoji = "🔴" if days <= 1 else "🟡" if days <= 3 else "🟠"
|
||||
|
||||
text += f"{urgency_emoji} **{display_name}**\n"
|
||||
text += f" 💳 Баланс: {current_balance:.2f}₽ / {price:.2f}₽\n"
|
||||
text += f" 💸 Нужно: {needed:.2f}₽\n"
|
||||
text += f" 📋 {sub_name}\n"
|
||||
text += f" ⏰ Истекает через: {days} дн.\n\n"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_insufficient_balance_users")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting insufficient balance users: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения списка пользователей",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_subscriptions_list")
|
||||
async def autopay_subscriptions_list_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
subscriptions_with_autopay = await db.get_subscriptions_for_autopay()
|
||||
|
||||
subscriptions_data = []
|
||||
|
||||
from datetime import datetime
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
for user_sub in subscriptions_with_autopay:
|
||||
try:
|
||||
user_obj = await db.get_user_by_telegram_id(user_sub.user_id)
|
||||
username = user_obj.username if user_obj else 'N/A'
|
||||
|
||||
expires_in_days = (user_sub.expires_at - current_time).days
|
||||
|
||||
subscriptions_data.append({
|
||||
'user_id': user_sub.user_id,
|
||||
'username': username,
|
||||
'auto_pay_days_before': user_sub.auto_pay_days_before,
|
||||
'expires_in_days': expires_in_days,
|
||||
'subscription_id': user_sub.id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing subscription {user_sub.id}: {e}")
|
||||
continue
|
||||
|
||||
subscriptions_data.sort(key=lambda x: x['expires_in_days'])
|
||||
|
||||
text = f"📋 Подписки с автоплатежом ({len(subscriptions_data)})\n\n"
|
||||
|
||||
if subscriptions_data:
|
||||
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
|
||||
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
|
||||
normal = [s for s in subscriptions_data if s['expires_in_days'] > s['auto_pay_days_before']]
|
||||
|
||||
text += f"📊 Статус:\n"
|
||||
text += f"• ❌ Истекли: {len(expired)}\n"
|
||||
text += f"• ⚠️ Скоро продление: {len(due_soon)}\n"
|
||||
text += f"• ✅ Нормальные: {len(normal)}\n\n"
|
||||
|
||||
text += "👥 Нажмите на пользователя для подробностей:"
|
||||
else:
|
||||
text += "📭 Нет подписок с включенным автоплатежом"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_subscriptions_keyboard(subscriptions_data, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay subscriptions: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения списка подписок",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("autopay_user_detail_"))
|
||||
async def autopay_user_detail_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
|
||||
target_user = await db.get_user_by_telegram_id(user_id)
|
||||
if not target_user:
|
||||
await callback.answer("❌ Пользователь не найден")
|
||||
return
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user_id)
|
||||
autopay_subs = [sub for sub in user_subs if sub.auto_pay_enabled]
|
||||
|
||||
from datetime import datetime
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
text = f"👤 Пользователь с автоплатежом\n\n"
|
||||
|
||||
display_name = target_user.first_name or "N/A"
|
||||
if target_user.username:
|
||||
display_name += f" (@{target_user.username})"
|
||||
|
||||
text += f"📛 Имя: {display_name}\n"
|
||||
text += f"🆔 ID: {user_id}\n"
|
||||
text += f"💰 Баланс: {target_user.balance:.2f}₽\n\n"
|
||||
|
||||
text += f"🔄 Подписки с автоплатежом ({len(autopay_subs)}):\n\n"
|
||||
|
||||
for sub in autopay_subs:
|
||||
subscription = await db.get_subscription_by_id(sub.subscription_id)
|
||||
if not subscription:
|
||||
continue
|
||||
|
||||
days_left = (sub.expires_at - current_time).days
|
||||
|
||||
if days_left <= 0:
|
||||
status = "❌ Истекла"
|
||||
elif days_left <= sub.auto_pay_days_before:
|
||||
status = "⚠️ Скоро продление"
|
||||
else:
|
||||
status = "✅ Активна"
|
||||
|
||||
text += f"📋 {subscription.name}\n"
|
||||
text += f" {status} (через {days_left} дн.)\n"
|
||||
text += f" 💰 Цена продления: {subscription.price}₽\n"
|
||||
text += f" 📅 Продлять за: {sub.auto_pay_days_before} дн.\n"
|
||||
|
||||
if target_user.balance < subscription.price:
|
||||
needed = subscription.price - target_user.balance
|
||||
text += f" ⚠️ Нужно еще {needed:.2f}₽\n"
|
||||
else:
|
||||
text += f" ✅ Средств достаточно\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_user_detail_keyboard(user_id, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing autopay user detail: {e}")
|
||||
await callback.answer("❌ Ошибка получения информации")
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from database import Database, UserSubscription, User, Subscription
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from referral_utils import process_referral_rewards
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AutoPayService:
|
||||
|
||||
def __init__(self, db: Database, api: Optional[RemnaWaveAPI] = None, bot=None):
|
||||
self.db = db
|
||||
self.api = api
|
||||
self.bot = bot
|
||||
self.is_running = False
|
||||
self.check_task = None
|
||||
|
||||
async def start(self):
|
||||
if self.is_running:
|
||||
logger.warning("AutoPay service is already running")
|
||||
return
|
||||
|
||||
self.is_running = True
|
||||
logger.info("🔄 Starting AutoPay service...")
|
||||
|
||||
self.check_task = asyncio.create_task(self._periodic_check())
|
||||
|
||||
async def stop(self):
|
||||
if not self.is_running:
|
||||
return
|
||||
|
||||
self.is_running = False
|
||||
logger.info("⏹ Stopping AutoPay service...")
|
||||
|
||||
if self.check_task:
|
||||
self.check_task.cancel()
|
||||
try:
|
||||
await self.check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _periodic_check(self):
|
||||
while self.is_running:
|
||||
try:
|
||||
await self.process_autopayments()
|
||||
# Проверяем каждые 30 минут
|
||||
await asyncio.sleep(1800)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("AutoPay periodic check cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AutoPay periodic check: {e}")
|
||||
await asyncio.sleep(300)
|
||||
|
||||
async def process_autopayments(self) -> dict:
|
||||
logger.info("🔄 Processing autopayments...")
|
||||
|
||||
try:
|
||||
subscriptions_to_pay = await self.db.get_subscriptions_for_autopay()
|
||||
|
||||
if not subscriptions_to_pay:
|
||||
logger.info("No subscriptions ready for autopay")
|
||||
return {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
logger.info(f"Found {len(subscriptions_to_pay)} subscriptions for autopay")
|
||||
|
||||
stats = {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
for user_sub in subscriptions_to_pay:
|
||||
try:
|
||||
result = await self._process_single_autopayment(user_sub)
|
||||
stats['processed'] += 1
|
||||
|
||||
if result['success']:
|
||||
stats['successful'] += 1
|
||||
logger.info(f"✅ Autopay successful for user {user_sub.user_id}, subscription {user_sub.id}")
|
||||
elif result['reason'] == 'insufficient_balance':
|
||||
stats['insufficient_balance'] += 1
|
||||
logger.info(f"💳 Insufficient balance for user {user_sub.user_id}, subscription {user_sub.id}")
|
||||
else:
|
||||
stats['failed'] += 1
|
||||
stats['errors'].append(f"User {user_sub.user_id}: {result['reason']}")
|
||||
logger.warning(f"❌ Autopay failed for user {user_sub.user_id}: {result['reason']}")
|
||||
|
||||
except Exception as e:
|
||||
stats['processed'] += 1
|
||||
stats['failed'] += 1
|
||||
error_msg = f"User {user_sub.user_id}: {str(e)}"
|
||||
stats['errors'].append(error_msg)
|
||||
logger.error(f"Error processing autopay for user {user_sub.user_id}: {e}")
|
||||
|
||||
logger.info(f"📊 Autopay processing complete: {stats['successful']} successful, "
|
||||
f"{stats['failed']} failed, {stats['insufficient_balance']} insufficient balance")
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in process_autopayments: {e}")
|
||||
return {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': [str(e)]
|
||||
}
|
||||
|
||||
async def _process_single_autopayment(self, user_sub: UserSubscription) -> dict:
|
||||
try:
|
||||
user = await self.db.get_user_by_telegram_id(user_sub.user_id)
|
||||
if not user:
|
||||
return {'success': False, 'reason': 'User not found'}
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
return {'success': False, 'reason': 'Subscription plan not found'}
|
||||
|
||||
if subscription.is_trial:
|
||||
logger.info(f"Skipping autopay for trial subscription: user {user_sub.user_id}")
|
||||
return {'success': False, 'reason': 'Trial subscriptions are not eligible for autopay'}
|
||||
|
||||
if not user_sub.is_active or not user_sub.auto_pay_enabled:
|
||||
return {'success': False, 'reason': 'Subscription inactive or autopay disabled'}
|
||||
|
||||
if user.balance < subscription.price:
|
||||
await self._notify_insufficient_balance(user, subscription, user_sub)
|
||||
return {'success': False, 'reason': 'insufficient_balance'}
|
||||
|
||||
return await self._execute_autopayment(user, subscription, user_sub)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _process_single_autopayment: {e}")
|
||||
return {'success': False, 'reason': str(e)}
|
||||
|
||||
async def _execute_autopayment(self, user: User, subscription: Subscription, user_sub: UserSubscription) -> dict:
|
||||
try:
|
||||
user.balance -= subscription.price
|
||||
await self.db.update_user(user)
|
||||
|
||||
now = datetime.utcnow()
|
||||
if user_sub.expires_at > now:
|
||||
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
|
||||
else:
|
||||
new_expiry = now + timedelta(days=subscription.duration_days)
|
||||
|
||||
user_sub.expires_at = new_expiry
|
||||
user_sub.is_active = True
|
||||
await self.db.update_user_subscription(user_sub)
|
||||
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
remna_user_details = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
|
||||
if remna_user_details:
|
||||
user_uuid = remna_user_details.get('uuid')
|
||||
if user_uuid:
|
||||
expiry_str = new_expiry.isoformat() + 'Z'
|
||||
update_data = {
|
||||
'enable': True,
|
||||
'expireAt': expiry_str
|
||||
}
|
||||
await self.api.update_user(user_uuid, update_data)
|
||||
logger.info(f"Updated RemnaWave expiry for user {user_sub.user_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update RemnaWave expiry: {e}")
|
||||
|
||||
payment = await self.db.create_payment(
|
||||
user_id=user_sub.user_id,
|
||||
amount=-subscription.price,
|
||||
payment_type='autopay',
|
||||
description=f'Автоплатеж: {subscription.name}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await process_referral_rewards(
|
||||
user_sub.user_id,
|
||||
subscription.price,
|
||||
payment.id,
|
||||
self.db,
|
||||
self.bot,
|
||||
payment_type='autopay'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process referral rewards for autopay: {e}")
|
||||
|
||||
await self._notify_successful_autopay(user, subscription, user_sub, new_expiry)
|
||||
|
||||
return {'success': True, 'reason': 'Payment processed successfully'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing autopayment: {e}")
|
||||
try:
|
||||
user.balance += subscription.price
|
||||
await self.db.update_user(user)
|
||||
except:
|
||||
pass
|
||||
return {'success': False, 'reason': str(e)}
|
||||
|
||||
async def _notify_successful_autopay(self, user: User, subscription: Subscription,
|
||||
user_sub: UserSubscription, new_expiry: datetime):
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from utils import format_datetime
|
||||
|
||||
text = f"✅ Автоматическое продление подписки\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Списано: {subscription.price} руб.\n"
|
||||
text += f"📅 Продлено до: {format_datetime(new_expiry, user.language)}\n"
|
||||
text += f"💳 Остаток на балансе: {user.balance} руб.\n\n"
|
||||
text += f"🔄 Следующее продление произойдет автоматически за {user_sub.auto_pay_days_before} дн. до истечения.\n\n"
|
||||
text += f"ℹ️ Для отключения автоплатежа перейдите в 'Мои подписки' → выберите подписку"
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="💰 Баланс", callback_data="balance")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send autopay success notification: {e}")
|
||||
|
||||
async def _notify_insufficient_balance(self, user: User, subscription: Subscription, user_sub: UserSubscription):
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from utils import format_datetime
|
||||
|
||||
needed = subscription.price - user.balance
|
||||
days_left = (user_sub.expires_at - datetime.utcnow()).days
|
||||
|
||||
text = f"⚠️ Не удалось автоматически продлить подписку\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Нужно для продления: {subscription.price} руб.\n"
|
||||
text += f"💳 Ваш баланс: {user.balance} руб.\n"
|
||||
text += f"💸 Недостает: {needed} руб.\n\n"
|
||||
text += f"📅 Подписка истекает: {format_datetime(user_sub.expires_at, user.language)}\n"
|
||||
text += f"⏰ Осталось дней: {days_left}\n\n"
|
||||
text += f"💡 Пополните баланс для автоматического продления"
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send insufficient balance notification: {e}")
|
||||
|
||||
async def get_service_status(self) -> dict:
|
||||
return {
|
||||
'is_running': self.is_running,
|
||||
'check_interval': 1800, # 30 минут
|
||||
'has_api': self.api is not None,
|
||||
'has_bot': self.bot is not None
|
||||
}
|
||||
@@ -4,50 +4,35 @@ from typing import List, Dict
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
# Bot configuration
|
||||
BOT_TOKEN: str
|
||||
BOT_USERNAME: str
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str
|
||||
|
||||
# RemnaWave API
|
||||
REMNAWAVE_URL: str
|
||||
REMNAWAVE_TOKEN: str
|
||||
SUBSCRIPTION_BASE_URL: str
|
||||
|
||||
# Admin configuration
|
||||
ADMIN_IDS: List[int]
|
||||
SUPPORT_USERNAME: str
|
||||
DEFAULT_LANGUAGE: str
|
||||
|
||||
# Trial subscription
|
||||
TRIAL_ENABLED: bool
|
||||
TRIAL_DURATION_DAYS: int
|
||||
TRIAL_TRAFFIC_GB: int
|
||||
TRIAL_SQUAD_UUID: str
|
||||
|
||||
# Referral system
|
||||
REFERRAL_FIRST_REWARD: float
|
||||
REFERRAL_REFERRED_BONUS: float
|
||||
REFERRAL_THRESHOLD: float
|
||||
REFERRAL_PERCENTAGE: float
|
||||
|
||||
# Конфигурация мониторинга подписок
|
||||
MONITOR_ENABLED: bool = True
|
||||
MONITOR_CHECK_INTERVAL: int = 3600 # 1 час
|
||||
MONITOR_DAILY_CHECK_HOUR: int = 10 # 10:00 утра
|
||||
MONITOR_WARNING_DAYS: int = 3 # предупреждать за 3 дня
|
||||
|
||||
# Новые настройки для удаления истекших подписок
|
||||
DELETE_EXPIRED_TRIAL_DAYS: int = 1 # Удалять триальные через 1 день после истечения
|
||||
DELETE_EXPIRED_REGULAR_DAYS: int = 7 # Удалять обычные через 7 дней после истечения
|
||||
AUTO_DELETE_ENABLED: bool = False # Автоматическое удаление при ежедневной проверке
|
||||
MONITOR_CHECK_INTERVAL: int = 3600
|
||||
MONITOR_DAILY_CHECK_HOUR: int = 10
|
||||
MONITOR_WARNING_DAYS: int = 3
|
||||
DELETE_EXPIRED_TRIAL_DAYS: int = 1
|
||||
DELETE_EXPIRED_REGULAR_DAYS: int = 7
|
||||
AUTO_DELETE_ENABLED: bool = False
|
||||
|
||||
LUCKY_GAME_ENABLED: bool = True
|
||||
LUCKY_GAME_REWARD: float = 50.0 # Размер награды за выигрыш
|
||||
LUCKY_GAME_NUMBERS: int = 30 # Общее количество чисел (1-30)
|
||||
LUCKY_GAME_WINNING_COUNT: int = 3 # Количество выигрышных чисел
|
||||
LUCKY_GAME_REWARD: float = 50.0
|
||||
LUCKY_GAME_NUMBERS: int = 30
|
||||
LUCKY_GAME_WINNING_COUNT: int = 3
|
||||
|
||||
STARS_ENABLED: bool = True
|
||||
STARS_RATES: Dict[int, float] = None
|
||||
@@ -79,7 +64,6 @@ def load_config() -> Config:
|
||||
return default
|
||||
|
||||
def parse_stars_rates() -> Dict[int, float]:
|
||||
"""Парсинг курсов звезд из переменных окружения"""
|
||||
default_rates = {
|
||||
100: 150.0,
|
||||
150: 220.0,
|
||||
@@ -106,49 +90,32 @@ def load_config() -> Config:
|
||||
return Config(
|
||||
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
|
||||
BOT_USERNAME=os.getenv('BOT_USERNAME', ''),
|
||||
|
||||
# Database
|
||||
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
|
||||
|
||||
# RemnaWave API
|
||||
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
|
||||
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
|
||||
SUBSCRIPTION_BASE_URL=os.getenv('SUBSCRIPTION_BASE_URL', ''),
|
||||
|
||||
# Admin configuration
|
||||
ADMIN_IDS=parse_admin_ids(os.getenv('ADMIN_IDS', '')),
|
||||
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
|
||||
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
|
||||
|
||||
# Trial subscription
|
||||
TRIAL_ENABLED=get_bool('TRIAL_ENABLED', False),
|
||||
TRIAL_DURATION_DAYS=get_int('TRIAL_DURATION_DAYS', 3),
|
||||
TRIAL_TRAFFIC_GB=get_int('TRIAL_TRAFFIC_GB', 2),
|
||||
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', ''),
|
||||
|
||||
# Referral system
|
||||
REFERRAL_FIRST_REWARD=get_float('REFERRAL_FIRST_REWARD', 150.0),
|
||||
REFERRAL_REFERRED_BONUS=get_float('REFERRAL_REFERRED_BONUS', 150.0),
|
||||
REFERRAL_THRESHOLD=get_float('REFERRAL_THRESHOLD', 300.0),
|
||||
REFERRAL_PERCENTAGE=get_float('REFERRAL_PERCENTAGE', 0.25),
|
||||
|
||||
MONITOR_ENABLED=get_bool('MONITOR_ENABLED', True),
|
||||
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600), # 1 час по умолчанию
|
||||
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10), # 10 утра
|
||||
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3), # за 3 дня
|
||||
|
||||
# Новые настройки для удаления истекших подписок
|
||||
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600),
|
||||
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10),
|
||||
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3),
|
||||
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
|
||||
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
|
||||
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
|
||||
|
||||
# Игра удачи
|
||||
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
|
||||
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
|
||||
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
|
||||
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
|
||||
|
||||
# Telegram Stars
|
||||
STARS_ENABLED=get_bool('STARS_ENABLED', True),
|
||||
STARS_RATES=parse_stars_rates()
|
||||
)
|
||||
|
||||
+632
-97
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_
|
||||
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_, Column
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
import logging
|
||||
@@ -56,7 +56,7 @@ class Subscription(Base):
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
price: Mapped[float] = mapped_column(Float)
|
||||
duration_days: Mapped[int] = mapped_column(Integer)
|
||||
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0) # 0 = unlimited
|
||||
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0)
|
||||
squad_uuid: Mapped[str] = mapped_column(String(255))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
@@ -74,7 +74,9 @@ class UserSubscription(Base):
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
traffic_limit_gb: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow)
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow)
|
||||
auto_pay_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
auto_pay_days_before: Mapped[int] = mapped_column(Integer, default=3)
|
||||
|
||||
class Payment(Base):
|
||||
__tablename__ = 'payments'
|
||||
@@ -124,13 +126,24 @@ class StarPayment(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
|
||||
stars_amount: Mapped[int] = mapped_column(Integer) # Количество звезд
|
||||
rub_amount: Mapped[float] = mapped_column(Float) # Сумма в рублях
|
||||
status: Mapped[str] = mapped_column(String(50), default='pending') # pending, completed, cancelled
|
||||
telegram_payment_charge_id: Mapped[Optional[str]] = mapped_column(String(255)) # ID платежа от Telegram
|
||||
stars_amount: Mapped[int] = mapped_column(Integer)
|
||||
rub_amount: Mapped[float] = mapped_column(Float)
|
||||
status: Mapped[str] = mapped_column(String(50), default='pending')
|
||||
telegram_payment_charge_id: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
|
||||
class ServiceRule(Base):
|
||||
__tablename__ = 'service_rules'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(String(200), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
page_order = Column(Integer, nullable=False, default=1)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
class Database:
|
||||
def __init__(self, database_url: str):
|
||||
self.engine = create_async_engine(
|
||||
@@ -153,42 +166,140 @@ class Database:
|
||||
await self.migrate_subscription_imported_field()
|
||||
await self.migrate_referral_tables()
|
||||
await self.migrate_star_payments_table()
|
||||
await self.migrate_autopay_fields()
|
||||
|
||||
async def toggle_autopay(self, user_subscription_id: int, enabled: bool) -> bool:
|
||||
"""Включает/выключает автоплатеж для подписки"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
result = await session.execute(
|
||||
update(UserSubscription)
|
||||
.where(UserSubscription.id == user_subscription_id)
|
||||
.values(auto_pay_enabled=enabled)
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling autopay: {e}")
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
async def set_autopay_days(self, user_subscription_id: int, days_before: int) -> bool:
|
||||
"""Устанавливает количество дней до истечения для автоплатежа"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
result = await session.execute(
|
||||
update(UserSubscription)
|
||||
.where(UserSubscription.id == user_subscription_id)
|
||||
.values(auto_pay_days_before=days_before)
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting autopay days: {e}")
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
async def get_subscriptions_for_autopay(self, days_threshold: int = None) -> List[UserSubscription]:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, and_
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
if days_threshold is None:
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
conditions = []
|
||||
for days in [1, 2, 3, 5, 7]:
|
||||
threshold_date = current_time + timedelta(days=days)
|
||||
conditions.append(
|
||||
and_(
|
||||
UserSubscription.auto_pay_days_before == days,
|
||||
UserSubscription.expires_at <= threshold_date,
|
||||
UserSubscription.expires_at > current_time
|
||||
)
|
||||
)
|
||||
|
||||
from sqlalchemy import or_
|
||||
query = select(UserSubscription).where(
|
||||
and_(
|
||||
UserSubscription.auto_pay_enabled == True,
|
||||
UserSubscription.is_active == True,
|
||||
or_(*conditions)
|
||||
)
|
||||
)
|
||||
else:
|
||||
threshold_date = datetime.utcnow() + timedelta(days=days_threshold)
|
||||
query = select(UserSubscription).where(
|
||||
and_(
|
||||
UserSubscription.auto_pay_enabled == True,
|
||||
UserSubscription.is_active == True,
|
||||
UserSubscription.expires_at <= threshold_date,
|
||||
UserSubscription.expires_at > datetime.utcnow()
|
||||
)
|
||||
)
|
||||
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscriptions for autopay: {e}")
|
||||
return []
|
||||
|
||||
async def migrate_referral_tables(self):
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS referral_programs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
referrer_id BIGINT NOT NULL,
|
||||
referred_id BIGINT UNIQUE NOT NULL,
|
||||
referral_code VARCHAR(20) NOT NULL,
|
||||
first_reward_paid BOOLEAN DEFAULT FALSE,
|
||||
total_earned DOUBLE PRECISION DEFAULT 0.0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
first_reward_at TIMESTAMP,
|
||||
|
||||
INDEX idx_referrer (referrer_id),
|
||||
INDEX idx_referred (referred_id),
|
||||
INDEX idx_referral_code (referral_code)
|
||||
)
|
||||
"""))
|
||||
try:
|
||||
await conn.execute(text("SELECT 1 FROM referral_programs LIMIT 1"))
|
||||
logger.info("referral_programs table already exists")
|
||||
except Exception:
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE referral_programs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
referrer_id BIGINT NOT NULL,
|
||||
referred_id BIGINT UNIQUE NOT NULL,
|
||||
referral_code VARCHAR(20) NOT NULL,
|
||||
first_reward_paid BOOLEAN DEFAULT FALSE,
|
||||
total_earned DOUBLE PRECISION DEFAULT 0.0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
first_reward_at TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
try:
|
||||
await conn.execute(text("CREATE INDEX idx_referrer ON referral_programs(referrer_id)"))
|
||||
await conn.execute(text("CREATE INDEX idx_referred ON referral_programs(referred_id)"))
|
||||
await conn.execute(text("CREATE INDEX idx_referral_code ON referral_programs(referral_code)"))
|
||||
except Exception as e:
|
||||
logger.warning(f"Some referral_programs indexes may already exist: {e}")
|
||||
|
||||
logger.info("Created referral_programs table")
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS referral_earnings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
referrer_id BIGINT NOT NULL,
|
||||
referred_id BIGINT NOT NULL,
|
||||
amount DOUBLE PRECISION NOT NULL,
|
||||
earning_type VARCHAR(20) NOT NULL,
|
||||
related_payment_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_referrer_earnings (referrer_id),
|
||||
INDEX idx_referred_earnings (referred_id),
|
||||
INDEX idx_earning_type (earning_type)
|
||||
)
|
||||
"""))
|
||||
try:
|
||||
await conn.execute(text("SELECT 1 FROM referral_earnings LIMIT 1"))
|
||||
logger.info("referral_earnings table already exists")
|
||||
except Exception:
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE referral_earnings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
referrer_id BIGINT NOT NULL,
|
||||
referred_id BIGINT NOT NULL,
|
||||
amount DOUBLE PRECISION NOT NULL,
|
||||
earning_type VARCHAR(20) NOT NULL,
|
||||
related_payment_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
try:
|
||||
await conn.execute(text("CREATE INDEX idx_referrer_earnings ON referral_earnings(referrer_id)"))
|
||||
await conn.execute(text("CREATE INDEX idx_referred_earnings ON referral_earnings(referred_id)"))
|
||||
await conn.execute(text("CREATE INDEX idx_earning_type ON referral_earnings(earning_type)"))
|
||||
except Exception as e:
|
||||
logger.warning(f"Some referral_earnings indexes may already exist: {e}")
|
||||
|
||||
logger.info("Created referral_earnings table")
|
||||
|
||||
logger.info("Successfully created referral system tables")
|
||||
except Exception as e:
|
||||
@@ -289,13 +400,17 @@ class Database:
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE subscriptions
|
||||
ADD COLUMN IF NOT EXISTS is_imported BOOLEAN DEFAULT FALSE
|
||||
"""))
|
||||
logger.info("Successfully added is_imported field to subscriptions table")
|
||||
except Exception as e:
|
||||
logger.info(f"Migration may have already been applied: {e}")
|
||||
await conn.execute(text("SELECT is_imported FROM subscriptions LIMIT 1"))
|
||||
logger.info("is_imported field already exists")
|
||||
except Exception:
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE subscriptions
|
||||
ADD COLUMN is_imported BOOLEAN DEFAULT FALSE
|
||||
"""))
|
||||
logger.info("Successfully added is_imported field to subscriptions table")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding is_imported field: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during subscription migration: {e}")
|
||||
|
||||
@@ -612,10 +727,8 @@ class Database:
|
||||
async def update_user_subscription(self, user_subscription: UserSubscription) -> bool:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
# Устанавливаем время обновления
|
||||
user_subscription.updated_at = datetime.utcnow()
|
||||
|
||||
# Обновляем подписку
|
||||
await session.merge(user_subscription)
|
||||
await session.commit()
|
||||
return True
|
||||
@@ -627,17 +740,27 @@ class Database:
|
||||
async def migrate_user_subscriptions(self):
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN IF NOT EXISTS traffic_limit_gb INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP
|
||||
"""))
|
||||
logger.info("Successfully migrated user_subscriptions table")
|
||||
except Exception as e:
|
||||
logger.info(f"Migration may have already been applied or error occurred: {e}")
|
||||
fields_to_add = [
|
||||
("traffic_limit_gb", "INTEGER"),
|
||||
("updated_at", "TIMESTAMP")
|
||||
]
|
||||
|
||||
for field_name, field_type in fields_to_add:
|
||||
try:
|
||||
await conn.execute(text(f"SELECT {field_name} FROM user_subscriptions LIMIT 1"))
|
||||
except Exception:
|
||||
try:
|
||||
await conn.execute(text(f"""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN {field_name} {field_type}
|
||||
"""))
|
||||
logger.info(f"Added {field_name} column to user_subscriptions")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding {field_name} column: {e}")
|
||||
|
||||
logger.info("Successfully migrated user_subscriptions table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during migration: {e}")
|
||||
logger.error(f"Error during user_subscriptions migration: {e}")
|
||||
|
||||
async def get_expiring_subscriptions(self, user_id: int, days_threshold: int = 3) -> List[UserSubscription]:
|
||||
async with self.session_factory() as session:
|
||||
@@ -1216,7 +1339,7 @@ class Database:
|
||||
update(Promocode)
|
||||
.where(
|
||||
and_(
|
||||
~Promocode.code.startswith('REF'), # Исключаем реферальные
|
||||
~Promocode.code.startswith('REF'),
|
||||
Promocode.is_active == True
|
||||
)
|
||||
)
|
||||
@@ -1562,49 +1685,461 @@ class Database:
|
||||
return []
|
||||
|
||||
async def migrate_star_payments_table(self):
|
||||
"""Создание таблицы для платежей через звезды"""
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
db_type = str(conn.get_dialect().name).lower()
|
||||
try:
|
||||
await conn.execute(text("SELECT 1 FROM star_payments LIMIT 1"))
|
||||
logger.info("star_payments table already exists")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if db_type == 'postgresql':
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS star_payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
stars_amount INTEGER NOT NULL,
|
||||
rub_amount DOUBLE PRECISION NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
telegram_payment_charge_id VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_star_payments_user ON star_payments(user_id)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_star_payments_status ON star_payments(status)
|
||||
"""))
|
||||
else:
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS star_payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
stars_amount INTEGER NOT NULL,
|
||||
rub_amount DOUBLE PRECISION NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
telegram_payment_charge_id VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
|
||||
INDEX idx_star_payments_user (user_id),
|
||||
INDEX idx_star_payments_status (status)
|
||||
)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE star_payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
stars_amount INTEGER NOT NULL,
|
||||
rub_amount DOUBLE PRECISION NOT NULL,
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
telegram_payment_charge_id VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
try:
|
||||
await conn.execute(text("CREATE INDEX idx_star_payments_user ON star_payments(user_id)"))
|
||||
await conn.execute(text("CREATE INDEX idx_star_payments_status ON star_payments(status)"))
|
||||
except Exception as e:
|
||||
logger.warning(f"Some indexes may already exist: {e}")
|
||||
|
||||
logger.info("Successfully created star_payments table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating star_payments table: {e}")
|
||||
pass
|
||||
|
||||
async def create_service_rule(self, title: str, content: str, page_order: int = None) -> ServiceRule:
|
||||
async with self.session_factory() as session:
|
||||
if page_order is None:
|
||||
result = await session.execute(
|
||||
select(func.max(ServiceRule.page_order)).where(ServiceRule.is_active == True)
|
||||
)
|
||||
max_order = result.scalar() or 0
|
||||
page_order = max_order + 1
|
||||
|
||||
rule = ServiceRule(
|
||||
title=title,
|
||||
content=content,
|
||||
page_order=page_order
|
||||
)
|
||||
session.add(rule)
|
||||
await session.commit()
|
||||
await session.refresh(rule)
|
||||
return rule
|
||||
|
||||
async def get_all_service_rules(self, active_only: bool = True) -> List[ServiceRule]:
|
||||
async with self.session_factory() as session:
|
||||
query = select(ServiceRule).order_by(ServiceRule.page_order)
|
||||
if active_only:
|
||||
query = query.where(ServiceRule.is_active == True)
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_service_rule_by_id(self, rule_id: int) -> Optional[ServiceRule]:
|
||||
async with self.session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(ServiceRule).where(ServiceRule.id == rule_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def update_service_rule(self, rule: ServiceRule) -> bool:
|
||||
try:
|
||||
async with self.session_factory() as session:
|
||||
await session.merge(rule)
|
||||
await session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating service rule: {e}")
|
||||
return False
|
||||
|
||||
async def delete_service_rule(self, rule_id: int) -> bool:
|
||||
try:
|
||||
async with self.session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(ServiceRule).where(ServiceRule.id == rule_id)
|
||||
)
|
||||
rule = result.scalar_one_or_none()
|
||||
if rule:
|
||||
await session.delete(rule)
|
||||
await session.commit()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting service rule: {e}")
|
||||
return False
|
||||
|
||||
async def reorder_service_rules(self, rule_orders: List[tuple]) -> bool:
|
||||
try:
|
||||
async with self.session_factory() as session:
|
||||
for rule_id, new_order in rule_orders:
|
||||
await session.execute(
|
||||
update(ServiceRule).where(ServiceRule.id == rule_id).values(page_order=new_order)
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error reordering service rules: {e}")
|
||||
return False
|
||||
|
||||
async def migrate_service_rules_table(self):
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
try:
|
||||
await conn.execute(text("SELECT 1 FROM service_rules LIMIT 1"))
|
||||
logger.info("service_rules table already exists")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE service_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
page_order INTEGER NOT NULL DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
try:
|
||||
await conn.execute(text("CREATE INDEX idx_service_rules_order ON service_rules(page_order)"))
|
||||
await conn.execute(text("CREATE INDEX idx_service_rules_active ON service_rules(is_active)"))
|
||||
except Exception as e:
|
||||
logger.warning(f"Some indexes may already exist: {e}")
|
||||
|
||||
check_result = await conn.execute(text("SELECT COUNT(*) FROM service_rules"))
|
||||
count = check_result.scalar()
|
||||
|
||||
if count == 0:
|
||||
await conn.execute(text("""
|
||||
INSERT INTO service_rules (title, content, page_order) VALUES
|
||||
('Общие положения',
|
||||
'**1. Общие положения**
|
||||
|
||||
Настоящие Правила определяют условия использования VPN-сервиса.
|
||||
|
||||
**1.1** Используя наш сервис, вы соглашаетесь с данными правилами.
|
||||
|
||||
**1.2** Мы оставляем за собой право изменять правила в любое время.
|
||||
|
||||
**1.3** Продолжение использования сервиса после изменений означает ваше согласие с новыми условиями.',
|
||||
1),
|
||||
|
||||
('Права и обязанности',
|
||||
'**2. Права и обязанности пользователей**
|
||||
|
||||
**2.1 Права пользователя:**
|
||||
• Использовать VPN-сервис в соответствии с тарифным планом
|
||||
• Получать техническую поддержку
|
||||
• Защиту персональных данных
|
||||
|
||||
**2.2 Обязанности пользователя:**
|
||||
• Не использовать сервис для незаконной деятельности
|
||||
• Не передавать данные доступа третьим лицам
|
||||
• Своевременно оплачивать услуги
|
||||
|
||||
**2.3 Запрещается:**
|
||||
• Попытки взлома или нарушения работы сервиса
|
||||
• Спам и рассылка нежелательных сообщений
|
||||
• Нарушение авторских прав',
|
||||
2),
|
||||
|
||||
('Оплата и возврат средств',
|
||||
'**3. Условия оплаты и возврата**
|
||||
|
||||
**3.1 Оплата:**
|
||||
• Все платежи производятся в российских рублях
|
||||
• Доступны различные способы оплаты
|
||||
• Средства зачисляются автоматически или в течение 24 часов
|
||||
|
||||
**3.2 Возврат средств:**
|
||||
• Возврат возможен в течение 7 дней с момента оплаты
|
||||
• При технических проблемах возврат производится полностью
|
||||
• Обращайтесь в поддержку для возврата
|
||||
|
||||
**3.3 Скидки и промокоды:**
|
||||
• Действуют ограничения по времени и количеству использований
|
||||
• Нельзя комбинировать несколько скидок',
|
||||
3)
|
||||
"""))
|
||||
logger.info("Inserted default service rules")
|
||||
|
||||
logger.info("Successfully created service_rules table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating service_rules table: {e}")
|
||||
|
||||
async def migrate_autopay_fields(self):
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
try:
|
||||
await conn.execute(text("SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1"))
|
||||
logger.info("Autopay fields already exist")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
|
||||
"""))
|
||||
logger.info("Added auto_pay_enabled column")
|
||||
except Exception as e:
|
||||
logger.warning(f"Column auto_pay_enabled may already exist: {e}")
|
||||
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
|
||||
"""))
|
||||
logger.info("Added auto_pay_days_before column")
|
||||
except Exception as e:
|
||||
logger.warning(f"Column auto_pay_days_before may already exist: {e}")
|
||||
|
||||
logger.info("Successfully added autopay fields to user_subscriptions table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding autopay fields: {e}")
|
||||
|
||||
async def get_autopay_history(self, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, desc
|
||||
|
||||
result = await session.execute(
|
||||
select(Payment)
|
||||
.where(Payment.payment_type == 'autopay')
|
||||
.order_by(desc(Payment.created_at))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
payments = result.scalars().all()
|
||||
|
||||
autopay_history = []
|
||||
|
||||
for payment in payments:
|
||||
try:
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.telegram_id == payment.user_id)
|
||||
)
|
||||
user_obj = user_result.scalar_one_or_none()
|
||||
|
||||
autopay_history.append({
|
||||
'payment_id': payment.id,
|
||||
'user_id': payment.user_id,
|
||||
'username': user_obj.username if user_obj else 'N/A',
|
||||
'first_name': user_obj.first_name if user_obj else 'N/A',
|
||||
'amount': payment.amount,
|
||||
'description': payment.description,
|
||||
'status': payment.status,
|
||||
'created_at': payment.created_at
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing autopay history for payment {payment.id}: {e}")
|
||||
continue
|
||||
|
||||
return autopay_history
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay history: {e}")
|
||||
return []
|
||||
|
||||
async def disable_autopay_for_user(self, user_id: int) -> int:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await session.execute(
|
||||
update(UserSubscription)
|
||||
.where(
|
||||
and_(
|
||||
UserSubscription.user_id == user_id,
|
||||
UserSubscription.auto_pay_enabled == True
|
||||
)
|
||||
)
|
||||
.values(auto_pay_enabled=False)
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
return result.rowcount
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error disabling autopay for user {user_id}: {e}")
|
||||
await session.rollback()
|
||||
return 0
|
||||
|
||||
async def get_autopay_subscription_by_id(self, subscription_id: int) -> Optional[UserSubscription]:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await session.execute(
|
||||
select(UserSubscription).where(
|
||||
and_(
|
||||
UserSubscription.id == subscription_id,
|
||||
UserSubscription.auto_pay_enabled == True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay subscription {subscription_id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_autopay_statistics(self) -> Dict[str, Any]:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, func, and_, case
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
total_autopay = await session.execute(
|
||||
select(func.count(UserSubscription.id))
|
||||
.where(UserSubscription.auto_pay_enabled == True)
|
||||
)
|
||||
total_autopay_subscriptions = total_autopay.scalar() or 0
|
||||
|
||||
active_autopay = await session.execute(
|
||||
select(func.count(UserSubscription.id))
|
||||
.where(
|
||||
and_(
|
||||
UserSubscription.auto_pay_enabled == True,
|
||||
UserSubscription.is_active == True,
|
||||
UserSubscription.expires_at > datetime.utcnow()
|
||||
)
|
||||
)
|
||||
)
|
||||
active_autopay_subscriptions = active_autopay.scalar() or 0
|
||||
|
||||
expired_autopay = await session.execute(
|
||||
select(func.count(UserSubscription.id))
|
||||
.where(
|
||||
and_(
|
||||
UserSubscription.auto_pay_enabled == True,
|
||||
UserSubscription.expires_at <= datetime.utcnow()
|
||||
)
|
||||
)
|
||||
)
|
||||
expired_autopay_subscriptions = expired_autopay.scalar() or 0
|
||||
|
||||
ready_for_autopay = []
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
for days in [1, 2, 3, 5, 7]:
|
||||
threshold_date = current_time + timedelta(days=days)
|
||||
|
||||
ready_count = await session.execute(
|
||||
select(func.count(UserSubscription.id))
|
||||
.where(
|
||||
and_(
|
||||
UserSubscription.auto_pay_enabled == True,
|
||||
UserSubscription.is_active == True,
|
||||
UserSubscription.auto_pay_days_before == days,
|
||||
UserSubscription.expires_at <= threshold_date,
|
||||
UserSubscription.expires_at > current_time
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
count = ready_count.scalar() or 0
|
||||
ready_for_autopay.append({
|
||||
'days': days,
|
||||
'count': count
|
||||
})
|
||||
|
||||
return {
|
||||
'total_autopay_subscriptions': total_autopay_subscriptions,
|
||||
'active_autopay_subscriptions': active_autopay_subscriptions,
|
||||
'expired_autopay_subscriptions': expired_autopay_subscriptions,
|
||||
'ready_for_autopay': ready_for_autopay
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay statistics: {e}")
|
||||
return {
|
||||
'total_autopay_subscriptions': 0,
|
||||
'active_autopay_subscriptions': 0,
|
||||
'expired_autopay_subscriptions': 0,
|
||||
'ready_for_autopay': []
|
||||
}
|
||||
|
||||
async def get_users_with_insufficient_autopay_balance(self) -> List[Dict[str, Any]]:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, and_
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
insufficient_users = []
|
||||
|
||||
autopay_subs = await session.execute(
|
||||
select(UserSubscription)
|
||||
.where(
|
||||
and_(
|
||||
UserSubscription.auto_pay_enabled == True,
|
||||
UserSubscription.is_active == True,
|
||||
UserSubscription.expires_at > current_time
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for user_sub in autopay_subs.scalars().all():
|
||||
try:
|
||||
threshold_date = current_time + timedelta(days=user_sub.auto_pay_days_before)
|
||||
|
||||
if user_sub.expires_at <= threshold_date:
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.telegram_id == user_sub.user_id)
|
||||
)
|
||||
user_obj = user_result.scalar_one_or_none()
|
||||
|
||||
if not user_obj:
|
||||
continue
|
||||
|
||||
sub_result = await session.execute(
|
||||
select(Subscription).where(Subscription.id == user_sub.subscription_id)
|
||||
)
|
||||
subscription = sub_result.scalar_one_or_none()
|
||||
|
||||
if not subscription:
|
||||
continue
|
||||
|
||||
if user_obj.balance < subscription.price:
|
||||
days_left = (user_sub.expires_at - current_time).days
|
||||
needed_amount = subscription.price - user_obj.balance
|
||||
|
||||
insufficient_users.append({
|
||||
'user_id': user_obj.telegram_id,
|
||||
'username': user_obj.username or 'N/A',
|
||||
'first_name': user_obj.first_name or 'N/A',
|
||||
'current_balance': user_obj.balance,
|
||||
'subscription_price': subscription.price,
|
||||
'needed_amount': needed_amount,
|
||||
'subscription_name': subscription.name,
|
||||
'expires_in_days': days_left,
|
||||
'auto_pay_days_before': user_sub.auto_pay_days_before
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing user subscription {user_sub.id}: {e}")
|
||||
continue
|
||||
|
||||
return insufficient_users
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting users with insufficient autopay balance: {e}")
|
||||
return []
|
||||
|
||||
+506
-33
@@ -8,7 +8,7 @@ import logging
|
||||
import secrets
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from database import Database, User, ReferralProgram
|
||||
from database import Database, User, ReferralProgram, ServiceRule
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from keyboards import *
|
||||
from translations import t
|
||||
@@ -60,6 +60,12 @@ class BotStates(StatesGroup):
|
||||
admin_debug_user_structure = State()
|
||||
admin_rename_plans_confirm = State()
|
||||
waiting_number_choice = State()
|
||||
waiting_rule_title = State()
|
||||
waiting_rule_content = State()
|
||||
waiting_rule_order = State()
|
||||
waiting_rule_edit_title = State()
|
||||
waiting_rule_edit_content = State()
|
||||
waiting_rule_edit_order = State()
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -92,10 +98,9 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
|
||||
success = await create_referral_from_start_param(user.telegram_id, start_param, db, bot)
|
||||
|
||||
if success:
|
||||
import os
|
||||
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
|
||||
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
|
||||
|
||||
threshold = config.REFERRAL_THRESHOLD if config else 300.0
|
||||
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
|
||||
|
||||
await message.answer(
|
||||
"🎁 Добро пожаловать!\n\n"
|
||||
f"Вы перешли по реферальной ссылке! После пополнения баланса на {threshold:.0f}₽ "
|
||||
@@ -422,7 +427,6 @@ async def topup_balance_callback(callback: CallbackQuery, **kwargs):
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
# Проверяем, включены ли Telegram Stars
|
||||
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
|
||||
|
||||
text = t('topup_balance', user.language)
|
||||
@@ -533,10 +537,8 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем обычные платежи
|
||||
payments = await db.get_user_payments(user.telegram_id)
|
||||
|
||||
# Получаем платежи через звезды
|
||||
star_payments = await db.get_user_star_payments(user.telegram_id, 5)
|
||||
|
||||
if not payments and not star_payments:
|
||||
@@ -544,7 +546,6 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
else:
|
||||
text = "📊 " + t('payment_history', user.language) + ":\n\n"
|
||||
|
||||
# Объединяем и сортируем все платежи по дате
|
||||
all_payments = []
|
||||
|
||||
for payment in payments[:10]:
|
||||
@@ -567,10 +568,8 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
'stars': star_payment.stars_amount
|
||||
})
|
||||
|
||||
# Сортируем по дате (новые сначала)
|
||||
all_payments.sort(key=lambda x: x['date'], reverse=True)
|
||||
|
||||
# Показываем последние 10 платежей
|
||||
for payment in all_payments[:10]:
|
||||
date_str = format_datetime(payment['date'], user.language)
|
||||
status = format_payment_status(payment['status'], user.language)
|
||||
@@ -940,6 +939,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
|
||||
|
||||
if not is_trial and not is_imported and subscription.price > 0:
|
||||
text += f"\n\n🔄 **Автоплатеж:**\n"
|
||||
if user_sub.auto_pay_enabled:
|
||||
text += f"✅ Включен (за {user_sub.auto_pay_days_before} дн. до истечения)\n"
|
||||
text += f"💰 Стоимость продления: {subscription.price} руб.\n"
|
||||
text += f"💳 Ваш баланс: {user.balance} руб."
|
||||
|
||||
if user.balance < subscription.price:
|
||||
needed = subscription.price - user.balance
|
||||
text += f"\n⚠️ Недостаточно средств! Нужно еще {needed} руб."
|
||||
else:
|
||||
text += f"❌ Отключен\n"
|
||||
text += f"💡 Включите для автоматического продления"
|
||||
|
||||
if user_sub.short_uuid and api:
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
@@ -959,7 +972,14 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend, is_imported),
|
||||
reply_markup=user_subscription_detail_keyboard(
|
||||
user_sub_id,
|
||||
user.language,
|
||||
show_extend,
|
||||
is_imported,
|
||||
is_trial,
|
||||
user_sub.auto_pay_enabled
|
||||
),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
@@ -1254,23 +1274,48 @@ async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwarg
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('enter_promocode', user.language),
|
||||
await state.clear()
|
||||
|
||||
edited_message = await callback.message.edit_text(
|
||||
"🎁 Введите промокод:\n\n"
|
||||
"• Обычные промокоды (скидки)\n"
|
||||
"• Реферальные коды (REF...)\n\n"
|
||||
"ℹ️ После ввода вы вернетесь в главное меню",
|
||||
reply_markup=cancel_keyboard(user.language)
|
||||
)
|
||||
|
||||
await state.update_data(promocode_message_id=edited_message.message_id)
|
||||
await state.set_state(BotStates.waiting_promocode)
|
||||
|
||||
@router.message(StateFilter(BotStates.waiting_promocode))
|
||||
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Ошибка пользователя")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
code = message.text.strip().upper()
|
||||
|
||||
state_data = await state.get_data()
|
||||
promocode_message_id = state_data.get('promocode_message_id')
|
||||
|
||||
if not validate_promocode_format(code):
|
||||
await message.answer(t('invalid_input', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Неверный формат промокода",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -1278,17 +1323,50 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
|
||||
if promocode and promocode.is_active:
|
||||
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
|
||||
await message.answer(t('promocode_expired', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Промокод истек",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
if promocode.used_count >= promocode.usage_limit:
|
||||
await message.answer(t('promocode_limit', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Лимит использования промокода исчерпан",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
success = await db.use_promocode(user.telegram_id, promocode)
|
||||
|
||||
if not success:
|
||||
await message.answer(t('promocode_used', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Вы уже использовали этот промокод",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await db.add_balance(user.telegram_id, promocode.discount_amount)
|
||||
@@ -1302,18 +1380,23 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
)
|
||||
|
||||
discount_text = f"{promocode.discount_amount} руб."
|
||||
await message.answer(
|
||||
success_msg = await message.answer(
|
||||
t('promocode_success', user.language, discount=discount_text),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
logger.info(f"Deleted promocode request message {promocode_message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
log_user_action(user.telegram_id, "promocode_used", code)
|
||||
return
|
||||
|
||||
if code.startswith("REF"):
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
@@ -1326,42 +1409,93 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
|
||||
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
|
||||
if existing_reverse_referral and existing_reverse_referral.referrer_id == user.telegram_id:
|
||||
await message.answer(
|
||||
response_msg = await message.answer(
|
||||
"❌ Нельзя использовать код человека, которого вы пригласили!\n\n"
|
||||
"Взаимные рефералы не допускаются."
|
||||
"Взаимные рефералы не допускаются.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
success = await create_referral_from_promocode(user.telegram_id, code, db, bot)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
"🎉 Реферальный код активирован!\n\n"
|
||||
"После пополнения баланса на 200₽ вы получите бонус 150₽!",
|
||||
threshold = config.REFERRAL_THRESHOLD if config else 300.0
|
||||
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
|
||||
|
||||
success_msg = await message.answer(
|
||||
f"🎉 Реферальный код активирован!\n\n"
|
||||
f"После пополнения баланса на {threshold:.0f}₽ вы получите бонус {referred_bonus:.0f}₽!",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
logger.info(f"Deleted promocode request message {promocode_message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
log_user_action(user.telegram_id, "referral_code_used", code)
|
||||
return
|
||||
else:
|
||||
existing_referral = await db.get_referral_by_referred_id(user.telegram_id)
|
||||
if existing_referral:
|
||||
await message.answer("❌ Вы уже использовали реферальный код!")
|
||||
error_text = "❌ Вы уже использовали реферальный код!"
|
||||
else:
|
||||
await message.answer("❌ Неверный реферальный код!")
|
||||
error_text = "❌ Неверный реферальный код!"
|
||||
|
||||
response_msg = await message.answer(
|
||||
error_text,
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await message.answer(t('promocode_not_found', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Промокод не найден\n\n"
|
||||
"Проверьте правильность ввода и попробуйте снова через главное меню.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
logger.info(f"Deleted promocode request message {promocode_message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling promocode: {e}")
|
||||
await message.answer(
|
||||
t('error_occurred', user.language),
|
||||
response_msg = await message.answer(
|
||||
"❌ Произошла ошибка при обработке промокода",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
|
||||
@router.callback_query(F.data == "referral_program")
|
||||
async def referral_program_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
@@ -1545,3 +1679,342 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing referrals: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки")
|
||||
|
||||
@router.callback_query(F.data == "service_rules")
|
||||
async def service_rules_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
rules = await db.get_all_service_rules(active_only=True)
|
||||
|
||||
if not rules:
|
||||
await callback.message.edit_text(
|
||||
"📜 Правила сервиса пока не добавлены администратором.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
await show_rules_page(callback, rules, 0, user.language)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing service rules: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки правил")
|
||||
|
||||
async def show_rules_page(callback: CallbackQuery, rules: List[ServiceRule], page_index: int, lang: str = 'ru'):
|
||||
if page_index < 0 or page_index >= len(rules):
|
||||
page_index = 0
|
||||
|
||||
rule = rules[page_index]
|
||||
total_pages = len(rules)
|
||||
|
||||
safe_title = rule.title.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace(']', '\\]').replace('`', '\\`')
|
||||
text = f"📜 **{safe_title}**\n\n"
|
||||
text += rule.content
|
||||
|
||||
if total_pages > 1:
|
||||
text += f"\n\n📄 Страница {page_index + 1} из {total_pages}"
|
||||
|
||||
if len(text) > 4000:
|
||||
text = text[:3950] + "\n\n... (текст обрезан)"
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=service_rules_keyboard(page_index, total_pages, lang),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except Exception as markdown_error:
|
||||
logger.warning(f"Markdown parsing failed in user rules, retrying without markdown: {markdown_error}")
|
||||
try:
|
||||
clean_text = text.replace('**', '').replace('*', '').replace('_', '').replace('`', '')
|
||||
await callback.message.edit_text(
|
||||
clean_text,
|
||||
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
|
||||
)
|
||||
except Exception as edit_error:
|
||||
await callback.message.answer(
|
||||
clean_text,
|
||||
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("rules_page_"))
|
||||
async def rules_page_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
page_index = int(callback.data.split("_")[-1])
|
||||
rules = await db.get_all_service_rules(active_only=True)
|
||||
|
||||
if not rules:
|
||||
await callback.answer("❌ Правила не найдены")
|
||||
return
|
||||
|
||||
await show_rules_page(callback, rules, page_index, user.language)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Error parsing rules page number: {e}")
|
||||
await callback.answer("❌ Ошибка навигации")
|
||||
except Exception as e:
|
||||
logger.error(f"Error switching rules page: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки страницы")
|
||||
|
||||
@router.callback_query(F.data == "cancel", StateFilter(BotStates.waiting_promocode))
|
||||
async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
db = kwargs.get('db')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
show_trial = False
|
||||
show_lucky_game = True
|
||||
|
||||
if config and config.TRIAL_ENABLED and db:
|
||||
try:
|
||||
has_used = await db.has_used_trial(user.telegram_id)
|
||||
show_trial = not has_used
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking trial availability: {e}")
|
||||
|
||||
if config:
|
||||
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('main_menu', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("toggle_autopay_"))
|
||||
async def toggle_autopay_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer("❌ План подписки не найден")
|
||||
return
|
||||
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
|
||||
return
|
||||
|
||||
new_state = not user_sub.auto_pay_enabled
|
||||
success = await db.toggle_autopay(user_sub_id, new_state)
|
||||
|
||||
if success:
|
||||
status_text = "включен" if new_state else "отключен"
|
||||
await callback.answer(f"✅ Автоплатеж {status_text}")
|
||||
|
||||
updated_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if updated_sub:
|
||||
await autopay_settings_callback(callback, db, user=user)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка изменения настроек")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling autopay: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
@router.callback_query(F.data == "autopay_help")
|
||||
async def autopay_help_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
text = "🔄 **Автоплатеж - как это работает?**\n\n"
|
||||
|
||||
text += "**🎯 Что такое автоплатеж:**\n"
|
||||
text += "Автоматическое продление подписки за несколько дней до истечения срока действия.\n\n"
|
||||
|
||||
text += "**⚙️ Как настроить:**\n"
|
||||
text += "1. Перейдите в 'Мои подписки'\n"
|
||||
text += "2. Выберите нужную подписку\n"
|
||||
text += "3. Нажмите 'Настроить автоплатеж'\n"
|
||||
text += "4. Включите автоплатеж и выберите за сколько дней до истечения продлевать\n\n"
|
||||
|
||||
text += "**💰 Требования:**\n"
|
||||
text += "• На балансе должно быть достаточно средств\n"
|
||||
text += "• Подписка должна быть активной\n"
|
||||
text += "• Автоплатеж работает только с обычными подписками\n\n"
|
||||
|
||||
text += "**📅 Варианты продления:**\n"
|
||||
text += "• За 1 день - для тех, кто следит за балансом\n"
|
||||
text += "• За 3 дня - рекомендуемый вариант\n"
|
||||
text += "• За 5 дней - для подстраховки\n"
|
||||
text += "• За 7 дней - максимальный запас времени\n\n"
|
||||
|
||||
text += "**🔔 Уведомления:**\n"
|
||||
text += "Вы получите уведомление при успешном продлении или если недостаточно средств.\n\n"
|
||||
|
||||
text += "**❓ Проблемы:**\n"
|
||||
text += "Если автоплатеж не сработал, проверьте баланс и обратитесь в поддержку."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_help_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "autopay_insufficient_balance_help")
|
||||
async def autopay_insufficient_balance_help_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
text = "⚠️ **Недостаточно средств для автоплатежа**\n\n"
|
||||
|
||||
text += f"💳 **Ваш текущий баланс:** {user.balance}₽\n\n"
|
||||
|
||||
text += "**🔄 Что происходит:**\n"
|
||||
text += "Система пыталась автоматически продлить вашу подписку, но на балансе недостаточно средств.\n\n"
|
||||
|
||||
text += "**✅ Что нужно сделать:**\n"
|
||||
text += "1. Пополните баланс на нужную сумму\n"
|
||||
text += "2. Автоплатеж попробует снова при следующей проверке\n"
|
||||
text += "3. Или продлите подписку вручную\n\n"
|
||||
|
||||
text += "**⏰ Важно:**\n"
|
||||
text += "Пополните баланс до истечения срока подписки, иначе доступ будет приостановлен."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_help_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("autopay_settings_"))
|
||||
async def autopay_settings_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer("❌ План подписки не найден")
|
||||
return
|
||||
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
|
||||
return
|
||||
|
||||
from datetime import datetime
|
||||
now = datetime.utcnow()
|
||||
days_left = (user_sub.expires_at - now).days
|
||||
|
||||
text = f"⚙️ Настройки автоплатежа\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Цена продления: {subscription.price} руб.\n"
|
||||
text += f"⏱ Длительность: {subscription.duration_days} дн.\n"
|
||||
text += f"📅 Истекает через: {days_left} дн.\n\n"
|
||||
|
||||
status_emoji = "✅" if user_sub.auto_pay_enabled else "❌"
|
||||
status_text = "Включен" if user_sub.auto_pay_enabled else "Отключен"
|
||||
text += f"🔄 Автоплатеж: {status_emoji} {status_text}\n"
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
text += f"📅 Продлять за: {user_sub.auto_pay_days_before} дн. до истечения\n\n"
|
||||
|
||||
if user.balance >= subscription.price:
|
||||
text += f"✅ На балансе достаточно средств ({user.balance}₽)\n"
|
||||
text += f"💡 Подписка будет автоматически продлена"
|
||||
else:
|
||||
needed = subscription.price - user.balance
|
||||
text += f"⚠️ Недостаточно средств на балансе!\n"
|
||||
text += f"💳 Ваш баланс: {user.balance}₽\n"
|
||||
text += f"💸 Нужно еще: {needed}₽\n"
|
||||
text += f"💡 Пополните баланс для автоматического продления"
|
||||
else:
|
||||
text += f"\n💡 Включите автоплатеж для автоматического продления подписки\n"
|
||||
text += f"🎯 Рекомендуем включить за 3 дня до истечения"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_settings_keyboard(user_sub_id, user_sub, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing autopay settings: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки настроек")
|
||||
|
||||
@router.callback_query(F.data.startswith("autopay_days_"))
|
||||
async def autopay_days_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
parts = callback.data.split("_")
|
||||
user_sub_id = int(parts[2])
|
||||
days = int(parts[3])
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
success = await db.set_autopay_days(user_sub_id, days)
|
||||
|
||||
if success:
|
||||
await callback.answer(f"✅ Установлено: продлять за {days} дн.")
|
||||
|
||||
updated_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if updated_sub:
|
||||
await autopay_settings_callback(callback, db, user=user)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка обновления настроек")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting autopay days: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
+214
-11
@@ -35,7 +35,8 @@ def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: boo
|
||||
[
|
||||
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
|
||||
],
|
||||
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
|
||||
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")],
|
||||
[InlineKeyboardButton(text="📜 Правила сервиса", callback_data="service_rules")]
|
||||
])
|
||||
|
||||
if is_admin:
|
||||
@@ -109,22 +110,52 @@ def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru'
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru',
|
||||
show_extend: bool = False, is_imported: bool = False,
|
||||
is_trial: bool = False, autopay_enabled: bool = False) -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if is_imported:
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
|
||||
elif is_trial:
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="🛒 Купить полную подписку", callback_data="buy_subscription")])
|
||||
else:
|
||||
if show_extend:
|
||||
buttons.append([InlineKeyboardButton(text="⏰ " + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
|
||||
if autopay_enabled:
|
||||
autopay_text = "🔄✅ Настроить автоплатеж"
|
||||
else:
|
||||
autopay_text = "🔄❌ Настроить автоплатеж"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=autopay_text, callback_data=f"autopay_settings_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_confirmation_keyboard(subscription_id: int, action: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_autopay_{action}_{subscription_id}"),
|
||||
InlineKeyboardButton(text="❌ Нет", callback_data=f"autopay_settings_{subscription_id}")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_help_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура помощи по автоплатежам"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
@@ -159,13 +190,17 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
|
||||
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"),
|
||||
InlineKeyboardButton(text="🔄 Автоплатежи", callback_data="admin_autopay")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system"),
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
|
||||
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
|
||||
@@ -613,19 +648,15 @@ def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура с вариантами пополнения через звезды"""
|
||||
buttons = []
|
||||
|
||||
# Сортируем по количеству звезд
|
||||
sorted_rates = sorted(stars_rates.items())
|
||||
|
||||
# Создаем кнопки по 2 в ряд
|
||||
for i in range(0, len(sorted_rates), 2):
|
||||
row = []
|
||||
for j in range(2):
|
||||
if i + j < len(sorted_rates):
|
||||
stars, rubles = sorted_rates[i + j]
|
||||
# Определяем выгодность предложения
|
||||
if stars >= 500:
|
||||
emoji = "🔥" # Выгодное предложение
|
||||
elif stars >= 250:
|
||||
@@ -640,14 +671,186 @@ def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> Inl
|
||||
))
|
||||
buttons.append(row)
|
||||
|
||||
# Добавляем кнопку назад
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def stars_payment_keyboard(stars_amount: int, rub_amount: float, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура подтверждения платежа через звезды (не используется в send_invoice)"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def service_rules_keyboard(current_page: int, total_pages: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if total_pages > 1:
|
||||
nav_row = []
|
||||
|
||||
if current_page > 0:
|
||||
nav_row.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"rules_page_{current_page - 1}"))
|
||||
|
||||
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
|
||||
|
||||
if current_page < total_pages - 1:
|
||||
nav_row.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"rules_page_{current_page + 1}"))
|
||||
|
||||
buttons.append(nav_row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_rules_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 Список правил", callback_data="admin_rules_list")],
|
||||
[InlineKeyboardButton(text="➕ Добавить страницу", callback_data="admin_rules_create")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_rules_list_keyboard(rules, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
for rule in rules:
|
||||
status_emoji = "🟢" if rule.is_active else "🔴"
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{status_emoji} {rule.page_order}. {rule.title}",
|
||||
callback_data=f"admin_rule_view_{rule.id}"
|
||||
)
|
||||
])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="➕ Добавить страницу", callback_data="admin_rules_create")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_rules")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_rule_edit_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✏️ Редактировать заголовок", callback_data=f"admin_rule_edit_title_{rule_id}"),
|
||||
InlineKeyboardButton(text="📝 Редактировать содержимое", callback_data=f"admin_rule_edit_content_{rule_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄 Изменить порядок", callback_data=f"admin_rule_edit_order_{rule_id}"),
|
||||
InlineKeyboardButton(text="🟢/🔴 Вкл/Выкл", callback_data=f"admin_rule_toggle_{rule_id}")
|
||||
],
|
||||
[InlineKeyboardButton(text="🗑 Удалить", callback_data=f"admin_rule_delete_{rule_id}")],
|
||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_rules_list")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_rule_confirm_delete_{rule_id}"),
|
||||
InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_rule_view_{rule_id}")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_settings_keyboard(user_sub_id: int, user_sub, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
toggle_text = "❌ Отключить автоплатеж"
|
||||
toggle_callback = f"toggle_autopay_{user_sub_id}"
|
||||
else:
|
||||
toggle_text = "✅ Включить автоплатеж"
|
||||
toggle_callback = f"toggle_autopay_{user_sub_id}"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)])
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
buttons.append([InlineKeyboardButton(text="📅 Настроить дни до продления", callback_data="noop")])
|
||||
|
||||
days_row = []
|
||||
for days in [1, 3, 5, 7]:
|
||||
emoji = "🔹" if user_sub.auto_pay_days_before == days else "⚪"
|
||||
days_row.append(InlineKeyboardButton(
|
||||
text=f"{emoji} {days}д",
|
||||
callback_data=f"autopay_days_{user_sub_id}_{days}"
|
||||
))
|
||||
buttons.append(days_row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 К подписке", callback_data=f"view_sub_{user_sub_id}")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_status_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_subscriptions_keyboard(subscriptions_data: List[Dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
|
||||
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
|
||||
|
||||
critical_subs = expired + due_soon
|
||||
|
||||
for sub_data in critical_subs[:8]:
|
||||
username = sub_data['username'] if sub_data['username'] != 'N/A' else f"ID:{sub_data['user_id']}"
|
||||
days = sub_data['expires_in_days']
|
||||
|
||||
if days <= 0:
|
||||
status_emoji = "❌"
|
||||
status_text = f"Истекла"
|
||||
elif days <= sub_data['auto_pay_days_before']:
|
||||
status_emoji = "⚠️"
|
||||
status_text = f"Через {days}д"
|
||||
else:
|
||||
status_emoji = "✅"
|
||||
status_text = f"Через {days}д"
|
||||
|
||||
button_text = f"{status_emoji} @{username} ({status_text})"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"autopay_user_detail_{sub_data['user_id']}"
|
||||
)
|
||||
])
|
||||
|
||||
if len(subscriptions_data) > 8:
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"... и еще {len(subscriptions_data) - 8}",
|
||||
callback_data="noop"
|
||||
)
|
||||
])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_user_detail_keyboard(user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 Добавить баланс", callback_data=f"admin_add_balance_to_{user_id}")],
|
||||
[InlineKeyboardButton(text="📋 Управление подписками", callback_data=f"admin_user_subscriptions_{user_id}")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data=f"autopay_user_detail_{user_id}")],
|
||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="autopay_subscriptions_list")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
|
||||
def autopay_statistics_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⚠️ Недостаточно средств", callback_data="autopay_insufficient_balance_users")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@@ -8,6 +8,7 @@ from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
from lucky_game import lucky_game_router
|
||||
from stars_handlers import stars_router
|
||||
from autopay_service import AutoPayService
|
||||
|
||||
print("🚀 Запуск бота...")
|
||||
print(f"📍 Рабочая директория: {os.getcwd()}")
|
||||
@@ -46,6 +47,40 @@ class BotApplication:
|
||||
self.bot = None
|
||||
self.dp = None
|
||||
self.monitor_service = None
|
||||
self.autopay_service = None
|
||||
|
||||
async def _init_autopay_service(self):
|
||||
"""Инициализирует сервис автоплатежей"""
|
||||
try:
|
||||
logger.info("🔧 Initializing autopay service...")
|
||||
|
||||
if not self.bot:
|
||||
logger.error("❌ Bot instance is None, cannot initialize autopay")
|
||||
return
|
||||
|
||||
if not self.db:
|
||||
logger.error("❌ Database instance is None, cannot initialize autopay")
|
||||
return
|
||||
|
||||
self.autopay_service = AutoPayService(self.db, self.api, self.bot)
|
||||
|
||||
self.dp.workflow_data["autopay_service"] = self.autopay_service
|
||||
logger.info("✅ Autopay service added to workflow_data")
|
||||
|
||||
logger.info("🚀 Starting autopay service...")
|
||||
await self.autopay_service.start()
|
||||
|
||||
status = await self.autopay_service.get_service_status()
|
||||
if status['is_running']:
|
||||
logger.info("✅ Autopay service started successfully")
|
||||
logger.info(f"📊 Autopay status: interval=30min")
|
||||
else:
|
||||
logger.warning("⚠️ Autopay service created but not running")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to initialize autopay service: {e}", exc_info=True)
|
||||
logger.warning("⚠️ Continuing without autopay service")
|
||||
self.autopay_service = None
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
@@ -97,6 +132,7 @@ class BotApplication:
|
||||
self._setup_dispatcher()
|
||||
|
||||
await self._init_monitor_service()
|
||||
await self._init_autopay_service()
|
||||
|
||||
if self.config.STARS_ENABLED:
|
||||
logger.info("✅ Telegram Stars пополнение включено")
|
||||
@@ -249,6 +285,13 @@ class BotApplication:
|
||||
|
||||
async def shutdown(self):
|
||||
logger.info("Shutting down bot...")
|
||||
|
||||
if self.autopay_service:
|
||||
try:
|
||||
await self.autopay_service.stop()
|
||||
logger.info("Autopay service stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping autopay service: {e}")
|
||||
|
||||
if self.monitor_service:
|
||||
try:
|
||||
|
||||
+94
-27
@@ -17,7 +17,6 @@ stars_router = Router()
|
||||
|
||||
@stars_router.callback_query(F.data == "topup_stars")
|
||||
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
|
||||
"""Показать варианты пополнения через звезды"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
@@ -70,7 +69,6 @@ async def topup_stars_callback(callback: CallbackQuery, **kwargs):
|
||||
|
||||
@stars_router.callback_query(F.data.startswith("buy_stars_"))
|
||||
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Обработка покупки звезд"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
@@ -117,8 +115,8 @@ async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
f"💳 **Оплата через Telegram Stars**\n\n"
|
||||
f"⭐ Количество звезд: {stars_amount}\n"
|
||||
f"💰 Сумма пополнения: {rub_amount:.0f}₽\n\n"
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе выше\n\n"
|
||||
f"❌ Если передумали - нажмите кнопку ниже",
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️\n\n"
|
||||
f"Если передумали - нажмите кнопку ниже \n\n❌ Отменить платеж",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
|
||||
@@ -130,7 +128,7 @@ async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
f"💳 **Оплата через Telegram Stars**\n\n"
|
||||
f"⭐ Количество звезд: {stars_amount}\n"
|
||||
f"💰 Сумма пополнения: {rub_amount:.0f}₽\n\n"
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе выше",
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
|
||||
@@ -164,7 +162,6 @@ async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
|
||||
@stars_router.pre_checkout_query()
|
||||
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
|
||||
"""Обработка pre-checkout запроса для звезд"""
|
||||
try:
|
||||
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
|
||||
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
|
||||
@@ -194,7 +191,6 @@ async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: D
|
||||
|
||||
@stars_router.message(F.successful_payment)
|
||||
async def successful_payment_handler(message: Message, db: Database, **kwargs):
|
||||
"""Обработка успешного платежа звездами"""
|
||||
user = kwargs.get('user')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
@@ -267,7 +263,6 @@ async def successful_payment_handler(message: Message, db: Database, **kwargs):
|
||||
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
|
||||
|
||||
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
|
||||
"""Получить информацию о курсах звезд"""
|
||||
if not stars_rates:
|
||||
return "Курсы не настроены"
|
||||
|
||||
@@ -284,6 +279,7 @@ def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
|
||||
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
@@ -311,30 +307,101 @@ async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **
|
||||
if success:
|
||||
await callback.answer("✅ Платеж отменен", show_alert=True)
|
||||
|
||||
if config and config.STARS_RATES:
|
||||
if bot:
|
||||
invoice_deleted = False
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
await bot.delete_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id
|
||||
)
|
||||
invoice_deleted = True
|
||||
logger.info(f"Deleted invoice message {callback.message.message_id} for cancelled payment {payment_id}")
|
||||
except TelegramBadRequest as e:
|
||||
logger.warning(f"Failed to delete invoice message: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting invoice message: {e}")
|
||||
|
||||
if invoice_deleted:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
text = "❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\n"
|
||||
text += "🚀 **Преимущества:**\n"
|
||||
text += "• Мгновенное зачисление\n"
|
||||
text += "• Безопасные платежи через Telegram\n"
|
||||
text += "• Без комиссий и скрытых платежей\n\n"
|
||||
text += "💎 **Доступные варианты:**\n"
|
||||
|
||||
sorted_rates = sorted(config.STARS_RATES.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
if stars >= 500:
|
||||
bonus_text = " 🔥 Выгодно!"
|
||||
elif stars >= 250:
|
||||
bonus_text = " 💎 Хорошо!"
|
||||
else:
|
||||
bonus_text = ""
|
||||
text += f"• {stars} ⭐ → {rubles:.0f}₽{bonus_text}\n"
|
||||
|
||||
text += f"\n💡 Выберите другой вариант:"
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text=text,
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен\n\n💰 Управление балансом:",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send menu after invoice deletion: {e}")
|
||||
else:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send fallback message: {e}")
|
||||
else:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
if config and config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
logger.warning("Failed to edit message when bot instance not available")
|
||||
|
||||
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
|
||||
else:
|
||||
|
||||
+3
-19
@@ -147,12 +147,9 @@ class SubscriptionMonitorService:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
|
||||
await asyncio.sleep(3600) # Wait 1 hour before retry
|
||||
await asyncio.sleep(3600)
|
||||
|
||||
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Удаляет истекшие триальные подписки
|
||||
"""
|
||||
try:
|
||||
logger.info("🗑️ Starting deletion of expired trial subscriptions...")
|
||||
|
||||
@@ -179,12 +176,10 @@ class SubscriptionMonitorService:
|
||||
for user_sub in user_subs:
|
||||
results['total_checked'] += 1
|
||||
|
||||
# Получаем информацию о подписке
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription or not subscription.is_trial:
|
||||
continue
|
||||
|
||||
# Проверяем, истекла ли подписка достаточно давно
|
||||
expires_at_utc = user_sub.expires_at
|
||||
if expires_at_utc.tzinfo is None:
|
||||
expires_at_utc = expires_at_utc.replace(tzinfo=None)
|
||||
@@ -192,12 +187,11 @@ class SubscriptionMonitorService:
|
||||
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
if expires_at_utc > cutoff_date and not force:
|
||||
continue # Подписка еще не готова к удалению
|
||||
continue
|
||||
|
||||
logger.info(f"🗑️ Deleting expired trial subscription '{subscription.name}' for user {user.telegram_id} "
|
||||
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
|
||||
|
||||
# Удаляем из RemnaWave API
|
||||
api_deleted = False
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
@@ -213,7 +207,6 @@ class SubscriptionMonitorService:
|
||||
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
|
||||
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
|
||||
|
||||
# Удаляем из базы данных
|
||||
db_deleted = await self.db.delete_user_subscription(user_sub.id)
|
||||
if db_deleted:
|
||||
results['deleted_from_db'] += 1
|
||||
@@ -248,9 +241,6 @@ class SubscriptionMonitorService:
|
||||
}
|
||||
|
||||
async def delete_expired_regular_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Удаляет истекшие обычные подписки
|
||||
"""
|
||||
try:
|
||||
logger.info("🗑️ Starting deletion of expired regular subscriptions...")
|
||||
|
||||
@@ -277,16 +267,13 @@ class SubscriptionMonitorService:
|
||||
for user_sub in user_subs:
|
||||
results['total_checked'] += 1
|
||||
|
||||
# Получаем информацию о подписке
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription or subscription.is_trial:
|
||||
continue
|
||||
|
||||
# Пропускаем импортированные подписки (они не удаляются)
|
||||
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
|
||||
continue
|
||||
|
||||
# Проверяем, истекла ли подписка достаточно давно
|
||||
expires_at_utc = user_sub.expires_at
|
||||
if expires_at_utc.tzinfo is None:
|
||||
expires_at_utc = expires_at_utc.replace(tzinfo=None)
|
||||
@@ -294,12 +281,11 @@ class SubscriptionMonitorService:
|
||||
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
if expires_at_utc > cutoff_date and not force:
|
||||
continue # Подписка еще не готова к удалению
|
||||
continue
|
||||
|
||||
logger.info(f"🗑️ Deleting expired regular subscription '{subscription.name}' for user {user.telegram_id} "
|
||||
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
|
||||
|
||||
# Удаляем из RemnaWave API
|
||||
api_deleted = False
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
@@ -315,7 +301,6 @@ class SubscriptionMonitorService:
|
||||
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
|
||||
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
|
||||
|
||||
# Удаляем из базы данных
|
||||
db_deleted = await self.db.delete_user_subscription(user_sub.id)
|
||||
if db_deleted:
|
||||
results['deleted_from_db'] += 1
|
||||
@@ -536,7 +521,6 @@ class SubscriptionMonitorService:
|
||||
deactivated_count = await self.deactivate_expired_subscriptions()
|
||||
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
|
||||
|
||||
# Автоматическое удаление, если включено в конфиге
|
||||
deleted_trials = 0
|
||||
deleted_regular = 0
|
||||
if getattr(self.config, 'AUTO_DELETE_ENABLED', False):
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@ TRANSLATIONS = {
|
||||
'admin_panel': 'Админ панель',
|
||||
'trial_subscription': '🆓 Тестовая подписка',
|
||||
'trial_not_available': '❌ Тестовая подписка недоступна',
|
||||
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
|
||||
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе: \n\n"Мои подписки"',
|
||||
'trial_error': '❌ Ошибка при создании тестовой подписки',
|
||||
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
|
||||
'trial_info': 'Попробуйте наш сервис бесплатно! \n\n🧪 Тестовая подписка выдается на {days} дня!\n\nОграничение по трафику - {traffic} гб',
|
||||
'subscriptions_list': '📋 Список подписок в продаже:',
|
||||
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
|
||||
'topup_balance': 'Пополнить баланс',
|
||||
|
||||
Reference in New Issue
Block a user