diff --git a/bot/handlers/admin/__init__.py b/bot/handlers/admin/__init__.py
index 2444ab2..ecf8b9f 100644
--- a/bot/handlers/admin/__init__.py
+++ b/bot/handlers/admin/__init__.py
@@ -7,6 +7,7 @@ from . import user_management
from . import statistics
from . import sync_admin
from . import logs_admin
+from . import payments
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(sync_admin.router)
admin_router_aggregate.include_router(logs_admin.router)
+admin_router_aggregate.include_router(payments.router)
__all__ = ("admin_router_aggregate", )
diff --git a/bot/handlers/admin/common.py b/bot/handlers/admin/common.py
index a43c57d..8204404 100644
--- a/bot/handlers/admin/common.py
+++ b/bot/handlers/admin/common.py
@@ -123,6 +123,10 @@ async def admin_panel_actions_callback_handler(
await callback.answer(_("admin_sync_initiated_from_panel"))
elif action == "queue_status":
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":
try:
await callback.message.edit_text(
diff --git a/bot/handlers/admin/payments.py b/bot/handlers/admin/payments.py
new file mode 100644
index 0000000..2b4a95e
--- /dev/null
+++ b/bot/handlers/admin/payments.py
@@ -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} {payment.amount} {payment.currency}\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="💰 Все платежи")]
+ text_parts.append(f"📊 Показано {len(payments)} из {total_count} платежей (стр. {page + 1}/{total_pages})\n")
+
+ for i, payment in enumerate(payments, 1):
+ text_parts.append(f"{page * page_size + i}. {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()
\ No newline at end of file
diff --git a/bot/keyboards/inline/admin_keyboards.py b/bot/keyboards/inline/admin_keyboards.py
index bf0d4e7..293100e 100644
--- a/bot/keyboards/inline/admin_keyboards.py
+++ b/bot/keyboards/inline/admin_keyboards.py
@@ -39,12 +39,14 @@ def get_stats_monitoring_keyboard(i18n_instance, lang: str) -> InlineKeyboardMar
builder.button(text=_(key="admin_stats_button"),
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"),
callback_data="admin_action:view_logs_menu")
builder.button(text=_(key="back_to_admin_panel_button"),
callback_data="admin_action:main")
- builder.adjust(2, 1)
+ builder.adjust(2, 1, 1)
return builder.as_markup()
diff --git a/db/dal/payment_dal.py b/db/dal/payment_dal.py
index 5a93e6d..9bb2c12 100644
--- a/db/dal/payment_dal.py
+++ b/db/dal/payment_dal.py
@@ -90,6 +90,22 @@ async def get_recent_payment_logs_with_user(session: AsyncSession,
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(
session: AsyncSession, payment_db_id: int,
provider_payment_id: str, new_status: str) -> Optional[Payment]:
diff --git a/locales/en.json b/locales/en.json
index 43d5ca8..5712a5f 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -122,6 +122,28 @@
"admin_stats_recent_payments_header": "Recent Payments:",
"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_view_payments_button": "💰 Payments",
+ "admin_payments_header": "💰 All Payments",
+ "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_sync_time": "Time",
"admin_stats_sync_status": "Status",
diff --git a/locales/ru.json b/locales/ru.json
index 1cb6640..8197e9a 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -122,6 +122,28 @@
"admin_stats_recent_payments_header": "Последние платежи:",
"admin_stats_payment_item": "{status_emoji} {amount} {currency} от {user_info} ({p_status}) [{p_date}]",
"admin_stats_no_payments_found": "Платежей пока нет.",
+
+ "admin_view_payments_button": "💰 Платежи",
+ "admin_payments_header": "💰 Все платежи",
+ "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_sync_time": "Время",
"admin_stats_sync_status": "Статус",