From 8f494c0b98e8787f77a79e0505bbfb2b8b9d3563 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sun, 25 May 2025 20:34:16 +0300 Subject: [PATCH] add round for balance and less callback for tariffs --- database/referrals.py | 3 +- database/statistics.py | 42 +++++++--- handlers/admin/stats/stats_handler.py | 31 +++++-- handlers/admin/users/users_handler.py | 113 ++++++++++++++++---------- 4 files changed, 127 insertions(+), 62 deletions(-) diff --git a/database/referrals.py b/database/referrals.py index 2a41e51e..3867e15c 100644 --- a/database/referrals.py +++ b/database/referrals.py @@ -171,7 +171,8 @@ async def get_total_referral_bonus( result = await session.execute( text(bonus_query), {"tg_id": referrer_tg_id, "max_levels": max_levels} ) - total_bonus = result.scalar() or 0.0 + total_bonus_raw = result.scalar() + total_bonus = round(float(total_bonus_raw or 0), 2) logger.debug(f"Получена общая сумма бонусов от рефералов: {total_bonus}") return total_bonus diff --git a/database/statistics.py b/database/statistics.py index 0514b768..d383ff2c 100644 --- a/database/statistics.py +++ b/database/statistics.py @@ -55,11 +55,26 @@ async def count_trial_keys(session: AsyncSession) -> int: ) -async def get_tariff_distribution(session: AsyncSession) -> list[tuple[int, int]]: +async def get_tariff_distribution( + session: AsyncSession, include_unbound: bool = False +) -> tuple[list[tuple[int, int]], list[dict]]: result = await session.execute( - select(Key.tariff_id, func.count(Key.client_id)).group_by(Key.tariff_id) + select(Key.tariff_id, func.count(Key.client_id)) + .where(Key.tariff_id.isnot(None)) + .group_by(Key.tariff_id) ) - return result.all() + tariff_counts = result.all() + + if not include_unbound: + return tariff_counts + + result = await session.execute( + select(Key.expiry_time) + .where(Key.tariff_id.is_(None)) + ) + no_tariff_keys = [{"expiry_time": row[0]} for row in result.all()] + + return tariff_counts, no_tariff_keys async def get_tariff_names( @@ -79,20 +94,23 @@ async def count_total_referrals(session: AsyncSession) -> int: async def sum_payments_since(session: AsyncSession, since: date) -> float: - return await session.scalar( - select(func.coalesce(func.sum(Payment.amount), 0)).where( - Payment.created_at >= since - ) + result = await session.scalar( + select(func.coalesce(func.sum(Payment.amount), 0)) + .where(Payment.created_at >= since) ) + return round(float(result), 2) async def sum_payments_between(session: AsyncSession, start: date, end: date) -> float: - return await session.scalar( - select(func.coalesce(func.sum(Payment.amount), 0)).where( - Payment.created_at >= start, Payment.created_at < end - ) + result = await session.scalar( + select(func.coalesce(func.sum(Payment.amount), 0)) + .where(Payment.created_at >= start, Payment.created_at < end) ) + return round(float(result), 2) async def sum_total_payments(session: AsyncSession) -> float: - return await session.scalar(select(func.coalesce(func.sum(Payment.amount), 0))) + result = await session.scalar( + select(func.coalesce(func.sum(Payment.amount), 0)) + ) + return round(float(result), 2) diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index 4fb5d358..f8e0c46c 100644 --- a/handlers/admin/stats/stats_handler.py +++ b/handlers/admin/stats/stats_handler.py @@ -5,6 +5,8 @@ from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.types import CallbackQuery from sqlalchemy.ext.asyncio import AsyncSession +from collections import Counter + from bot import bot from config import ADMIN_ID @@ -67,17 +69,36 @@ async def handle_stats(callback_query: CallbackQuery, session: AsyncSession): expired_keys = total_keys - active_keys trial_keys_count = await count_trial_keys(session) - tariff_counts = await get_tariff_distribution(session) - tariff_names = await get_tariff_names( - session, [tid for tid, _ in tariff_counts] - ) + tariff_counts, no_tariff_keys = await get_tariff_distribution(session, include_unbound=True) + tariff_names = await get_tariff_names(session, [tid for tid, _ in tariff_counts]) tariff_stats_text = "" for tid, count in tariff_counts: name = tariff_names.get(tid, f"ID {tid}") tariff_stats_text += f"├ {name}: {count}\n" + + duration_buckets = Counter() + now_ts = int(datetime.utcnow().timestamp() * 1000) + + for key in no_tariff_keys: + duration_days = round((key["expiry_time"] - now_ts) / (1000 * 60 * 60 * 24)) + if 25 <= duration_days <= 35: + bucket = "Без тарифа: 1 мес" + elif 80 <= duration_days <= 100: + bucket = "Без тарифа: 3 мес" + elif 170 <= duration_days <= 200: + bucket = "Без тарифа: 6 мес" + elif 350 <= duration_days <= 380: + bucket = "Без тарифа: 12 мес" + else: + bucket = "Без тарифа: прочее" + duration_buckets[bucket] += 1 + + for name, count in duration_buckets.items(): + tariff_stats_text += f"├ {name}: {count}\n" + tariff_stats_text = ( - "└ По тарифам:\n" + tariff_stats_text + "└ По тарифам и срокам:\n" + tariff_stats_text if tariff_stats_text else "└ Нет данных по тарифам\n" ) diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 0eb6c42f..2b6cb007 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -82,6 +82,11 @@ class UserEditorState(StatesGroup): selecting_country = State() +class RenewTariffState(StatesGroup): + selecting_group = State() + selecting_tariff = State() + + class BanUserStates(StatesGroup): waiting_for_reason = State() waiting_for_ban_duration = State() @@ -554,14 +559,20 @@ async def handle_key_edit( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_back"), IsAdminFilter() -) -async def handle_users_back_to_key_edit( +@router.callback_query(F.data == "back:renew", IsAdminFilter()) +async def handle_back_to_key_menu( callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, session: AsyncSession, + state: FSMContext, ): + data = await state.get_data() + email = data["email"] + tg_id = data["tg_id"] + await state.clear() + + callback_data = AdminUserEditorCallback( + action="users_key_edit", data=email, tg_id=tg_id + ) await handle_key_edit( callback_query=callback_query, callback_data=callback_data, @@ -577,27 +588,21 @@ async def handle_user_choose_tariff_group( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: AsyncSession, + state: FSMContext, ): email = callback_data.data tg_id = callback_data.tg_id + await state.set_state(RenewTariffState.selecting_group) + await state.update_data(email=email, tg_id=tg_id) + result = await session.execute(select(Tariff.group_code).distinct()) groups = [row[0] for row in result.fetchall()] builder = InlineKeyboardBuilder() for group_code in groups: - builder.button( - text=group_code, - callback_data=AdminUserEditorCallback( - action="users_renew_group", data=f"{email}|{group_code}", tg_id=tg_id - ).pack(), - ) - builder.button( - text="🔙 Назад", - callback_data=AdminUserEditorCallback( - action="users_back", data=email, tg_id=tg_id - ).pack(), - ) + builder.button(text=group_code, callback_data=f"group:{group_code}") + builder.button(text="🔙 Назад", callback_data="back:renew") builder.adjust(1) await callback_query.message.edit_text( @@ -606,16 +611,15 @@ async def handle_user_choose_tariff_group( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_renew_group"), IsAdminFilter() -) +@router.callback_query(F.data.startswith("group:"), IsAdminFilter()) async def handle_user_choose_tariff( callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, session: AsyncSession, + state: FSMContext, ): - email, group_code = callback_data.data.split("|") - tg_id = callback_data.tg_id + group_code = callback_query.data.split(":", 1)[1] + await state.update_data(group_code=group_code) + await state.set_state(RenewTariffState.selecting_tariff) result = await session.execute( select(Tariff) @@ -631,19 +635,10 @@ async def handle_user_choose_tariff( builder = InlineKeyboardBuilder() for tariff in tariffs: builder.button( - text=f"{tariff.duration_days}д / {int(tariff.price_rub)}₽", - callback_data=AdminUserEditorCallback( - action="users_renew_confirm", - data=f"{email}|{tariff.id}", - tg_id=tg_id, - ).pack(), + text=f"{tariff.name} – {int(tariff.price_rub)}₽", + callback_data=f"confirm:{tariff.id}" ) - builder.button( - text="🔙 Назад", - callback_data=AdminUserEditorCallback( - action="users_renew", data=email, tg_id=tg_id - ).pack(), - ) + builder.button(text="🔙 Назад", callback_data="back:group") builder.adjust(1) await callback_query.message.edit_text( @@ -652,17 +647,16 @@ async def handle_user_choose_tariff( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_renew_confirm"), IsAdminFilter() -) +@router.callback_query(F.data.startswith("confirm:"), IsAdminFilter()) async def handle_user_renew_confirm( callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, session: AsyncSession, + state: FSMContext, ): - email, tariff_id = callback_data.data.split("|") - tg_id = callback_data.tg_id - tariff_id = int(tariff_id) + tariff_id = int(callback_query.data.split(":")[1]) + data = await state.get_data() + email = data["email"] + tg_id = data["tg_id"] stmt = ( update(Key) @@ -671,11 +665,42 @@ async def handle_user_renew_confirm( ) await session.execute(stmt) await session.commit() + await state.clear() + + callback_data = AdminUserEditorCallback( + action="users_key_edit", data=email, tg_id=tg_id + ) + + await handle_key_edit( + callback_query=callback_query, + callback_data=callback_data, + session=session, + update=False, + ) + + +@router.callback_query(F.data == "back:group", IsAdminFilter()) +async def handle_back_to_group( + callback_query: CallbackQuery, + state: FSMContext, + session: AsyncSession, +): + data = await state.get_data() + + result = await session.execute(select(Tariff.group_code).distinct()) + groups = [row[0] for row in result.fetchall()] + + builder = InlineKeyboardBuilder() + for group_code in groups: + builder.button(text=group_code, callback_data=f"group:{group_code}") + builder.button(text="🔙 Назад", callback_data="back:renew") + builder.adjust(1) await callback_query.message.edit_text( - text="✅ Тариф успешно обновлён.", - reply_markup=build_key_edit_kb({"tg_id": tg_id}, email), + text="📁 Выберите тарифную группу:", + reply_markup=builder.as_markup(), ) + await state.set_state(RenewTariffState.selecting_group) @router.callback_query(