Files
remnawave-bedolaga-telegram…/app/handlers/admin/contests.py

984 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import math
from datetime import datetime, timezone, time
from zoneinfo import ZoneInfo
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.referral_contest import (
create_referral_contest,
get_contest_events_count,
get_contest_leaderboard,
get_referral_contest,
get_referral_contests_count,
list_referral_contests,
toggle_referral_contest,
update_referral_contest,
delete_referral_contest,
)
from app.keyboards.admin import (
get_admin_contests_keyboard,
get_admin_contests_root_keyboard,
get_admin_pagination_keyboard,
get_contest_mode_keyboard,
get_referral_contest_manage_keyboard,
)
from app.localization.texts import get_texts
from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
logger = logging.getLogger(__name__)
PAGE_SIZE = 5
def _ensure_timezone(tz_name: str) -> ZoneInfo:
try:
return ZoneInfo(tz_name)
except Exception: # noqa: BLE001
logger.warning("Не удалось загрузить TZ %s, используем UTC", tz_name)
return ZoneInfo("UTC")
def _format_contest_summary(contest, texts, tz: ZoneInfo) -> str:
start_local = contest.start_at if contest.start_at.tzinfo else contest.start_at.replace(tzinfo=timezone.utc)
end_local = contest.end_at if contest.end_at.tzinfo else contest.end_at.replace(tzinfo=timezone.utc)
start_local = start_local.astimezone(tz)
end_local = end_local.astimezone(tz)
status = texts.t("ADMIN_CONTEST_STATUS_ACTIVE", "🟢 Активен") if contest.is_active else texts.t(
"ADMIN_CONTEST_STATUS_INACTIVE", "⚪️ Выключен"
)
period = (
f"{start_local.strftime('%d.%m %H:%M')}"
f"{end_local.strftime('%d.%m %H:%M')} ({tz.key})"
)
summary_time = contest.daily_summary_time.strftime("%H:%M") if contest.daily_summary_time else "12:00"
summary_times = contest.daily_summary_times or summary_time
parts = [
f"{status}",
f"Период: <b>{period}</b>",
f"Дневная сводка: <b>{summary_times}</b>",
]
if contest.prize_text:
parts.append(texts.t("ADMIN_CONTEST_PRIZE", "Приз: {prize}").format(prize=contest.prize_text))
if contest.last_daily_summary_date:
parts.append(
texts.t("ADMIN_CONTEST_LAST_DAILY", "Последняя сводка: {date}").format(
date=contest.last_daily_summary_date.strftime("%d.%m")
)
)
return "\n".join(parts)
def _parse_local_datetime(value: str, tz: ZoneInfo) -> datetime | None:
try:
dt = datetime.strptime(value.strip(), "%d.%m.%Y %H:%M")
except ValueError:
return None
return dt.replace(tzinfo=tz)
def _parse_time(value: str):
try:
return datetime.strptime(value.strip(), "%H:%M").time()
except ValueError:
return None
def _parse_times(value: str) -> list[time]:
times: list[time] = []
for part in value.split(","):
part = part.strip()
if not part:
continue
parsed = _parse_time(part)
if parsed:
times.append(parsed)
return times
@admin_required
@error_handler
async def show_contests_menu(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
texts = get_texts(db_user.language)
if not settings.is_contests_enabled():
await callback.message.edit_text(
texts.t(
"ADMIN_CONTESTS_DISABLED",
"Конкурсы отключены через переменную окружения CONTESTS_ENABLED.",
),
reply_markup=get_admin_contests_root_keyboard(db_user.language),
)
await callback.answer()
return
await callback.message.edit_text(
texts.t("ADMIN_CONTESTS_TITLE", "🏆 <b>Конкурсы</b>\n\nВыберите действие:"),
reply_markup=get_admin_contests_root_keyboard(db_user.language),
)
await callback.answer()
@admin_required
@error_handler
async def show_referral_contests_menu(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_CONTESTS_TITLE", "🏆 <b>Конкурсы</b>\n\nВыберите действие:"),
reply_markup=get_admin_contests_keyboard(db_user.language),
)
await callback.answer()
@admin_required
@error_handler
async def list_contests(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t(
"ADMIN_CONTESTS_DISABLED",
"Конкурсы отключены через переменную окружения CONTESTS_ENABLED.",
),
show_alert=True,
)
return
page = 1
if callback.data.startswith("admin_contests_list_page_"):
try:
page = int(callback.data.split("_")[-1])
except Exception:
page = 1
total = await get_referral_contests_count(db)
total_pages = max(1, math.ceil(total / PAGE_SIZE))
page = max(1, min(page, total_pages))
offset = (page - 1) * PAGE_SIZE
contests = await list_referral_contests(db, limit=PAGE_SIZE, offset=offset)
texts = get_texts(db_user.language)
lines = [texts.t("ADMIN_CONTESTS_LIST_HEADER", "🏆 <b>Конкурсы</b>\n")]
if not contests:
lines.append(texts.t("ADMIN_CONTESTS_EMPTY", "Пока нет созданных конкурсов."))
else:
for contest in contests:
lines.append(f"• <b>{contest.title}</b> (#{contest.id})")
contest_tz = _ensure_timezone(contest.timezone or settings.TIMEZONE)
lines.append(_format_contest_summary(contest, texts, contest_tz))
lines.append("")
keyboard_rows: list[list[types.InlineKeyboardButton]] = []
for contest in contests:
title = contest.title if len(contest.title) <= 25 else contest.title[:22] + "..."
keyboard_rows.append(
[
types.InlineKeyboardButton(
text=f"🔎 {title}",
callback_data=f"admin_contest_view_{contest.id}",
)
]
)
pagination = get_admin_pagination_keyboard(
page,
total_pages,
"admin_contests_list",
back_callback="admin_contests",
language=db_user.language,
)
keyboard_rows.extend(pagination.inline_keyboard)
await callback.message.edit_text(
"\n".join(lines),
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
)
await callback.answer()
@admin_required
@error_handler
async def show_contest_details(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
texts = get_texts(db_user.language)
if not contest:
await callback.answer(texts.t("ADMIN_CONTEST_NOT_FOUND", "Конкурс не найден."), show_alert=True)
return
tz = _ensure_timezone(contest.timezone or settings.TIMEZONE)
leaderboard = await get_contest_leaderboard(db, contest.id, limit=5)
total_events = await get_contest_events_count(db, contest.id)
lines = [
f"🏆 <b>{contest.title}</b>",
_format_contest_summary(contest, texts, tz),
texts.t("ADMIN_CONTEST_TOTAL_EVENTS", "Зачётов: <b>{count}</b>").format(count=total_events),
]
if contest.description:
lines.append("")
lines.append(contest.description)
if leaderboard:
lines.append("")
lines.append(texts.t("ADMIN_CONTEST_LEADERBOARD_TITLE", "📊 Топ участников:"))
for idx, (user, score, _) in enumerate(leaderboard, start=1):
lines.append(f"{idx}. {user.full_name}{score}")
await callback.message.edit_text(
"\n".join(lines),
reply_markup=get_referral_contest_manage_keyboard(
contest.id,
is_active=contest.is_active,
can_delete=(
not contest.is_active
and (contest.end_at.replace(tzinfo=timezone.utc) if contest.end_at.tzinfo is None else contest.end_at)
< datetime.now(timezone.utc)
),
language=db_user.language,
),
)
await callback.answer()
@admin_required
@error_handler
async def toggle_contest(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
if not contest:
await callback.answer("Конкурс не найден", show_alert=True)
return
await toggle_referral_contest(db, contest, not contest.is_active)
await show_contest_details(callback, db_user, db)
@admin_required
@error_handler
async def prompt_edit_summary_times(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
state: FSMContext,
):
texts = get_texts(db_user.language)
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
if not contest:
await callback.answer(texts.t("ADMIN_CONTEST_NOT_FOUND", "Конкурс не найден."), show_alert=True)
return
await state.set_state(AdminStates.editing_referral_contest_summary_times)
await state.update_data(contest_id=contest_id)
kb = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.BACK,
callback_data=f"admin_contest_view_{contest_id}",
)
]
]
)
await callback.message.edit_text(
texts.t(
"ADMIN_CONTEST_ENTER_DAILY_TIME",
"Во сколько отправлять ежедневные итоги? Формат ЧЧ:ММ или несколько через запятую (12:00,18:00).",
),
reply_markup=kb,
)
await callback.answer()
@admin_required
@error_handler
async def process_edit_summary_times(
message: types.Message,
state: FSMContext,
db_user,
db: AsyncSession,
):
texts = get_texts(db_user.language)
data = await state.get_data()
contest_id = data.get("contest_id")
if not contest_id:
await message.answer(texts.ERROR)
await state.clear()
return
times = _parse_times(message.text or "")
summary_time = times[0] if times else _parse_time(message.text or "")
if not summary_time:
await message.answer(
texts.t("ADMIN_CONTEST_INVALID_TIME", "Не удалось распознать время. Формат: 12:00 или 12:00,18:00")
)
await state.clear()
return
contest = await get_referral_contest(db, int(contest_id))
if not contest:
await message.answer(texts.t("ADMIN_CONTEST_NOT_FOUND", "Конкурс не найден."))
await state.clear()
return
await update_referral_contest(
db,
contest,
daily_summary_time=summary_time,
daily_summary_times=",".join(t.strftime("%H:%M") for t in times) if times else None,
)
await message.answer(texts.t("ADMIN_UPDATED", "Обновлено"))
await state.clear()
@admin_required
@error_handler
async def delete_contest(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
texts = get_texts(db_user.language)
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
if not contest:
await callback.answer(texts.t("ADMIN_CONTEST_NOT_FOUND", "Конкурс не найден."), show_alert=True)
return
now_utc = datetime.utcnow()
if contest.is_active or contest.end_at > now_utc:
await callback.answer(
texts.t("ADMIN_CONTEST_DELETE_RESTRICT", "Удалять можно только завершённые конкурсы."),
show_alert=True,
)
return
await delete_referral_contest(db, contest)
await callback.answer(texts.t("ADMIN_CONTEST_DELETED", "Конкурс удалён."), show_alert=True)
await list_contests(callback, db_user, db)
@admin_required
@error_handler
async def show_leaderboard(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
texts = get_texts(db_user.language)
if not contest:
await callback.answer(texts.t("ADMIN_CONTEST_NOT_FOUND", "Конкурс не найден."), show_alert=True)
return
leaderboard = await get_contest_leaderboard(db, contest_id, limit=10)
if not leaderboard:
await callback.answer(texts.t("ADMIN_CONTEST_EMPTY_LEADERBOARD", "Пока нет участников."), show_alert=True)
return
lines = [
texts.t("ADMIN_CONTEST_LEADERBOARD_TITLE", "📊 Топ участников:"),
]
for idx, (user, score, _) in enumerate(leaderboard, start=1):
lines.append(f"{idx}. {user.full_name} ({user.telegram_id}) — {score}")
await callback.message.edit_text(
"\n".join(lines),
reply_markup=get_referral_contest_manage_keyboard(
contest_id, is_active=contest.is_active, language=db_user.language
),
)
await callback.answer()
@admin_required
@error_handler
async def start_contest_creation(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
state: FSMContext,
):
texts = get_texts(db_user.language)
if not settings.is_contests_enabled():
await callback.answer(
texts.t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
await state.clear()
await state.set_state(AdminStates.creating_referral_contest_mode)
await callback.message.edit_text(
texts.t(
"ADMIN_CONTEST_MODE_PROMPT",
"Выберите условие зачёта: реферал должен купить подписку или достаточно регистрации.",
),
reply_markup=get_contest_mode_keyboard(db_user.language),
)
await callback.answer()
@admin_required
@error_handler
async def select_contest_mode(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
state: FSMContext,
):
texts = get_texts(db_user.language)
mode = "referral_paid" if callback.data == "admin_contest_mode_paid" else "referral_registered"
await state.update_data(contest_type=mode)
await state.set_state(AdminStates.creating_referral_contest_title)
await callback.message.edit_text(
texts.t("ADMIN_CONTEST_ENTER_TITLE", "Введите название конкурса:"),
reply_markup=None,
)
await callback.answer()
@admin_required
@error_handler
async def process_title(message: types.Message, state: FSMContext, db_user, db: AsyncSession):
title = message.text.strip()
texts = get_texts(db_user.language)
await state.update_data(title=title)
await state.set_state(AdminStates.creating_referral_contest_description)
await message.answer(
texts.t("ADMIN_CONTEST_ENTER_DESCRIPTION", "Опишите конкурс (или отправьте '-' чтобы пропустить):")
)
@admin_required
@error_handler
async def process_description(message: types.Message, state: FSMContext, db_user, db: AsyncSession):
description = message.text.strip()
if description in {"-", "skip", "пропустить"}:
description = None
await state.update_data(description=description)
await state.set_state(AdminStates.creating_referral_contest_prize)
texts = get_texts(db_user.language)
await message.answer(
texts.t("ADMIN_CONTEST_ENTER_PRIZE", "Укажите призы/выгоды конкурса (или '-' чтобы пропустить):")
)
@admin_required
@error_handler
async def process_prize(message: types.Message, state: FSMContext, db_user, db: AsyncSession):
prize = message.text.strip()
if prize in {"-", "skip", "пропустить"}:
prize = None
await state.update_data(prize=prize)
await state.set_state(AdminStates.creating_referral_contest_start)
texts = get_texts(db_user.language)
await message.answer(
texts.t(
"ADMIN_CONTEST_ENTER_START",
"Введите дату и время старта (дд.мм.гггг чч:мм) по вашему часовому поясу:",
)
)
@admin_required
@error_handler
async def process_start_date(message: types.Message, state: FSMContext, db_user, db: AsyncSession):
tz = _ensure_timezone(settings.TIMEZONE)
start_dt = _parse_local_datetime(message.text, tz)
texts = get_texts(db_user.language)
if not start_dt:
await message.answer(
texts.t("ADMIN_CONTEST_INVALID_DATE", "Не удалось распознать дату. Формат: 01.06.2024 12:00")
)
return
await state.update_data(start_at=start_dt.isoformat())
await state.set_state(AdminStates.creating_referral_contest_end)
await message.answer(
texts.t(
"ADMIN_CONTEST_ENTER_END",
"Введите дату и время окончания (дд.мм.гггг чч:мм) по вашему часовому поясу:",
)
)
@admin_required
@error_handler
async def process_end_date(message: types.Message, state: FSMContext, db_user, db: AsyncSession):
tz = _ensure_timezone(settings.TIMEZONE)
end_dt = _parse_local_datetime(message.text, tz)
texts = get_texts(db_user.language)
if not end_dt:
await message.answer(
texts.t("ADMIN_CONTEST_INVALID_DATE", "Не удалось распознать дату. Формат: 01.06.2024 12:00")
)
return
data = await state.get_data()
start_raw = data.get("start_at")
start_dt = datetime.fromisoformat(start_raw) if start_raw else None
if start_dt and end_dt <= start_dt:
await message.answer(
texts.t(
"ADMIN_CONTEST_END_BEFORE_START",
"Дата окончания должна быть позже даты начала.",
)
)
return
await state.update_data(end_at=end_dt.isoformat())
await state.set_state(AdminStates.creating_referral_contest_time)
await message.answer(
texts.t(
"ADMIN_CONTEST_ENTER_DAILY_TIME",
"Во сколько отправлять ежедневные итоги? Укажите время в формате ЧЧ:ММ (например, 12:00).",
)
)
@admin_required
@error_handler
async def finalize_contest_creation(message: types.Message, state: FSMContext, db_user, db: AsyncSession):
times = _parse_times(message.text or "")
summary_time = times[0] if times else _parse_time(message.text)
texts = get_texts(db_user.language)
if not summary_time:
await message.answer(
texts.t("ADMIN_CONTEST_INVALID_TIME", "Не удалось распознать время. Формат: 12:00 или 12:00,18:00")
)
return
data = await state.get_data()
tz = _ensure_timezone(settings.TIMEZONE)
start_at_raw = data.get("start_at")
end_at_raw = data.get("end_at")
if not start_at_raw or not end_at_raw:
await message.answer(texts.t("ADMIN_CONTEST_INVALID_DATE", "Не удалось распознать дату."))
return
start_at = (
datetime.fromisoformat(start_at_raw)
.astimezone(timezone.utc)
.replace(tzinfo=None)
)
end_at = (
datetime.fromisoformat(end_at_raw)
.astimezone(timezone.utc)
.replace(tzinfo=None)
)
contest_type = data.get("contest_type") or "referral_paid"
contest = await create_referral_contest(
db,
title=data.get("title"),
description=data.get("description"),
prize_text=data.get("prize"),
contest_type=contest_type,
start_at=start_at,
end_at=end_at,
daily_summary_time=summary_time,
daily_summary_times=",".join(t.strftime("%H:%M") for t in times) if times else None,
timezone_name=tz.key,
created_by=db_user.id,
)
await state.clear()
await message.answer(
texts.t("ADMIN_CONTEST_CREATED", "Конкурс создан!"),
reply_markup=get_referral_contest_manage_keyboard(
contest.id,
is_active=contest.is_active,
language=db_user.language,
),
)
@admin_required
@error_handler
async def show_detailed_stats(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
if not contest:
await callback.answer("Конкурс не найден.", show_alert=True)
return
from app.services.referral_contest_service import referral_contest_service
stats = await referral_contest_service.get_detailed_contest_stats(db, contest_id)
# Общее сообщение с основной статистикой
general_lines = [
"📈 <b>Статистика конкурса</b>",
f"🏆 {contest.title}",
"",
f"👥 Участников (рефереров): <b>{stats['total_participants']}</b>",
f"📨 Приглашено рефералов: <b>{stats['total_invited']}</b>",
"",
f"💳 Рефералов оплатили: <b>{stats.get('paid_count', 0)}</b>",
f"❌ Рефералов не оплатили: <b>{stats.get('unpaid_count', 0)}</b>",
"",
"<b>💰 СУММЫ:</b>",
f" 🛒 Покупки подписок: <b>{stats.get('subscription_total', 0) // 100} руб.</b>",
f" 📥 Пополнения баланса: <b>{stats.get('deposit_total', 0) // 100} руб.</b>",
]
await callback.message.edit_text(
"\n".join(general_lines),
reply_markup=get_referral_contest_manage_keyboard(
contest_id, is_active=contest.is_active, language=db_user.language
),
)
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: admin_contest_detailed_stats_page_{contest_id}_page_{page}
parts = callback.data.split("_")
contest_id = int(parts[5]) # contest_id после page
page = int(parts[7]) # page после второго page
# Получаем 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.extend([
f"• <b>{p['full_name']}</b>",
f" 📨 Приглашено: {p['total_referrals']}",
f" 💰 Оплатили: {p['paid_referrals']}",
f"Не оплатили: {p['unpaid_referrals']}",
f" 💵 Сумма: {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.edit_text(
"\n".join(lines),
reply_markup=pagination,
)
await callback.answer()
@admin_required
@error_handler
async def sync_contest(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
"""Синхронизировать события конкурса с реальными платежами."""
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
if not contest:
await callback.answer("Конкурс не найден.", show_alert=True)
return
await callback.answer("🔄 Синхронизация запущена...", show_alert=False)
from app.services.referral_contest_service import referral_contest_service
stats = await referral_contest_service.sync_contest(db, contest_id)
if "error" in stats:
await callback.message.answer(
f"❌ Ошибка синхронизации:\n{stats['error']}",
)
return
# Формируем сообщение о результатах
# Показываем точные даты которые использовались для фильтрации
start_str = stats.get('contest_start', contest.start_at.isoformat())
end_str = stats.get('contest_end', contest.end_at.isoformat())
lines = [
"✅ <b>Синхронизация завершена!</b>",
"",
f"📊 <b>Конкурс:</b> {contest.title}",
f"📅 <b>Период:</b> {contest.start_at.strftime('%d.%m.%Y')} - {contest.end_at.strftime('%d.%m.%Y')}",
f"🔍 <b>Фильтр транзакций:</b>",
f" <code>{start_str}</code>",
f" <code>{end_str}</code>",
"",
f"📝 Рефералов в периоде: <b>{stats.get('total_events', 0)}</b>",
f"⚠️ Отфильтровано (вне периода): <b>{stats.get('filtered_out_events', 0)}</b>",
f"📊 Всего событий в БД: <b>{stats.get('total_all_events', 0)}</b>",
"",
f"🔄 Обновлено сумм: <b>{stats.get('updated', 0)}</b>",
f"⏭ Без изменений: <b>{stats.get('skipped', 0)}</b>",
"",
f"💳 Рефералов оплатили: <b>{stats.get('paid_count', 0)}</b>",
f"❌ Рефералов не оплатили: <b>{stats.get('unpaid_count', 0)}</b>",
"",
"<b>💰 СУММЫ:</b>",
f" 🛒 Покупки подписок: <b>{stats.get('subscription_total', 0) // 100} руб.</b>",
f" 📥 Пополнения баланса: <b>{stats.get('deposit_total', 0) // 100} руб.</b>",
]
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
back_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⬅️ Назад к конкурсу", callback_data=f"admin_contest_view_{contest_id}")]
])
await callback.message.answer(
"\n".join(lines),
parse_mode="HTML",
reply_markup=back_keyboard,
)
# Обновляем основное сообщение с новой статистикой
detailed_stats = await referral_contest_service.get_detailed_contest_stats(db, contest_id)
general_lines = [
f"🏆 <b>{contest.title}</b>",
f"📅 Период: {contest.start_at.strftime('%d.%m.%Y')} - {contest.end_at.strftime('%d.%m.%Y')}",
"",
f"👥 Участников (рефереров): <b>{detailed_stats['total_participants']}</b>",
f"📨 Приглашено рефералов: <b>{detailed_stats['total_invited']}</b>",
"",
f"💳 Рефералов оплатили: <b>{detailed_stats.get('paid_count', 0)}</b>",
f"❌ Рефералов не оплатили: <b>{detailed_stats.get('unpaid_count', 0)}</b>",
f"🛒 Покупки подписок: <b>{detailed_stats['total_paid_amount'] // 100} руб.</b>",
]
await callback.message.edit_text(
"\n".join(general_lines),
reply_markup=get_referral_contest_manage_keyboard(
contest_id, is_active=contest.is_active, language=db_user.language
),
)
@admin_required
@error_handler
async def debug_contest_transactions(
callback: types.CallbackQuery,
db_user,
db: AsyncSession,
):
"""Показать транзакции рефералов конкурса для отладки."""
if not settings.is_contests_enabled():
await callback.answer(
get_texts(db_user.language).t("ADMIN_CONTESTS_DISABLED", "Конкурсы отключены."),
show_alert=True,
)
return
contest_id = int(callback.data.split("_")[-1])
contest = await get_referral_contest(db, contest_id)
if not contest:
await callback.answer("Конкурс не найден.", show_alert=True)
return
await callback.answer("🔍 Загружаю данные...", show_alert=False)
from app.database.crud.referral_contest import debug_contest_transactions as debug_txs
debug_data = await debug_txs(db, contest_id, limit=10)
if "error" in debug_data:
await callback.message.answer(f"❌ Ошибка: {debug_data['error']}")
return
deposit_total = debug_data.get('deposit_total_kopeks', 0) // 100
subscription_total = debug_data.get('subscription_total_kopeks', 0) // 100
lines = [
"🔍 <b>Отладка транзакций конкурса</b>",
"",
f"📊 <b>Конкурс:</b> {contest.title}",
f"📅 <b>Период фильтрации:</b>",
f" Начало: <code>{debug_data.get('contest_start')}</code>",
f" Конец: <code>{debug_data.get('contest_end')}</code>",
f"👥 <b>Рефералов в периоде:</b> {debug_data.get('referral_count', 0)}",
f"⚠️ <b>Отфильтровано (вне периода):</b> {debug_data.get('filtered_out', 0)}",
f"📊 <b>Всего событий в БД:</b> {debug_data.get('total_all_events', 0)}",
"",
"<b>💰 СУММЫ:</b>",
f" 📥 Пополнения баланса: <b>{deposit_total}</b> руб.",
f" 🛒 Покупки подписок: <b>{subscription_total}</b> руб.",
"",
]
# Показываем транзакции В периоде
txs_in = debug_data.get('transactions_in_period', [])
if txs_in:
lines.append(f"✅ <b>Транзакции в периоде</b> (первые {len(txs_in)}):")
for tx in txs_in[:5]: # Показываем максимум 5
lines.append(
f"{tx['created_at'][:10]} | "
f"{tx['type']} | "
f"{tx['amount_kopeks'] // 100}₽ | "
f"user={tx['user_id']}"
)
if len(txs_in) > 5:
lines.append(f" ... и ещё {len(txs_in) - 5}")
else:
lines.append("✅ <b>Транзакций в периоде:</b> 0")
lines.append("")
# Показываем транзакции ВНЕ периода
txs_out = debug_data.get('transactions_outside_period', [])
if txs_out:
lines.append(f"❌ <b>Транзакции вне периода</b> (первые {len(txs_out)}):")
for tx in txs_out[:5]:
lines.append(
f"{tx['created_at'][:10]} | "
f"{tx['type']} | "
f"{tx['amount_kopeks'] // 100}₽ | "
f"user={tx['user_id']}"
)
if len(txs_out) > 5:
lines.append(f" ... и ещё {len(txs_out) - 5}")
else:
lines.append("❌ <b>Транзакций вне периода:</b> 0")
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
back_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⬅️ Назад к конкурсу", callback_data=f"admin_contest_view_{contest_id}")]
])
await callback.message.answer(
"\n".join(lines),
parse_mode="HTML",
reply_markup=back_keyboard,
)
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")
dp.callback_query.register(list_contests, F.data == "admin_contests_list")
dp.callback_query.register(list_contests, F.data.startswith("admin_contests_list_page_"))
dp.callback_query.register(show_contest_details, F.data.startswith("admin_contest_view_"))
dp.callback_query.register(toggle_contest, F.data.startswith("admin_contest_toggle_"))
dp.callback_query.register(prompt_edit_summary_times, F.data.startswith("admin_contest_edit_times_"))
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(sync_contest, F.data.startswith("admin_contest_sync_"))
dp.callback_query.register(debug_contest_transactions, F.data.startswith("admin_contest_debug_"))
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"]))
dp.message.register(process_title, AdminStates.creating_referral_contest_title)
dp.message.register(process_description, AdminStates.creating_referral_contest_description)
dp.message.register(process_prize, AdminStates.creating_referral_contest_prize)
dp.message.register(process_start_date, AdminStates.creating_referral_contest_start)
dp.message.register(process_end_date, AdminStates.creating_referral_contest_end)
dp.message.register(finalize_contest_creation, AdminStates.creating_referral_contest_time)
dp.message.register(process_edit_summary_times, AdminStates.editing_referral_contest_summary_times)