Add payments feature to admin panel
- Integrated payments functionality into the admin panel by adding a new payments router and corresponding handlers. - Updated the admin panel actions to include a view payments option, enhancing admin capabilities. - Implemented new database functions to retrieve successful payment counts and details for export. - Enhanced localization with new strings for payments management in both English and Russian.
This commit is contained in:
@@ -7,6 +7,7 @@ from . import user_management
|
|||||||
from . import statistics
|
from . import statistics
|
||||||
from . import sync_admin
|
from . import sync_admin
|
||||||
from . import logs_admin
|
from . import logs_admin
|
||||||
|
from . import payments
|
||||||
|
|
||||||
admin_router_aggregate = Router(name="admin_features_router")
|
admin_router_aggregate = Router(name="admin_features_router")
|
||||||
|
|
||||||
@@ -17,5 +18,6 @@ admin_router_aggregate.include_router(user_management.router)
|
|||||||
admin_router_aggregate.include_router(statistics.router)
|
admin_router_aggregate.include_router(statistics.router)
|
||||||
admin_router_aggregate.include_router(sync_admin.router)
|
admin_router_aggregate.include_router(sync_admin.router)
|
||||||
admin_router_aggregate.include_router(logs_admin.router)
|
admin_router_aggregate.include_router(logs_admin.router)
|
||||||
|
admin_router_aggregate.include_router(payments.router)
|
||||||
|
|
||||||
__all__ = ("admin_router_aggregate", )
|
__all__ = ("admin_router_aggregate", )
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ async def admin_panel_actions_callback_handler(
|
|||||||
await callback.answer(_("admin_sync_initiated_from_panel"))
|
await callback.answer(_("admin_sync_initiated_from_panel"))
|
||||||
elif action == "queue_status":
|
elif action == "queue_status":
|
||||||
await show_queue_status_handler(callback, i18n_data)
|
await show_queue_status_handler(callback, i18n_data)
|
||||||
|
elif action == "view_payments":
|
||||||
|
from . import payments as admin_payments_handlers
|
||||||
|
await admin_payments_handlers.view_payments_handler(
|
||||||
|
callback, i18n_data, settings, session)
|
||||||
elif action == "main":
|
elif action == "main":
|
||||||
try:
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import logging
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from aiogram import Router, F, types
|
||||||
|
from aiogram.filters import StateFilter
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional, List
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config.settings import Settings
|
||||||
|
from db.dal import payment_dal
|
||||||
|
from db.models import Payment
|
||||||
|
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
||||||
|
from bot.middlewares.i18n import JsonI18n
|
||||||
|
|
||||||
|
router = Router(name="admin_payments_router")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_payments_with_pagination(session: AsyncSession, page: int = 0,
|
||||||
|
page_size: int = 10) -> tuple[List[Payment], int]:
|
||||||
|
"""Get payments with pagination and total count."""
|
||||||
|
offset = page * page_size
|
||||||
|
|
||||||
|
# Get total count
|
||||||
|
total_count = await payment_dal.get_payments_count(session)
|
||||||
|
|
||||||
|
# Get payments for current page
|
||||||
|
payments = await payment_dal.get_recent_payment_logs_with_user(
|
||||||
|
session, limit=page_size, offset=offset
|
||||||
|
)
|
||||||
|
|
||||||
|
return payments, total_count
|
||||||
|
|
||||||
|
|
||||||
|
def format_payment_text(payment: Payment, i18n: JsonI18n, lang: str) -> str:
|
||||||
|
"""Format single payment info as text."""
|
||||||
|
_ = lambda key, **kwargs: i18n.gettext(lang, key, **kwargs)
|
||||||
|
|
||||||
|
status_emoji = "✅" if payment.status == 'succeeded' else (
|
||||||
|
"⏳" if payment.status in ['pending', 'pending_yookassa'] else "❌"
|
||||||
|
)
|
||||||
|
|
||||||
|
user_info = f"User {payment.user_id}"
|
||||||
|
if payment.user and payment.user.username:
|
||||||
|
user_info += f" (@{payment.user.username})"
|
||||||
|
elif payment.user and payment.user.first_name:
|
||||||
|
user_info += f" ({payment.user.first_name})"
|
||||||
|
|
||||||
|
payment_date = payment.created_at.strftime('%Y-%m-%d %H:%M') if payment.created_at else "N/A"
|
||||||
|
|
||||||
|
provider_text = {
|
||||||
|
'yookassa': 'YooKassa',
|
||||||
|
'tribute': 'Tribute',
|
||||||
|
'telegram_stars': 'Telegram Stars',
|
||||||
|
'cryptopay': 'CryptoPay'
|
||||||
|
}.get(payment.provider, payment.provider or 'Unknown')
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{status_emoji} <b>{payment.amount} {payment.currency}</b>\n"
|
||||||
|
f"👤 {user_info}\n"
|
||||||
|
f"💳 {provider_text}\n"
|
||||||
|
f"📅 {payment_date}\n"
|
||||||
|
f"📋 {payment.status}\n"
|
||||||
|
f"📝 {payment.description or 'N/A'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def view_payments_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||||
|
settings: Settings, session: AsyncSession, page: int = 0):
|
||||||
|
"""Display paginated list of all payments."""
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
if not i18n or not callback.message:
|
||||||
|
await callback.answer("Error processing request.", show_alert=True)
|
||||||
|
return
|
||||||
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||||
|
|
||||||
|
page_size = 5 # Показываем по 5 платежей на странице
|
||||||
|
payments, total_count = await get_payments_with_pagination(session, page, page_size)
|
||||||
|
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 1
|
||||||
|
|
||||||
|
if not payments and page == 0:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
_("admin_no_payments_found", default="Платежи не найдены."),
|
||||||
|
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Format payments text
|
||||||
|
text_parts = [_("admin_payments_header", default="💰 <b>Все платежи</b>")]
|
||||||
|
text_parts.append(f"📊 Показано {len(payments)} из {total_count} платежей (стр. {page + 1}/{total_pages})\n")
|
||||||
|
|
||||||
|
for i, payment in enumerate(payments, 1):
|
||||||
|
text_parts.append(f"<b>{page * page_size + i}.</b> {format_payment_text(payment, i18n, current_lang)}")
|
||||||
|
text_parts.append("") # Empty line between payments
|
||||||
|
|
||||||
|
# Build keyboard with pagination and export
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
# Pagination buttons
|
||||||
|
nav_buttons = []
|
||||||
|
if page > 0:
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text="⬅️", callback_data=f"payments_page:{page-1}"))
|
||||||
|
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="noop"))
|
||||||
|
|
||||||
|
if page < total_pages - 1:
|
||||||
|
nav_buttons.append(InlineKeyboardButton(text="➡️", callback_data=f"payments_page:{page+1}"))
|
||||||
|
|
||||||
|
if nav_buttons:
|
||||||
|
builder.row(*nav_buttons)
|
||||||
|
|
||||||
|
# Export and refresh buttons
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=_("admin_export_payments_csv", default="📊 Экспорт CSV"),
|
||||||
|
callback_data="payments_export_csv"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=_("admin_refresh_payments", default="🔄 Обновить"),
|
||||||
|
callback_data=f"payments_page:{page}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Back button
|
||||||
|
builder.row(InlineKeyboardButton(
|
||||||
|
text=_("back_to_admin_panel_button"),
|
||||||
|
callback_data="admin_section:stats_monitoring"
|
||||||
|
))
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"\n".join(text_parts),
|
||||||
|
reply_markup=builder.as_markup(),
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("payments_page:"))
|
||||||
|
async def payments_pagination_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||||
|
settings: Settings, session: AsyncSession):
|
||||||
|
"""Handle pagination for payments list."""
|
||||||
|
try:
|
||||||
|
page = int(callback.data.split(":")[1])
|
||||||
|
await view_payments_handler(callback, i18n_data, settings, session, page)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
await callback.answer("Error processing pagination.", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "payments_export_csv")
|
||||||
|
async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data: dict,
|
||||||
|
settings: Settings, session: AsyncSession):
|
||||||
|
"""Export all successful payments to CSV file."""
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
if not i18n:
|
||||||
|
await callback.answer("Language service error.", show_alert=True)
|
||||||
|
return
|
||||||
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get all successful payments
|
||||||
|
all_payments = await payment_dal.get_all_succeeded_payments_with_user(session)
|
||||||
|
|
||||||
|
if not all_payments:
|
||||||
|
await callback.answer(
|
||||||
|
_("admin_no_payments_to_export", default="Нет платежей для экспорта."),
|
||||||
|
show_alert=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create CSV in memory
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Write header
|
||||||
|
writer.writerow([
|
||||||
|
_("admin_csv_payment_id", default="ID"),
|
||||||
|
_("admin_csv_user_id", default="User ID"),
|
||||||
|
_("admin_csv_username", default="Username"),
|
||||||
|
_("admin_csv_first_name", default="First Name"),
|
||||||
|
_("admin_csv_amount", default="Amount"),
|
||||||
|
_("admin_csv_currency", default="Currency"),
|
||||||
|
_("admin_csv_provider", default="Provider"),
|
||||||
|
_("admin_csv_status", default="Status"),
|
||||||
|
_("admin_csv_description", default="Description"),
|
||||||
|
_("admin_csv_months", default="Months"),
|
||||||
|
_("admin_csv_created_at", default="Created At"),
|
||||||
|
_("admin_csv_provider_payment_id", default="Provider Payment ID")
|
||||||
|
])
|
||||||
|
|
||||||
|
# Write payment data
|
||||||
|
for payment in all_payments:
|
||||||
|
writer.writerow([
|
||||||
|
payment.payment_id,
|
||||||
|
payment.user_id,
|
||||||
|
payment.user.username if payment.user and payment.user.username else "",
|
||||||
|
payment.user.first_name if payment.user and payment.user.first_name else "",
|
||||||
|
payment.amount,
|
||||||
|
payment.currency,
|
||||||
|
payment.provider or "",
|
||||||
|
payment.status,
|
||||||
|
payment.description or "",
|
||||||
|
payment.subscription_duration_months or "",
|
||||||
|
payment.created_at.strftime('%Y-%m-%d %H:%M:%S') if payment.created_at else "",
|
||||||
|
payment.provider_payment_id or ""
|
||||||
|
])
|
||||||
|
|
||||||
|
# Prepare file
|
||||||
|
csv_content = output.getvalue().encode('utf-8-sig') # UTF-8 with BOM for Excel
|
||||||
|
output.close()
|
||||||
|
|
||||||
|
# Generate filename with current date
|
||||||
|
current_time = datetime.now().strftime('%Y-%m-%d_%H-%M')
|
||||||
|
filename = f"payments_export_{current_time}.csv"
|
||||||
|
|
||||||
|
# Send file
|
||||||
|
from aiogram.types import BufferedInputFile
|
||||||
|
file = BufferedInputFile(csv_content, filename=filename)
|
||||||
|
|
||||||
|
await callback.message.reply_document(
|
||||||
|
document=file,
|
||||||
|
caption=_("admin_payments_export_success",
|
||||||
|
default="📊 Экспорт платежей завершен!\nВсего записей: {count}",
|
||||||
|
count=len(all_payments))
|
||||||
|
)
|
||||||
|
|
||||||
|
await callback.answer(
|
||||||
|
_("admin_export_sent", default="Файл отправлен!"),
|
||||||
|
show_alert=False
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to export payments CSV: {e}", exc_info=True)
|
||||||
|
await callback.answer(f"❌ Ошибка экспорта: {str(e)}", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "noop")
|
||||||
|
async def noop_handler(callback: types.CallbackQuery):
|
||||||
|
"""Handle no-op callback (for pagination display)."""
|
||||||
|
await callback.answer()
|
||||||
@@ -39,12 +39,14 @@ def get_stats_monitoring_keyboard(i18n_instance, lang: str) -> InlineKeyboardMar
|
|||||||
|
|
||||||
builder.button(text=_(key="admin_stats_button"),
|
builder.button(text=_(key="admin_stats_button"),
|
||||||
callback_data="admin_action:stats")
|
callback_data="admin_action:stats")
|
||||||
|
builder.button(text=_(key="admin_view_payments_button", default="💰 Платежи"),
|
||||||
|
callback_data="admin_action:view_payments")
|
||||||
builder.button(text=_(key="admin_view_logs_menu_button"),
|
builder.button(text=_(key="admin_view_logs_menu_button"),
|
||||||
callback_data="admin_action:view_logs_menu")
|
callback_data="admin_action:view_logs_menu")
|
||||||
|
|
||||||
builder.button(text=_(key="back_to_admin_panel_button"),
|
builder.button(text=_(key="back_to_admin_panel_button"),
|
||||||
callback_data="admin_action:main")
|
callback_data="admin_action:main")
|
||||||
builder.adjust(2, 1)
|
builder.adjust(2, 1, 1)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ async def get_recent_payment_logs_with_user(session: AsyncSession,
|
|||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_payments_count(session: AsyncSession) -> int:
|
||||||
|
"""Get total count of successful payments."""
|
||||||
|
stmt = select(func.count(Payment.payment_id)).where(Payment.status == 'succeeded')
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return result.scalar() or 0
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_succeeded_payments_with_user(session: AsyncSession) -> List[Payment]:
|
||||||
|
"""Get all successful payments with user data for export."""
|
||||||
|
stmt = (select(Payment).options(selectinload(Payment.user))
|
||||||
|
.where(Payment.status == 'succeeded')
|
||||||
|
.order_by(Payment.created_at.desc()))
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
async def update_provider_payment_and_status(
|
async def update_provider_payment_and_status(
|
||||||
session: AsyncSession, payment_db_id: int,
|
session: AsyncSession, payment_db_id: int,
|
||||||
provider_payment_id: str, new_status: str) -> Optional[Payment]:
|
provider_payment_id: str, new_status: str) -> Optional[Payment]:
|
||||||
|
|||||||
@@ -122,6 +122,28 @@
|
|||||||
"admin_stats_recent_payments_header": "Recent Payments:",
|
"admin_stats_recent_payments_header": "Recent Payments:",
|
||||||
"admin_stats_payment_item": "{status_emoji} {amount} {currency} from {user_info} ({p_status}) [{p_date}]",
|
"admin_stats_payment_item": "{status_emoji} {amount} {currency} from {user_info} ({p_status}) [{p_date}]",
|
||||||
"admin_stats_no_payments_found": "No payments found yet.",
|
"admin_stats_no_payments_found": "No payments found yet.",
|
||||||
|
|
||||||
|
"admin_view_payments_button": "💰 Payments",
|
||||||
|
"admin_payments_header": "💰 <b>All Payments</b>",
|
||||||
|
"admin_no_payments_found": "No payments found.",
|
||||||
|
"admin_export_payments_csv": "📊 Export CSV",
|
||||||
|
"admin_refresh_payments": "🔄 Refresh",
|
||||||
|
"admin_no_payments_to_export": "No payments to export.",
|
||||||
|
"admin_payments_export_success": "📊 Payments export completed!\nTotal records: {count}",
|
||||||
|
"admin_export_sent": "File sent!",
|
||||||
|
|
||||||
|
"admin_csv_payment_id": "ID",
|
||||||
|
"admin_csv_user_id": "User ID",
|
||||||
|
"admin_csv_username": "Username",
|
||||||
|
"admin_csv_first_name": "First Name",
|
||||||
|
"admin_csv_amount": "Amount",
|
||||||
|
"admin_csv_currency": "Currency",
|
||||||
|
"admin_csv_provider": "Provider",
|
||||||
|
"admin_csv_status": "Status",
|
||||||
|
"admin_csv_description": "Description",
|
||||||
|
"admin_csv_months": "Months",
|
||||||
|
"admin_csv_created_at": "Created At",
|
||||||
|
"admin_csv_provider_payment_id": "Provider Payment ID",
|
||||||
"admin_stats_last_sync_header": "Last Panel Sync:",
|
"admin_stats_last_sync_header": "Last Panel Sync:",
|
||||||
"admin_stats_sync_time": "Time",
|
"admin_stats_sync_time": "Time",
|
||||||
"admin_stats_sync_status": "Status",
|
"admin_stats_sync_status": "Status",
|
||||||
|
|||||||
@@ -122,6 +122,28 @@
|
|||||||
"admin_stats_recent_payments_header": "Последние платежи:",
|
"admin_stats_recent_payments_header": "Последние платежи:",
|
||||||
"admin_stats_payment_item": "{status_emoji} {amount} {currency} от {user_info} ({p_status}) [{p_date}]",
|
"admin_stats_payment_item": "{status_emoji} {amount} {currency} от {user_info} ({p_status}) [{p_date}]",
|
||||||
"admin_stats_no_payments_found": "Платежей пока нет.",
|
"admin_stats_no_payments_found": "Платежей пока нет.",
|
||||||
|
|
||||||
|
"admin_view_payments_button": "💰 Платежи",
|
||||||
|
"admin_payments_header": "💰 <b>Все платежи</b>",
|
||||||
|
"admin_no_payments_found": "Платежи не найдены.",
|
||||||
|
"admin_export_payments_csv": "📊 Экспорт CSV",
|
||||||
|
"admin_refresh_payments": "🔄 Обновить",
|
||||||
|
"admin_no_payments_to_export": "Нет платежей для экспорта.",
|
||||||
|
"admin_payments_export_success": "📊 Экспорт платежей завершен!\nВсего записей: {count}",
|
||||||
|
"admin_export_sent": "Файл отправлен!",
|
||||||
|
|
||||||
|
"admin_csv_payment_id": "ID",
|
||||||
|
"admin_csv_user_id": "User ID",
|
||||||
|
"admin_csv_username": "Логин",
|
||||||
|
"admin_csv_first_name": "Имя",
|
||||||
|
"admin_csv_amount": "Сумма",
|
||||||
|
"admin_csv_currency": "Валюта",
|
||||||
|
"admin_csv_provider": "Платежная система",
|
||||||
|
"admin_csv_status": "Статус",
|
||||||
|
"admin_csv_description": "Описание",
|
||||||
|
"admin_csv_months": "Месяцев",
|
||||||
|
"admin_csv_created_at": "Дата создания",
|
||||||
|
"admin_csv_provider_payment_id": "ID платежа в системе",
|
||||||
"admin_stats_last_sync_header": "Последняя синхронизация с панелью:",
|
"admin_stats_last_sync_header": "Последняя синхронизация с панелью:",
|
||||||
"admin_stats_sync_time": "Время",
|
"admin_stats_sync_time": "Время",
|
||||||
"admin_stats_sync_status": "Статус",
|
"admin_stats_sync_status": "Статус",
|
||||||
|
|||||||
Reference in New Issue
Block a user