mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Add files via upload
This commit is contained in:
@@ -28,6 +28,7 @@ from .admin_promocodes import router as admin_promocodes_router
|
|||||||
from .admin_promocodes import promo_groups_router as admin_promo_groups_router
|
from .admin_promocodes import promo_groups_router as admin_promo_groups_router
|
||||||
from .admin_campaigns import router as admin_campaigns_router
|
from .admin_campaigns import router as admin_campaigns_router
|
||||||
from .admin_users import router as admin_users_router
|
from .admin_users import router as admin_users_router
|
||||||
|
from .admin_payments import router as admin_payments_router
|
||||||
from .media import router as media_router
|
from .media import router as media_router
|
||||||
|
|
||||||
# Main cabinet router
|
# Main cabinet router
|
||||||
@@ -65,5 +66,6 @@ router.include_router(admin_promocodes_router)
|
|||||||
router.include_router(admin_promo_groups_router)
|
router.include_router(admin_promo_groups_router)
|
||||||
router.include_router(admin_campaigns_router)
|
router.include_router(admin_campaigns_router)
|
||||||
router.include_router(admin_users_router)
|
router.include_router(admin_users_router)
|
||||||
|
router.include_router(admin_payments_router)
|
||||||
|
|
||||||
__all__ = ["router"]
|
__all__ = ["router"]
|
||||||
|
|||||||
417
app/cabinet/routes/admin_payments.py
Normal file
417
app/cabinet/routes/admin_payments.py
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
"""Admin routes for payment verification in cabinet."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.database.models import User, PaymentMethod
|
||||||
|
from app.services.payment_service import PaymentService
|
||||||
|
from app.services.payment_verification_service import (
|
||||||
|
list_recent_pending_payments,
|
||||||
|
get_payment_record,
|
||||||
|
run_manual_check,
|
||||||
|
SUPPORTED_MANUAL_CHECK_METHODS,
|
||||||
|
method_display_name,
|
||||||
|
PendingPayment,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..dependencies import get_cabinet_db, get_current_admin_user
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/admin/payments", tags=["Cabinet Admin Payments"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Schemas ============
|
||||||
|
|
||||||
|
class PendingPaymentResponse(BaseModel):
|
||||||
|
"""Pending payment details."""
|
||||||
|
id: int
|
||||||
|
method: str
|
||||||
|
method_display: str
|
||||||
|
identifier: str
|
||||||
|
amount_kopeks: int
|
||||||
|
amount_rubles: float
|
||||||
|
status: str
|
||||||
|
status_emoji: str
|
||||||
|
status_text: str
|
||||||
|
is_paid: bool
|
||||||
|
is_checkable: bool
|
||||||
|
created_at: datetime
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
payment_url: Optional[str] = None
|
||||||
|
user_id: Optional[int] = None
|
||||||
|
user_telegram_id: Optional[int] = None
|
||||||
|
user_username: Optional[str] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class PendingPaymentListResponse(BaseModel):
|
||||||
|
"""Paginated list of pending payments."""
|
||||||
|
items: List[PendingPaymentResponse]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
per_page: int
|
||||||
|
pages: int
|
||||||
|
|
||||||
|
|
||||||
|
class ManualCheckResponse(BaseModel):
|
||||||
|
"""Response after manual payment status check."""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
payment: Optional[PendingPaymentResponse] = None
|
||||||
|
status_changed: bool = False
|
||||||
|
old_status: Optional[str] = None
|
||||||
|
new_status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentsStatsResponse(BaseModel):
|
||||||
|
"""Statistics about pending payments."""
|
||||||
|
total_pending: int
|
||||||
|
by_method: dict
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Helper functions ============
|
||||||
|
|
||||||
|
def _get_status_info(record: PendingPayment) -> tuple[str, str]:
|
||||||
|
"""Get status emoji and text for a pending payment."""
|
||||||
|
status_str = (record.status or "").lower()
|
||||||
|
|
||||||
|
if record.is_paid:
|
||||||
|
return "✅", "Оплачено"
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.PAL24:
|
||||||
|
mapping = {
|
||||||
|
"new": ("⏳", "Ожидает оплаты"),
|
||||||
|
"process": ("⌛", "Обрабатывается"),
|
||||||
|
"success": ("✅", "Оплачено"),
|
||||||
|
"fail": ("❌", "Ошибка"),
|
||||||
|
"canceled": ("❌", "Отменено"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.MULENPAY:
|
||||||
|
mapping = {
|
||||||
|
"created": ("⏳", "Ожидает оплаты"),
|
||||||
|
"processing": ("⌛", "Обрабатывается"),
|
||||||
|
"hold": ("🔒", "На удержании"),
|
||||||
|
"success": ("✅", "Оплачено"),
|
||||||
|
"canceled": ("❌", "Отменено"),
|
||||||
|
"error": ("❌", "Ошибка"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.WATA:
|
||||||
|
mapping = {
|
||||||
|
"opened": ("⏳", "Ожидает оплаты"),
|
||||||
|
"pending": ("⏳", "Ожидает оплаты"),
|
||||||
|
"processing": ("⌛", "Обрабатывается"),
|
||||||
|
"paid": ("✅", "Оплачено"),
|
||||||
|
"closed": ("✅", "Оплачено"),
|
||||||
|
"declined": ("❌", "Отклонено"),
|
||||||
|
"canceled": ("❌", "Отменено"),
|
||||||
|
"expired": ("⌛", "Истёк"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.PLATEGA:
|
||||||
|
mapping = {
|
||||||
|
"pending": ("⏳", "Ожидает оплаты"),
|
||||||
|
"inprogress": ("⌛", "Обрабатывается"),
|
||||||
|
"confirmed": ("✅", "Оплачено"),
|
||||||
|
"failed": ("❌", "Ошибка"),
|
||||||
|
"canceled": ("❌", "Отменено"),
|
||||||
|
"expired": ("⌛", "Истёк"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.HELEKET:
|
||||||
|
if status_str in {"pending", "created", "waiting", "check", "processing"}:
|
||||||
|
return "⏳", "Ожидает оплаты"
|
||||||
|
if status_str in {"paid", "paid_over"}:
|
||||||
|
return "✅", "Оплачено"
|
||||||
|
if status_str in {"cancel", "canceled", "fail", "failed", "expired"}:
|
||||||
|
return "❌", "Отменено"
|
||||||
|
return "❓", "Неизвестно"
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.YOOKASSA:
|
||||||
|
mapping = {
|
||||||
|
"pending": ("⏳", "Ожидает оплаты"),
|
||||||
|
"waiting_for_capture": ("⌛", "Обрабатывается"),
|
||||||
|
"succeeded": ("✅", "Оплачено"),
|
||||||
|
"canceled": ("❌", "Отменено"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.CRYPTOBOT:
|
||||||
|
mapping = {
|
||||||
|
"active": ("⏳", "Ожидает оплаты"),
|
||||||
|
"paid": ("✅", "Оплачено"),
|
||||||
|
"expired": ("⌛", "Истёк"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.CLOUDPAYMENTS:
|
||||||
|
mapping = {
|
||||||
|
"pending": ("⏳", "Ожидает оплаты"),
|
||||||
|
"authorized": ("⌛", "Авторизовано"),
|
||||||
|
"completed": ("✅", "Оплачено"),
|
||||||
|
"failed": ("❌", "Ошибка"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.FREEKASSA:
|
||||||
|
mapping = {
|
||||||
|
"pending": ("⏳", "Ожидает оплаты"),
|
||||||
|
"success": ("✅", "Оплачено"),
|
||||||
|
"paid": ("✅", "Оплачено"),
|
||||||
|
"canceled": ("❌", "Отменено"),
|
||||||
|
"error": ("❌", "Ошибка"),
|
||||||
|
}
|
||||||
|
return mapping.get(status_str, ("❓", "Неизвестно"))
|
||||||
|
|
||||||
|
return "❓", "Неизвестно"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_checkable(record: PendingPayment) -> bool:
|
||||||
|
"""Check if payment can be manually checked."""
|
||||||
|
if record.method not in SUPPORTED_MANUAL_CHECK_METHODS:
|
||||||
|
return False
|
||||||
|
if not record.is_recent():
|
||||||
|
return False
|
||||||
|
status_str = (record.status or "").lower()
|
||||||
|
if record.method == PaymentMethod.PAL24:
|
||||||
|
return status_str in {"new", "process"}
|
||||||
|
if record.method == PaymentMethod.MULENPAY:
|
||||||
|
return status_str in {"created", "processing", "hold"}
|
||||||
|
if record.method == PaymentMethod.WATA:
|
||||||
|
return status_str in {"opened", "pending", "processing", "inprogress", "in_progress"}
|
||||||
|
if record.method == PaymentMethod.PLATEGA:
|
||||||
|
return status_str in {"pending", "inprogress", "in_progress"}
|
||||||
|
if record.method == PaymentMethod.HELEKET:
|
||||||
|
return status_str not in {"paid", "paid_over", "cancel", "canceled", "fail", "failed", "expired"}
|
||||||
|
if record.method == PaymentMethod.YOOKASSA:
|
||||||
|
return status_str in {"pending", "waiting_for_capture"}
|
||||||
|
if record.method == PaymentMethod.CRYPTOBOT:
|
||||||
|
return status_str in {"active"}
|
||||||
|
if record.method == PaymentMethod.CLOUDPAYMENTS:
|
||||||
|
return status_str in {"pending", "authorized"}
|
||||||
|
if record.method == PaymentMethod.FREEKASSA:
|
||||||
|
return status_str in {"pending", "created", "processing"}
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_payment_url(record: PendingPayment) -> Optional[str]:
|
||||||
|
"""Extract payment URL from record."""
|
||||||
|
payment = record.payment
|
||||||
|
payment_url = getattr(payment, "payment_url", None)
|
||||||
|
|
||||||
|
if record.method == PaymentMethod.PAL24:
|
||||||
|
payment_url = getattr(payment, "link_url", None) or getattr(payment, "link_page_url", None) or payment_url
|
||||||
|
elif record.method == PaymentMethod.WATA:
|
||||||
|
payment_url = getattr(payment, "url", None) or payment_url
|
||||||
|
elif record.method == PaymentMethod.YOOKASSA:
|
||||||
|
payment_url = getattr(payment, "confirmation_url", None) or payment_url
|
||||||
|
elif record.method == PaymentMethod.CRYPTOBOT:
|
||||||
|
payment_url = (
|
||||||
|
getattr(payment, "bot_invoice_url", None)
|
||||||
|
or getattr(payment, "mini_app_invoice_url", None)
|
||||||
|
or getattr(payment, "web_app_invoice_url", None)
|
||||||
|
or payment_url
|
||||||
|
)
|
||||||
|
elif record.method == PaymentMethod.PLATEGA:
|
||||||
|
payment_url = getattr(payment, "redirect_url", None) or payment_url
|
||||||
|
elif record.method == PaymentMethod.CLOUDPAYMENTS:
|
||||||
|
payment_url = getattr(payment, "payment_url", None) or payment_url
|
||||||
|
elif record.method == PaymentMethod.FREEKASSA:
|
||||||
|
payment_url = getattr(payment, "payment_url", None) or payment_url
|
||||||
|
|
||||||
|
return payment_url
|
||||||
|
|
||||||
|
|
||||||
|
def _record_to_response(record: PendingPayment) -> PendingPaymentResponse:
|
||||||
|
"""Convert PendingPayment to API response."""
|
||||||
|
status_emoji, status_text = _get_status_info(record)
|
||||||
|
return PendingPaymentResponse(
|
||||||
|
id=record.local_id,
|
||||||
|
method=record.method.value,
|
||||||
|
method_display=method_display_name(record.method),
|
||||||
|
identifier=record.identifier,
|
||||||
|
amount_kopeks=record.amount_kopeks,
|
||||||
|
amount_rubles=record.amount_kopeks / 100,
|
||||||
|
status=record.status or "",
|
||||||
|
status_emoji=status_emoji,
|
||||||
|
status_text=status_text,
|
||||||
|
is_paid=record.is_paid,
|
||||||
|
is_checkable=_is_checkable(record),
|
||||||
|
created_at=record.created_at,
|
||||||
|
expires_at=record.expires_at,
|
||||||
|
payment_url=_get_payment_url(record),
|
||||||
|
user_id=record.user.id if record.user else None,
|
||||||
|
user_telegram_id=record.user.telegram_id if record.user else None,
|
||||||
|
user_username=record.user.username if record.user else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Routes ============
|
||||||
|
|
||||||
|
@router.get("", response_model=PendingPaymentListResponse)
|
||||||
|
async def get_all_pending_payments(
|
||||||
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
|
per_page: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||||
|
method_filter: Optional[str] = Query(None, description="Filter by payment method"),
|
||||||
|
admin: User = Depends(get_current_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
|
):
|
||||||
|
"""Get all pending payments for admin verification."""
|
||||||
|
all_pending = await list_recent_pending_payments(db)
|
||||||
|
|
||||||
|
# Apply method filter if specified
|
||||||
|
if method_filter:
|
||||||
|
try:
|
||||||
|
filter_method = PaymentMethod(method_filter)
|
||||||
|
all_pending = [p for p in all_pending if p.method == filter_method]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
total = len(all_pending)
|
||||||
|
pages = math.ceil(total / per_page) if total > 0 else 1
|
||||||
|
|
||||||
|
# Paginate
|
||||||
|
start_idx = (page - 1) * per_page
|
||||||
|
page_payments = all_pending[start_idx:start_idx + per_page]
|
||||||
|
|
||||||
|
items = [_record_to_response(p) for p in page_payments]
|
||||||
|
|
||||||
|
return PendingPaymentListResponse(
|
||||||
|
items=items,
|
||||||
|
total=total,
|
||||||
|
page=page,
|
||||||
|
per_page=per_page,
|
||||||
|
pages=pages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats", response_model=PaymentsStatsResponse)
|
||||||
|
async def get_payments_stats(
|
||||||
|
admin: User = Depends(get_current_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
|
):
|
||||||
|
"""Get statistics about pending payments."""
|
||||||
|
all_pending = await list_recent_pending_payments(db)
|
||||||
|
|
||||||
|
by_method = {}
|
||||||
|
for p in all_pending:
|
||||||
|
method_name = method_display_name(p.method)
|
||||||
|
if method_name not in by_method:
|
||||||
|
by_method[method_name] = 0
|
||||||
|
by_method[method_name] += 1
|
||||||
|
|
||||||
|
return PaymentsStatsResponse(
|
||||||
|
total_pending=len(all_pending),
|
||||||
|
by_method=by_method,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{method}/{payment_id}", response_model=PendingPaymentResponse)
|
||||||
|
async def get_pending_payment_details(
|
||||||
|
method: str,
|
||||||
|
payment_id: int,
|
||||||
|
admin: User = Depends(get_current_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
|
):
|
||||||
|
"""Get details of a specific pending payment."""
|
||||||
|
try:
|
||||||
|
payment_method = PaymentMethod(method)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Invalid payment method: {method}",
|
||||||
|
)
|
||||||
|
|
||||||
|
record = await get_payment_record(db, payment_method, payment_id)
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Payment not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
return _record_to_response(record)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{method}/{payment_id}/check", response_model=ManualCheckResponse)
|
||||||
|
async def check_payment_status(
|
||||||
|
method: str,
|
||||||
|
payment_id: int,
|
||||||
|
admin: User = Depends(get_current_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
|
):
|
||||||
|
"""Manually check and update payment status."""
|
||||||
|
try:
|
||||||
|
payment_method = PaymentMethod(method)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Invalid payment method: {method}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get current record
|
||||||
|
record = await get_payment_record(db, payment_method, payment_id)
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Payment not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if manual check is available
|
||||||
|
if not _is_checkable(record):
|
||||||
|
return ManualCheckResponse(
|
||||||
|
success=False,
|
||||||
|
message="Ручная проверка недоступна для этого платежа",
|
||||||
|
payment=_record_to_response(record),
|
||||||
|
status_changed=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
old_status = record.status
|
||||||
|
old_is_paid = record.is_paid
|
||||||
|
|
||||||
|
# Run manual check
|
||||||
|
payment_service = PaymentService()
|
||||||
|
updated = await run_manual_check(db, payment_method, payment_id, payment_service)
|
||||||
|
|
||||||
|
if not updated:
|
||||||
|
return ManualCheckResponse(
|
||||||
|
success=False,
|
||||||
|
message="Не удалось проверить статус платежа",
|
||||||
|
payment=_record_to_response(record),
|
||||||
|
status_changed=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
status_changed = updated.status != old_status or updated.is_paid != old_is_paid
|
||||||
|
|
||||||
|
if status_changed:
|
||||||
|
_, new_status_text = _get_status_info(updated)
|
||||||
|
message = f"Статус обновлён: {new_status_text}"
|
||||||
|
logger.info(
|
||||||
|
f"Admin {admin.id} checked payment {method}/{payment_id}: {old_status} -> {updated.status}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = "Статус не изменился"
|
||||||
|
|
||||||
|
return ManualCheckResponse(
|
||||||
|
success=True,
|
||||||
|
message=message,
|
||||||
|
payment=_record_to_response(updated),
|
||||||
|
status_changed=status_changed,
|
||||||
|
old_status=old_status,
|
||||||
|
new_status=updated.status,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user