Complete CarPass product flows

This commit is contained in:
VPN SaaS Dev
2026-05-14 21:19:37 +09:00
parent a83f55c646
commit c0014ab4ea
28 changed files with 3006 additions and 159 deletions

View File

@@ -1,14 +1,22 @@
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, ServiceVisit
from app.models.car import (
AuditLog,
ServiceCenter,
ServiceCenterVerification,
ServiceEmployee,
ServiceVisit,
)
from app.models.user import User
from app.schemas.service_center import ServiceCenterRead, ServiceVisitRead
from app.schemas.service_center import AdminModerationDecision, ServiceCenterRead, ServiceVisitRead
router = APIRouter(prefix="/admin", tags=["admin"])
@@ -31,9 +39,23 @@ async def pending_service_centers(
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:
@@ -43,8 +65,21 @@ async def verify_service_center(
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "approved"
center.verified_at = datetime.now(UTC)
await mark_latest_verification(session, center.id, "approved", current_user.id)
await log_audit(session, actor=current_user, action="service_center.verify", target_type="service_center", target_id=center.id)
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
@@ -53,6 +88,7 @@ async def verify_service_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:
@@ -61,8 +97,20 @@ async def reject_service_center(
if center is None:
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "rejected"
await mark_latest_verification(session, center.id, "rejected", current_user.id)
await log_audit(session, actor=current_user, action="service_center.reject", target_type="service_center", target_id=center.id)
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
@@ -71,6 +119,7 @@ async def reject_service_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:
@@ -80,7 +129,50 @@ async def suspend_service_center(
raise HTTPException(status_code=404, detail="Service center not found")
center.verification_status = "suspended"
center.suspended_at = datetime.now(UTC)
await log_audit(session, actor=current_user, action="service_center.suspend", target_type="service_center", target_id=center.id)
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
@@ -126,7 +218,11 @@ async def disputes(
async def mark_latest_verification(
session: AsyncSession, service_center_id: int, status: str, reviewed_by: int
session: AsyncSession,
service_center_id: int,
status: str,
reviewed_by: int,
payload: AdminModerationDecision | None = None,
) -> None:
result = await session.execute(
select(ServiceCenterVerification)
@@ -139,3 +235,42 @@ async def mark_latest_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