Adding admin operations to the balance

This commit is contained in:
Vladless
2025-11-16 03:30:00 +03:00
parent 1d5c80cd35
commit 8b77ccda9e
9 changed files with 257 additions and 177 deletions
+1
View File
@@ -0,0 +1 @@
PAYMENT_SYSTEMS_EXCLUDED = ("referral", "coupon", "cashback", "admin")
+2 -1
View File
@@ -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))
)
+5 -4
View File
@@ -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()
)
+9 -7
View File
@@ -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()
+6 -2
View File
@@ -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))
)
+2 -1
View File
@@ -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)
+225
View File
@@ -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<blockquote>Админ {sign}{abs_amount}Р"
f"\n⏳ Дата: {date_str}</blockquote>"
)
@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"<b>💵 Изменение баланса</b>"
f"\n\n🆔 ID: <b>{tg_id}</b>"
f"\n💰 Баланс: <b>{balance}Р</b>"
f"\n📊 Операции админа (5):"
)
if records:
for amount, created_at in records:
text += format_admin_operation(amount, created_at)
else:
text += "\n<i>🚫 Операции отсутствуют</i>"
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"✅ К балансу пользователя добавлено <b>{amount}Р</b>"
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"✅ Из баланса пользователя было вычтено <b>{deducted}Р</b>"
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"✅ Баланс пользователя изменён на <b>{amount}Р</b>"
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))
-160
View File
@@ -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"<b>💵 Изменение баланса</b>"
f"\n\n🆔 ID: <b>{tg_id}</b>"
f"\n💰 Баланс: <b>{balance}Р</b>"
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<blockquote>💸 Сумма: {amount} | {payment_system}"
f"\n📌 Статус: {status}"
f"\n⏳ Дата: {date}</blockquote>"
)
else:
text += "\n <i>🚫 Отсутствуют</i>"
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"✅ К балансу пользователя добавлено <b>{amount}Р</b>"
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"✅ Из баланса пользователя было вычтено <b>{deducted}Р</b>"
await set_user_balance(session, tg_id, new_balance)
else:
text = f"✅ Баланс пользователя изменен на <b>{amount}Р</b>"
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(),
+7 -2
View File
@@ -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))),
)