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"Период: {period}", f"Дневная сводка: {summary_times}", ] 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", "🏆 Конкурсы\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", "🏆 Конкурсы\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", "🏆 Конкурсы\n")] if not contests: lines.append(texts.t("ADMIN_CONTESTS_EMPTY", "Пока нет созданных конкурсов.")) else: for contest in contests: lines.append(f"• {contest.title} (#{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"🏆 {contest.title}", _format_contest_summary(contest, texts, tz), texts.t("ADMIN_CONTEST_TOTAL_EVENTS", "Зачётов: {count}").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 = [ "📈 Статистика конкурса", f"🏆 {contest.title}", "", f"👥 Участников (рефереров): {stats['total_participants']}", f"📨 Приглашено рефералов: {stats['total_invited']}", "", f"💳 Рефералов оплатили: {stats.get('paid_count', 0)}", f"❌ Рефералов не оплатили: {stats.get('unpaid_count', 0)}", "", "💰 СУММЫ:", f" 🛒 Покупки подписок: {stats.get('subscription_total', 0) // 100} руб.", f" 📥 Пополнения баланса: {stats.get('deposit_total', 0) // 100} руб.", ] 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"• {p['full_name']}", 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 = [ "✅ Синхронизация завершена!", "", f"📊 Конкурс: {contest.title}", f"📅 Период: {contest.start_at.strftime('%d.%m.%Y')} - {contest.end_at.strftime('%d.%m.%Y')}", f"🔍 Фильтр транзакций:", f" {start_str}", f" {end_str}", "", f"📝 Рефералов в периоде: {stats.get('total_events', 0)}", f"⚠️ Отфильтровано (вне периода): {stats.get('filtered_out_events', 0)}", f"📊 Всего событий в БД: {stats.get('total_all_events', 0)}", "", f"🔄 Обновлено сумм: {stats.get('updated', 0)}", f"⏭ Без изменений: {stats.get('skipped', 0)}", "", f"💳 Рефералов оплатили: {stats.get('paid_count', 0)}", f"❌ Рефералов не оплатили: {stats.get('unpaid_count', 0)}", "", "💰 СУММЫ:", f" 🛒 Покупки подписок: {stats.get('subscription_total', 0) // 100} руб.", f" 📥 Пополнения баланса: {stats.get('deposit_total', 0) // 100} руб.", ] 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"🏆 {contest.title}", f"📅 Период: {contest.start_at.strftime('%d.%m.%Y')} - {contest.end_at.strftime('%d.%m.%Y')}", "", f"👥 Участников (рефереров): {detailed_stats['total_participants']}", f"📨 Приглашено рефералов: {detailed_stats['total_invited']}", "", f"💳 Рефералов оплатили: {detailed_stats.get('paid_count', 0)}", f"❌ Рефералов не оплатили: {detailed_stats.get('unpaid_count', 0)}", f"🛒 Покупки подписок: {detailed_stats['total_paid_amount'] // 100} руб.", ] 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 = [ "🔍 Отладка транзакций конкурса", "", f"📊 Конкурс: {contest.title}", f"📅 Период фильтрации:", f" Начало: {debug_data.get('contest_start')}", f" Конец: {debug_data.get('contest_end')}", f"👥 Рефералов в периоде: {debug_data.get('referral_count', 0)}", f"⚠️ Отфильтровано (вне периода): {debug_data.get('filtered_out', 0)}", f"📊 Всего событий в БД: {debug_data.get('total_all_events', 0)}", "", "💰 СУММЫ:", f" 📥 Пополнения баланса: {deposit_total} руб.", f" 🛒 Покупки подписок: {subscription_total} руб.", "", ] # Показываем транзакции В периоде txs_in = debug_data.get('transactions_in_period', []) if txs_in: lines.append(f"✅ Транзакции в периоде (первые {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("✅ Транзакций в периоде: 0") lines.append("") # Показываем транзакции ВНЕ периода txs_out = debug_data.get('transactions_outside_period', []) if txs_out: lines.append(f"❌ Транзакции вне периода (первые {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("❌ Транзакций вне периода: 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)