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}!