Files
drivers_bot/app/api/admin.py
2026-05-14 21:19:37 +09:00

277 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import UTC, datetime
import httpx
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_telegram_user, log_audit, require_platform_role
from app.core.config import settings
from app.db.session import get_session
from app.models.car import (
AuditLog,
ServiceCenter,
ServiceCenterVerification,
ServiceEmployee,
ServiceVisit,
)
from app.models.user import User
from app.schemas.service_center import AdminModerationDecision, ServiceCenterRead, ServiceVisitRead
router = APIRouter(prefix="/admin", tags=["admin"])
def require_admin_or_verifier(user: User) -> None:
require_platform_role(user, {"admin", "verifier", "moderator"})
@router.get("/service-centers/pending", response_model=list[ServiceCenterRead])
async def pending_service_centers(
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> list[ServiceCenter]:
require_admin_or_verifier(current_user)
result = await session.execute(
select(ServiceCenter)
.where(ServiceCenter.verification_status == "pending")
.order_by(ServiceCenter.created_at.asc())
)
return list(result.scalars())
@router.get("/service-centers/{service_center_id}", response_model=ServiceCenterRead)
async def admin_service_center_detail(
service_center_id: int,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ServiceCenter:
require_admin_or_verifier(current_user)
center = await session.get(ServiceCenter, service_center_id)
if center is None:
raise HTTPException(status_code=404, detail="Service center not found")
return center
@router.post("/service-centers/{service_center_id}/verify", response_model=ServiceCenterRead)
async def verify_service_center(
service_center_id: int,
payload: AdminModerationDecision | None = None,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ServiceCenter:
require_admin_or_verifier(current_user)
center = await session.get(ServiceCenter, service_center_id)
if center is None:
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "approved"
center.verified_at = datetime.now(UTC)
if center.owner_user_id:
owner = await session.get(User, center.owner_user_id)
if owner:
owner.platform_role = "service_owner"
await ensure_owner_employee(session, center.id, owner.id)
await notify_user(owner, f"Заявка СТО «{center.display_name or center.name}» одобрена. Панель СТО доступна в CarPass.")
await mark_latest_verification(session, center.id, "approved", current_user.id, payload)
await log_audit(
session,
actor=current_user,
action="service_center.verify",
target_type="service_center",
target_id=center.id,
metadata={"comment": payload.comment if payload else None},
)
await session.commit()
await session.refresh(center)
return center
@router.post("/service-centers/{service_center_id}/reject", response_model=ServiceCenterRead)
async def reject_service_center(
service_center_id: int,
payload: AdminModerationDecision | None = None,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ServiceCenter:
require_admin_or_verifier(current_user)
center = await session.get(ServiceCenter, service_center_id)
if center is None:
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "rejected"
if center.owner_user_id:
owner = await session.get(User, center.owner_user_id)
if owner:
reason = payload.reason or payload.comment if payload else None
await notify_user(owner, f"Заявка СТО «{center.display_name or center.name}» отклонена.{f' Причина: {reason}' if reason else ''}")
await mark_latest_verification(session, center.id, "rejected", current_user.id, payload)
await log_audit(
session,
actor=current_user,
action="service_center.reject",
target_type="service_center",
target_id=center.id,
metadata={"reason": payload.reason if payload else None, "comment": payload.comment if payload else None},
)
await session.commit()
await session.refresh(center)
return center
@router.post("/service-centers/{service_center_id}/suspend", response_model=ServiceCenterRead)
async def suspend_service_center(
service_center_id: int,
payload: AdminModerationDecision | None = None,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ServiceCenter:
require_platform_role(current_user, {"admin"})
center = await session.get(ServiceCenter, service_center_id)
if center is None:
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "suspended"
center.suspended_at = datetime.now(UTC)
if center.owner_user_id:
owner = await session.get(User, center.owner_user_id)
if owner:
reason = payload.reason or payload.comment if payload else None
await notify_user(owner, f"СТО «{center.display_name or center.name}» временно заблокировано.{f' Причина: {reason}' if reason else ''}")
await log_audit(
session,
actor=current_user,
action="service_center.suspend",
target_type="service_center",
target_id=center.id,
metadata={"reason": payload.reason if payload else None, "comment": payload.comment if payload else None},
)
await session.commit()
await session.refresh(center)
return center
@router.post("/service-centers/{service_center_id}/request-changes", response_model=ServiceCenterRead)
async def request_service_center_changes(
service_center_id: int,
payload: AdminModerationDecision,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> ServiceCenter:
require_admin_or_verifier(current_user)
center = await session.get(ServiceCenter, service_center_id)
if center is None:
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "needs_changes"
if center.owner_user_id:
owner = await session.get(User, center.owner_user_id)
if owner:
reason = payload.reason or payload.comment or "Администратор попросил уточнить данные заявки."
await notify_user(owner, f"По заявке СТО «{center.display_name or center.name}» нужны правки: {reason}")
await mark_latest_verification(session, center.id, "needs_changes", current_user.id, payload)
await log_audit(
session,
actor=current_user,
action="service_center.request_changes",
target_type="service_center",
target_id=center.id,
metadata={"reason": payload.reason, "comment": payload.comment},
)
await session.commit()
await session.refresh(center)
return center
@router.get("/audit-log")
async def audit_log(
limit: int = 100,
offset: int = 0,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> list[dict]:
require_platform_role(current_user, {"admin", "verifier", "moderator"})
limit = min(max(limit, 1), 200)
result = await session.execute(
select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit).offset(max(offset, 0))
)
return [
{
"id": item.id,
"actor_user_id": item.actor_user_id,
"actor_role": item.actor_role,
"action": item.action,
"target_type": item.target_type,
"target_id": item.target_id,
"metadata_json": item.metadata_json,
"created_at": item.created_at,
}
for item in result.scalars()
]
@router.get("/disputes", response_model=list[ServiceVisitRead])
async def disputes(
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> list[ServiceVisit]:
require_admin_or_verifier(current_user)
result = await session.execute(
select(ServiceVisit).where(ServiceVisit.status == "disputed").order_by(ServiceVisit.updated_at.desc())
)
return list(result.scalars())
async def mark_latest_verification(
session: AsyncSession,
service_center_id: int,
status: str,
reviewed_by: int,
payload: AdminModerationDecision | None = None,
) -> None:
result = await session.execute(
select(ServiceCenterVerification)
.where(ServiceCenterVerification.service_center_id == service_center_id)
.order_by(ServiceCenterVerification.created_at.desc())
.limit(1)
)
verification = result.scalar_one_or_none()
if verification:
verification.status = status
verification.reviewed_by = reviewed_by
verification.reviewed_at = datetime.now(UTC)
if payload and (payload.reason or payload.comment):
verification.comment = "\n".join(
item for item in [payload.reason, payload.comment] if item
)
async def ensure_owner_employee(session: AsyncSession, service_center_id: int, owner_user_id: int) -> None:
result = await session.execute(
select(ServiceEmployee).where(
ServiceEmployee.service_center_id == service_center_id,
ServiceEmployee.user_id == owner_user_id,
)
)
employee = result.scalar_one_or_none()
if employee is None:
session.add(
ServiceEmployee(
service_center_id=service_center_id,
user_id=owner_user_id,
role="owner",
status="active",
)
)
else:
employee.role = "owner"
employee.status = "active"
async def notify_user(user: User, text: str) -> None:
if not settings.bot_token:
return
try:
async with httpx.AsyncClient(timeout=5) as client:
await client.post(
f"https://api.telegram.org/bot{settings.bot_token}/sendMessage",
data={"chat_id": str(user.telegram_id), "text": text},
)
except Exception:
return