From 8b77ccda9ee34eca34fa741e2e0ded073cb5ed74 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sun, 16 Nov 2025 03:30:00 +0300 Subject: [PATCH] Adding admin operations to the balance --- core/constants.py | 1 + database/hot_leads.py | 3 +- database/statistics.py | 9 +- database/tracking_sources.py | 16 +- handlers/admin/sender/sender_handler.py | 8 +- handlers/admin/users/__init__.py | 3 +- handlers/admin/users/users_balance.py | 225 ++++++++++++++++++++++++ handlers/admin/users/users_manage.py | 160 ----------------- utils/csv_export.py | 9 +- 9 files changed, 257 insertions(+), 177 deletions(-) create mode 100644 core/constants.py create mode 100644 handlers/admin/users/users_balance.py diff --git a/core/constants.py b/core/constants.py new file mode 100644 index 00000000..cf562812 --- /dev/null +++ b/core/constants.py @@ -0,0 +1 @@ +PAYMENT_SYSTEMS_EXCLUDED = ("referral", "coupon", "cashback", "admin") \ No newline at end of file diff --git a/database/hot_leads.py b/database/hot_leads.py index a3b67b54..a68e634e 100644 --- a/database/hot_leads.py +++ b/database/hot_leads.py @@ -2,6 +2,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from database.models import Key, Payment, User +from core.constants import PAYMENT_SYSTEMS_EXCLUDED async def get_hot_leads(session: AsyncSession): @@ -16,7 +17,7 @@ async def get_hot_leads(session: AsyncSession): .where(User.trial == 1) .where(Payment.amount > 0) .where(Payment.status == "success") - .where(Payment.payment_system.notin_(["referral", "coupon", "cashback"])) + .where(Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED)) .where(~Payment.tg_id.in_(sub_active)) ) diff --git a/database/statistics.py b/database/statistics.py index e73f9542..8b18db07 100644 --- a/database/statistics.py +++ b/database/statistics.py @@ -4,6 +4,7 @@ from sqlalchemy import and_, exists, func, not_, select from sqlalchemy.ext.asyncio import AsyncSession from database.models import Key, Payment, Referral, Tariff, User +from core.constants import PAYMENT_SYSTEMS_EXCLUDED async def count_total_users(session: AsyncSession) -> int: @@ -116,7 +117,7 @@ async def sum_payments_since(session: AsyncSession, since: date) -> float: and_( Payment.created_at >= since, Payment.status == "success", - Payment.payment_system.notin_(["referral", "coupon", "cashback"]), + Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED), ) ) ) @@ -130,7 +131,7 @@ async def sum_payments_between(session: AsyncSession, start: date, end: date) -> Payment.created_at >= start, Payment.created_at < end, Payment.status == "success", - Payment.payment_system.notin_(["referral", "coupon", "cashback"]), + Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED), ) ) ) @@ -142,7 +143,7 @@ async def sum_total_payments(session: AsyncSession) -> float: select(func.coalesce(func.sum(Payment.amount), 0)).where( and_( Payment.status == "success", - Payment.payment_system.notin_(["referral", "coupon", "cashback"]), + Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED), ) ) ) @@ -158,7 +159,7 @@ async def count_hot_leads(session: AsyncSession) -> int: select(Payment.tg_id) .where(Payment.amount > 0) .where(Payment.status == "success") - .where(Payment.payment_system.notin_(["referral", "coupon", "cashback"])) + .where(Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED)) .where(not_(exists(subquery_active_keys.where(Key.tg_id == Payment.tg_id)))) .distinct() ) diff --git a/database/tracking_sources.py b/database/tracking_sources.py index 72f97257..1bd34abe 100644 --- a/database/tracking_sources.py +++ b/database/tracking_sources.py @@ -4,9 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from database.models import Payment, TrackingSource, User from logger import logger - - -EXCLUDED_PAYMENT_MARKERS = ["coupon", "referral", "cashback"] +from core.constants import PAYMENT_SYSTEMS_EXCLUDED async def create_tracking_source(session: AsyncSession, name: str, code: str, type_: str, created_by: int): @@ -43,7 +41,11 @@ async def get_all_tracking_sources(session: AsyncSession) -> list[dict]: payments_subq = ( select(func.count(func.distinct(Payment.tg_id))) .join(User, Payment.tg_id == User.tg_id) - .where((User.source_code == TrackingSource.code) & (Payment.status == "success")) + .where( + (User.source_code == TrackingSource.code) + & (Payment.status == "success") + & Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED) + ) .correlate(TrackingSource) .scalar_subquery() ) @@ -103,7 +105,7 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | .where( (User.source_code == code) & (Payment.status == "success") - & not_(Payment.payment_system.in_(EXCLUDED_PAYMENT_MARKERS)) + & Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED) & (Payment.created_at >= created_at) ) .scalar_subquery() @@ -115,7 +117,7 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | .where( (User.source_code == code) & (Payment.status == "success") - & not_(Payment.payment_system.in_(EXCLUDED_PAYMENT_MARKERS)) + & Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED) & (Payment.created_at >= created_at) ) .scalar_subquery() @@ -146,7 +148,7 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict | .where( (User.source_code == code) & (Payment.status == "success") - & not_(Payment.payment_system.in_(EXCLUDED_PAYMENT_MARKERS)) + & Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED) & (Payment.created_at >= created_at) ) .subquery() diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index f5a345f6..9bebda79 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -16,6 +16,7 @@ from database import create_blocked_user from database.models import BlockedUser, Key, ManualBan, Payment, Server, Tariff, User from filters.admin import IsAdminFilter from logger import logger +from core.constants import PAYMENT_SYSTEMS_EXCLUDED from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb @@ -113,7 +114,10 @@ async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str query = None if send_to == "subscribed": query = ( - select(distinct(User.tg_id)).join(Key).where(Key.expiry_time > now_ms).where(~User.tg_id.in_(banned_tg_ids)) + select(distinct(User.tg_id)) + .join(Key) + .where(Key.expiry_time > now_ms) + .where(~User.tg_id.in_(banned_tg_ids)) ) elif send_to == "unsubscribed": subquery = ( @@ -151,7 +155,7 @@ async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str .join(Payment, User.tg_id == Payment.tg_id) .where(Payment.status == "success") .where(Payment.amount > 0) - .where(Payment.payment_system.notin_(["referral", "coupon", "cashback"])) + .where(Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED)) .where(not_(exists(subquery_active_keys.where(Key.tg_id == User.tg_id)))) .where(~User.tg_id.in_(banned_tg_ids)) ) diff --git a/handlers/admin/users/__init__.py b/handlers/admin/users/__init__.py index be4f9201..c75b06de 100644 --- a/handlers/admin/users/__init__.py +++ b/handlers/admin/users/__init__.py @@ -1,10 +1,11 @@ from aiogram import Router -from . import users_bans, users_hwid, users_keys, users_manage +from . import users_bans, users_hwid, users_keys, users_manage, users_balance router = Router() router.include_router(users_manage.router) +router.include_router(users_balance.router) router.include_router(users_hwid.router) router.include_router(users_keys.router) router.include_router(users_bans.router) diff --git a/handlers/admin/users/users_balance.py b/handlers/admin/users/users_balance.py new file mode 100644 index 00000000..e47e33c0 --- /dev/null +++ b/handlers/admin/users/users_balance.py @@ -0,0 +1,225 @@ +from datetime import datetime + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from database import get_balance, set_user_balance, update_balance +from database.models import Payment +from database.payments import add_payment +from filters.admin import IsAdminFilter + +from .keyboard import ( + AdminUserEditorCallback, + build_users_balance_change_kb, + build_users_balance_kb, +) +from .users_states import UserEditorState + + +router = Router() + + +def format_admin_operation(amount: float, created_at: datetime) -> str: + date_str = created_at.strftime("%Y-%m-%d %H:%M:%S") + sign = "+" if amount > 0 else "-" if amount < 0 else "" + abs_amount = abs(amount) + return ( + f"\n
Админ {sign}{abs_amount}Р" + f"\n⏳ Дата: {date_str}
" + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_balance_edit"), + IsAdminFilter(), +) +async def handle_balance_change( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + session: AsyncSession, +): + tg_id = callback_data.tg_id + + balance = await get_balance(session, tg_id) + balance = int(balance or 0) + + stmt = ( + select(Payment.amount, Payment.created_at) + .where(Payment.tg_id == tg_id, Payment.payment_system == "admin") + .order_by(Payment.created_at.desc()) + .limit(5) + ) + result = await session.execute(stmt) + records = result.all() + + text = ( + f"💵 Изменение баланса" + f"\n\n🆔 ID: {tg_id}" + f"\n💰 Баланс: {balance}Р" + f"\n📊 Операции админа (5):" + ) + + if records: + for amount, created_at in records: + text += format_admin_operation(amount, created_at) + else: + text += "\n🚫 Операции отсутствуют" + + await callback_query.message.edit_text( + text=text, + reply_markup=await build_users_balance_kb(session, tg_id), + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_balance_add"), + IsAdminFilter(), +) +async def handle_balance_add( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext, + session: AsyncSession, +): + tg_id = callback_data.tg_id + amount = callback_data.data + + if amount is not None: + amount = int(amount) + old_balance = await get_balance(session, tg_id) + + if amount >= 0: + await update_balance(session, tg_id, amount) + new_balance = old_balance + amount + if amount != 0: + await add_payment( + session=session, + tg_id=tg_id, + amount=amount, + payment_system="admin", + status="success", + ) + else: + new_balance = max(0, old_balance + amount) + await set_user_balance(session, tg_id, new_balance) + deducted = old_balance - new_balance + if deducted > 0: + await add_payment( + session=session, + tg_id=tg_id, + amount=-deducted, + payment_system="admin", + status="success", + ) + + if old_balance != new_balance: + await handle_balance_change(callback_query, callback_data, session) + return + + await state.update_data(tg_id=tg_id, op_type="add") + await state.set_state(UserEditorState.waiting_for_balance) + + await callback_query.message.edit_text( + text="✍️ Введите сумму, которую хотите добавить на баланс пользователя:", + reply_markup=build_users_balance_change_kb(tg_id), + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_balance_take"), + IsAdminFilter(), +) +async def handle_balance_take( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext, +): + tg_id = callback_data.tg_id + + await state.update_data(tg_id=tg_id, op_type="take") + await state.set_state(UserEditorState.waiting_for_balance) + + await callback_query.message.edit_text( + text="✍️ Введите сумму, которую хотите вычесть из баланса пользователя:", + reply_markup=build_users_balance_change_kb(tg_id), + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_balance_set"), + IsAdminFilter(), +) +async def handle_balance_set( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext, +): + tg_id = callback_data.tg_id + + await state.update_data(tg_id=tg_id, op_type="set") + await state.set_state(UserEditorState.waiting_for_balance) + + await callback_query.message.edit_text( + text="✍️ Введите баланс, который хотите установить пользователю:", + reply_markup=build_users_balance_change_kb(tg_id), + ) + + +@router.message(UserEditorState.waiting_for_balance, IsAdminFilter()) +async def handle_balance_input(message: Message, state: FSMContext, session: AsyncSession): + data = await state.get_data() + tg_id = data.get("tg_id") + op_type = data.get("op_type") + + if not message.text.isdigit() or int(message.text) < 0: + await message.answer( + text="🚫 Пожалуйста, введите корректную сумму!", + reply_markup=build_users_balance_change_kb(tg_id), + ) + return + + amount = int(message.text) + + if op_type == "add": + text = f"✅ К балансу пользователя добавлено {amount}Р" + await update_balance(session, tg_id, amount) + if amount != 0: + await add_payment( + session=session, + tg_id=tg_id, + amount=amount, + payment_system="admin", + status="success", + ) + elif op_type == "take": + current_balance = await get_balance(session, tg_id) + new_balance = max(0, current_balance - amount) + deducted = current_balance if amount > current_balance else amount + text = f"✅ Из баланса пользователя было вычтено {deducted}Р" + await set_user_balance(session, tg_id, new_balance) + if deducted > 0: + await add_payment( + session=session, + tg_id=tg_id, + amount=-deducted, + payment_system="admin", + status="success", + ) + else: + current_balance = await get_balance(session, tg_id) + text = f"✅ Баланс пользователя изменён на {amount}Р" + await set_user_balance(session, tg_id, amount) + delta = amount - current_balance + if delta != 0: + await add_payment( + session=session, + tg_id=tg_id, + amount=delta, + payment_system="admin", + status="success", + ) + + await message.answer(text=text, reply_markup=build_users_balance_change_kb(tg_id)) diff --git a/handlers/admin/users/users_manage.py b/handlers/admin/users/users_manage.py index ba7bfcb0..14153681 100644 --- a/handlers/admin/users/users_manage.py +++ b/handlers/admin/users/users_manage.py @@ -15,10 +15,7 @@ from sqlalchemy import func, select, update from sqlalchemy.ext.asyncio import AsyncSession from database import ( - get_balance, get_key_details, - set_user_balance, - update_balance, update_trial, ) from database.models import Key, ManualBan, Payment, Referral, User @@ -35,8 +32,6 @@ from .keyboard import ( AdminUserEditorCallback, build_editor_kb, build_user_edit_kb, - build_users_balance_change_kb, - build_users_balance_kb, ) from .users_states import UserEditorState @@ -263,161 +258,6 @@ async def handle_trial_restore( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_edit"), - IsAdminFilter(), -) -async def handle_balance_change( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - session: AsyncSession, -): - tg_id = callback_data.tg_id - - stmt = ( - select(Payment.amount, Payment.payment_system, Payment.status, Payment.created_at) - .where(Payment.tg_id == tg_id) - .order_by(Payment.created_at.desc()) - .limit(5) - ) - result = await session.execute(stmt) - records = result.all() - - balance = await get_balance(session, tg_id) - balance = int(balance or 0) - - text = ( - f"💵 Изменение баланса" - f"\n\n🆔 ID: {tg_id}" - f"\n💰 Баланс: {balance}Р" - f"\n📊 Последние операции (5):" - ) - - if records: - for amount, payment_system, status, created_at in records: - date = created_at.strftime("%Y-%m-%d %H:%M:%S") - text += ( - f"\n
💸 Сумма: {amount} | {payment_system}" - f"\n📌 Статус: {status}" - f"\n⏳ Дата: {date}
" - ) - else: - text += "\n 🚫 Отсутствуют" - - await callback_query.message.edit_text( - text=text, - reply_markup=await build_users_balance_kb(session, tg_id), - ) - - -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_add"), - IsAdminFilter(), -) -async def handle_balance_add( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext, - session: AsyncSession, -): - tg_id = callback_data.tg_id - amount = callback_data.data - - if amount is not None: - amount = int(amount) - old_balance = await get_balance(session, tg_id) - - if amount >= 0: - await update_balance(session, tg_id, amount) - new_balance = old_balance + amount - else: - new_balance = max(0, old_balance + amount) - await set_user_balance(session, tg_id, new_balance) - - if old_balance != new_balance: - await handle_balance_change(callback_query, callback_data, session) - return - - await state.update_data(tg_id=tg_id, op_type="add") - await state.set_state(UserEditorState.waiting_for_balance) - - await callback_query.message.edit_text( - text="✍️ Введите сумму, которую хотите добавить на баланс пользователя:", - reply_markup=build_users_balance_change_kb(tg_id), - ) - - -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_take"), - IsAdminFilter(), -) -async def handle_balance_take( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext, -): - tg_id = callback_data.tg_id - - await state.update_data(tg_id=tg_id, op_type="take") - await state.set_state(UserEditorState.waiting_for_balance) - - await callback_query.message.edit_text( - text="✍️ Введите сумму, которую хотите вычесть из баланса пользователя:", - reply_markup=build_users_balance_change_kb(tg_id), - ) - - -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_set"), - IsAdminFilter(), -) -async def handle_balance_set( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext, -): - tg_id = callback_data.tg_id - - await state.update_data(tg_id=tg_id, op_type="set") - await state.set_state(UserEditorState.waiting_for_balance) - - await callback_query.message.edit_text( - text="✍️ Введите баланс, который хотите установить пользователю:", - reply_markup=build_users_balance_change_kb(tg_id), - ) - - -@router.message(UserEditorState.waiting_for_balance, IsAdminFilter()) -async def handle_balance_input(message: Message, state: FSMContext, session: AsyncSession): - data = await state.get_data() - tg_id = data.get("tg_id") - op_type = data.get("op_type") - - if not message.text.isdigit() or int(message.text) < 0: - await message.answer( - text="🚫 Пожалуйста, введите корректную сумму!", - reply_markup=build_users_balance_change_kb(tg_id), - ) - return - - amount = int(message.text) - - if op_type == "add": - text = f"✅ К балансу пользователя добавлено {amount}Р" - await update_balance(session, tg_id, amount) - elif op_type == "take": - current_balance = await get_balance(session, tg_id) - new_balance = max(0, current_balance - amount) - deducted = current_balance if amount > current_balance else amount - text = f"✅ Из баланса пользователя было вычтено {deducted}Р" - await set_user_balance(session, tg_id, new_balance) - else: - text = f"✅ Баланс пользователя изменен на {amount}Р" - await set_user_balance(session, tg_id, amount) - - await message.answer(text=text, reply_markup=build_users_balance_change_kb(tg_id)) - - @router.callback_query( AdminPanelCallback.filter(F.action == "restore_trials"), IsAdminFilter(), diff --git a/utils/csv_export.py b/utils/csv_export.py index 92a2f781..79589342 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -8,6 +8,7 @@ from sqlalchemy import exists, func, join, not_, select from sqlalchemy.ext.asyncio import AsyncSession from database.models import Key, Payment, Referral, Tariff, User +from core.constants import PAYMENT_SYSTEMS_EXCLUDED async def export_users_csv(session: AsyncSession) -> BufferedInputFile: @@ -61,6 +62,7 @@ async def export_payments_csv(session: AsyncSession) -> BufferedInputFile: Payment.created_at, ) .select_from(j) + .where(Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED)) .order_by(Payment.created_at.asc()) ) @@ -84,7 +86,10 @@ async def export_user_payments_csv(tg_id: int, session: AsyncSession) -> Buffere Payment.created_at, ) .select_from(j) - .where(User.tg_id == tg_id) + .where( + User.tg_id == tg_id, + Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED), + ) .order_by(Payment.created_at.asc()) ) @@ -169,7 +174,7 @@ async def export_hot_leads_csv(session: AsyncSession) -> BufferedInputFile: .where(Payment.tg_id == User.tg_id) .where(Payment.status == "success") .where(Payment.amount > 0) - .where(Payment.payment_system.notin_(["referral", "coupon", "cashback"])) + .where(Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED)) ), not_(exists(select(Key.tg_id).where(Key.tg_id == User.tg_id).where(Key.expiry_time > now_ts))), )