Compare commits

...

8 Commits

Author SHA1 Message Date
Fr1ngg edc891c93d Update README.md 2025-08-11 22:06:49 +03:00
Fr1ngg 2d706a4be0 Update keyboards.py 2025-08-11 22:01:38 +03:00
Fr1ngg 32cb81ef34 Update handlers.py 2025-08-11 22:01:09 +03:00
Fr1ngg 88aa3c4a3d Update database.py
autopay mod
2025-08-11 22:00:43 +03:00
Fr1ngg e813d3ccb7 Create autopay_service.py 2025-08-11 22:00:19 +03:00
Fr1ngg a24d7a4e24 Update main.py 2025-08-11 21:59:51 +03:00
Fr1ngg 39987b9717 Update admin_handlers.py
autopay mod
2025-08-11 21:59:31 +03:00
Fr1ngg c77a1ea4f8 Update README.md 2025-08-11 17:57:35 +03:00
7 changed files with 1669 additions and 210 deletions
+14 -1
View File
@@ -20,7 +20,8 @@
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
- 🌐 **Мультиязычность** - русский и английский
- 📋 **Правила сервиса**
- 📋 **Правила сервиса**
- ♾️ **Автопродление** - С настройкой вкл/выкл и кол-вом дней до автопродления.
### ⚙️ Для администраторов
- 📊 **Полная статистика** - пользователи, платежи, подписки
@@ -32,6 +33,7 @@
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
- 📋 **Правила сервиса** - Настройка страниц из админ панели
- ♾️ **Автопродление** - Статус сервиса, статистика автопродления, принудительное продление
## 📋 Требования
@@ -103,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` |
#### 💬 Сервис мониторинга сообщений
| Переменная | Описание | По умолчанию |
+413 -1
View File
@@ -7295,7 +7295,6 @@ async def admin_rule_delete_callback(callback: CallbackQuery, user: User, db: Da
@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
@@ -7327,3 +7326,416 @@ async def admin_rule_confirm_delete_callback(callback: CallbackQuery, user: User
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("❌ Ошибка получения информации")
+284
View File
@@ -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
}
+529 -201
View File
@@ -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'
@@ -164,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:
@@ -300,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}")
@@ -636,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:
@@ -1571,52 +1685,37 @@ 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:
@@ -1695,123 +1794,352 @@ class Database:
async def migrate_service_rules_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 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
)
"""))
if db_type == 'postgresql':
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("""
CREATE TABLE IF NOT EXISTS 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
)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_service_rules_order ON service_rules(page_order)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_service_rules_active ON service_rules(is_active)
"""))
await conn.execute(text("""
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$ language 'plpgsql'
"""))
await conn.execute(text("""
DROP TRIGGER IF EXISTS update_service_rules_updated_at ON service_rules
"""))
await conn.execute(text("""
CREATE TRIGGER update_service_rules_updated_at
BEFORE UPDATE ON service_rules
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column()
"""))
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")
else:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS 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 ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_service_rules_order (page_order),
INDEX idx_service_rules_active (is_active)
)
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}")
pass
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 []
+245 -1
View File
@@ -939,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)
@@ -958,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
)
@@ -1774,3 +1795,226 @@ async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext,
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("❌ Ошибка операции")
+141 -6
View File
@@ -110,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=[
[
@@ -160,14 +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="📜 Правила сервиса", callback_data="admin_rules"), # НОВАЯ КНОПКА
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")]
@@ -638,7 +671,6 @@ 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)
@@ -719,3 +751,106 @@ def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> Inline
]
])
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
+43
View File
@@ -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: