diff --git a/app/config.py b/app/config.py index a87c8dd0..434c9e30 100644 --- a/app/config.py +++ b/app/config.py @@ -182,6 +182,8 @@ class Settings(BaseSettings): AUTOPAY_WARNING_DAYS: str = "3,1" + ENABLE_AUTOPAY: bool = False + DEFAULT_AUTOPAY_ENABLED: bool = False DEFAULT_AUTOPAY_DAYS_BEFORE: int = 3 MIN_BALANCE_FOR_AUTOPAY_KOPEKS: int = 10000 diff --git a/app/database/crud/referral_contest.py b/app/database/crud/referral_contest.py index 652e2ee3..89e7426b 100644 --- a/app/database/crud/referral_contest.py +++ b/app/database/crud/referral_contest.py @@ -248,6 +248,16 @@ async def get_contest_events_count( return int(result.scalar_one()) +async def get_contest_events( + db: AsyncSession, + contest_id: int, +) -> List[ReferralContestEvent]: + result = await db.execute( + select(ReferralContestEvent).where(ReferralContestEvent.contest_id == contest_id) + ) + return list(result.scalars().all()) + + async def mark_daily_summary_sent( db: AsyncSession, contest: ReferralContest, diff --git a/app/handlers/admin/contests.py b/app/handlers/admin/contests.py index e88ce95b..7402cdab 100644 --- a/app/handlers/admin/contests.py +++ b/app/handlers/admin/contests.py @@ -683,7 +683,8 @@ async def show_detailed_stats( from app.services.referral_contest_service import referral_contest_service stats = await referral_contest_service.get_detailed_contest_stats(db, contest_id) - lines = [ + # Общее сообщение с основной статистикой + general_lines = [ "📈 Детальная статистика конкурса", f"🏆 {contest.title}", "", @@ -692,23 +693,74 @@ async def show_detailed_stats( f"💰 Оплатили подписок: {stats['total_paid_amount'] // 100} руб.", f"❌ Не оплатили: {stats['total_unpaid']}", "", - "📊 По участникам:", + "📊 Детали по участникам ниже:", ] - for p in stats['participants']: - lines.append( - f"• {p['full_name']}: приглашено {p['total_referrals']}, оплатили {p['paid_referrals']}, не оплатили {p['unpaid_referrals']}, сумма {p['total_paid_amount'] // 100} руб." - ) - await callback.message.edit_text( - "\n".join(lines), + "\n".join(general_lines), reply_markup=get_referral_contest_manage_keyboard( contest_id, is_active=contest.is_active, language=db_user.language ), ) + + # Пагинация участников + if stats['participants']: + await show_detailed_stats_page(callback, db_user, db, contest_id=contest_id, page=1, stats=stats) + else: + await callback.message.reply_text("📊 Нет участников с рефералами.") + await callback.answer() +@admin_required +@error_handler +async def show_detailed_stats_page( + callback: types.CallbackQuery, + db_user, + db: AsyncSession, + contest_id: int = None, + page: int = 1, + stats: dict = None, +): + if contest_id is None or stats is None: + # Парсим из callback.data + parts = callback.data.split("_") + contest_id = int(parts[-2]) + page = int(parts[-1]) + + # Получаем stats если не переданы + from app.services.referral_contest_service import referral_contest_service + stats = await referral_contest_service.get_detailed_contest_stats(db, contest_id) + + participants = stats['participants'] + total_participants = len(participants) + PAGE_SIZE = 10 + total_pages = math.ceil(total_participants / PAGE_SIZE) + + page = max(1, min(page, total_pages)) + offset = (page - 1) * PAGE_SIZE + page_participants = participants[offset:offset + PAGE_SIZE] + + lines = [f"📊 По участникам (страница {page}/{total_pages}):"] + for p in page_participants: + lines.append( + f"• {p['full_name']}: приглашено {p['total_referrals']}, оплатили {p['paid_referrals']}, не оплатили {p['unpaid_referrals']}, сумма {p['total_paid_amount'] // 100} руб." + ) + + pagination = get_admin_pagination_keyboard( + page, + total_pages, + f"admin_contest_detailed_stats_page_{contest_id}", + back_callback=f"admin_contest_view_{contest_id}", + language=db_user.language, + ) + + await callback.message.reply_text( + "\n".join(lines), + reply_markup=pagination, + ) + + def register_handlers(dp: Dispatcher): dp.callback_query.register(show_contests_menu, F.data == "admin_contests") dp.callback_query.register(show_referral_contests_menu, F.data == "admin_contests_referral") @@ -720,6 +772,7 @@ def register_handlers(dp: Dispatcher): dp.callback_query.register(delete_contest, F.data.startswith("admin_contest_delete_")) dp.callback_query.register(show_leaderboard, F.data.startswith("admin_contest_leaderboard_")) dp.callback_query.register(show_detailed_stats, F.data.startswith("admin_contest_detailed_stats_")) + dp.callback_query.register(show_detailed_stats_page, F.data.startswith("admin_contest_detailed_stats_page_")) dp.callback_query.register(start_contest_creation, F.data == "admin_contests_create") dp.callback_query.register(select_contest_mode, F.data.in_(["admin_contest_mode_paid", "admin_contest_mode_registered"])) diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index 9a0f6f95..a681d2bc 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -214,7 +214,8 @@ class MonitoringService: await self._check_trial_inactivity_notifications(db) await self._check_trial_channel_subscriptions(db) await self._check_expired_subscription_followups(db) - await self._process_autopayments(db) + if settings.ENABLE_AUTOPAY: + await self._process_autopayments(db) await self._cleanup_inactive_users(db) await self._sync_with_remnawave(db) @@ -931,7 +932,7 @@ class MonitoringService: autopay_subscriptions = [] for sub in all_autopay_subscriptions: days_before_expiry = (sub.end_date - current_time).days - if days_before_expiry <= sub.autopay_days_before: + if days_before_expiry <= min(sub.autopay_days_before, 3): autopay_subscriptions.append(sub) processed_count = 0 @@ -1059,12 +1060,16 @@ class MonitoringService: texts = get_texts(user.language) days_text = format_days_declension(days, user.language) - if subscription.autopay_enabled: - autopay_status = "✅ Включен - подписка продлится автоматически" - action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}" + if settings.ENABLE_AUTOPAY: + if subscription.autopay_enabled: + autopay_status = "✅ Включен - подписка продлится автоматически" + action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}" + else: + autopay_status = "❌ Отключен - не забудьте продлить вручную!" + action_text = "💡 Включите автоплатеж или продлите подписку вручную" else: autopay_status = "❌ Отключен - не забудьте продлить вручную!" - action_text = "💡 Включите автоплатеж или продлите подписку вручную" + action_text = "💡 Продлите подписку вручную" message = f""" ⚠️ Подписка истекает через {days_text}!