Files
erp-nutzfahrzeuge/backend/app/routers/vehicles.py
T
Leopoldadmin 341d0c6f38 fix: resolve all ruff and ESLint lint errors
Backend (ruff):
- F821: Add TYPE_CHECKING imports for Vehicle, Contact, File in models
  (file.py, retouch.py, sale.py, vehicle.py)
- E741: Rename ambiguous variable  to /
  (copilot_service.py, price_compare_service.py, openrouter.py)
- F841: Remove unused variable  in sale_service.py

Frontend (ESLint):
- react/no-unescaped-entities: Escape quotes in ContractPreview.tsx
- @next/next/no-img-element: Replace <img> with <Image> from next/image
  (FileGallery.tsx, FileList.tsx, FilePreview.tsx, BeforeAfterSlider.tsx)

Tests: 392 backend passed, 112 frontend passed, next build successful
2026-07-17 21:16:38 +02:00

199 lines
6.8 KiB
Python

"""Vehicles router: CRUD, filtering, sorting, and mobile.de integration endpoints."""
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, get_pagination
from app.models.user import User
from app.schemas.vehicle import (
MobileDePushResponse,
MobileDeStatusResponse,
VehicleCreate,
VehicleListResponse,
VehicleResponse,
VehicleUpdate,
)
from app.services import vehicle_service
from app.services import mobilede_service
router = APIRouter(prefix="/vehicles", tags=["vehicles"])
@router.get("/", response_model=VehicleListResponse, status_code=status.HTTP_200_OK)
async def list_vehicles(
db: AsyncSession = Depends(get_db),
pagination: dict = Depends(get_pagination),
vehicle_type: str | None = Query(None, description="Filter by vehicle type"),
availability: str | None = Query(None, description="Filter by availability"),
min_price: float | None = Query(None, ge=0, description="Minimum price"),
max_price: float | None = Query(None, ge=0, description="Maximum price"),
search: str | None = Query(None, description="Search in make, model, fin, location"),
sort: str | None = Query(None, description="Sort field (prefix - for descending)"),
current_user: User = Depends(get_current_user),
):
"""List vehicles with pagination, filtering, and sorting."""
vehicles, total = await vehicle_service.list_vehicles(
db,
page=pagination["page"],
page_size=pagination["page_size"],
vehicle_type=vehicle_type,
availability=availability,
min_price=min_price,
max_price=max_price,
search=search,
sort=sort,
)
return VehicleListResponse(
items=[VehicleResponse.model_validate(v) for v in vehicles],
total=total,
page=pagination["page"],
page_size=pagination["page_size"],
)
@router.post("/", response_model=VehicleResponse, status_code=status.HTTP_201_CREATED)
async def create_vehicle(
body: VehicleCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new vehicle."""
data = body.model_dump(exclude_unset=False)
try:
vehicle = await vehicle_service.create_vehicle(db, data)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"error": {"code": "DUPLICATE_FIN", "message": str(exc)}},
)
return VehicleResponse.model_validate(vehicle)
@router.get("/{vehicle_id}", response_model=VehicleResponse, status_code=status.HTTP_200_OK)
async def get_vehicle(
vehicle_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get a single vehicle by ID."""
vehicle = await vehicle_service.get_vehicle_by_id(db, vehicle_id)
if vehicle is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": "Vehicle not found"}},
)
return VehicleResponse.model_validate(vehicle)
@router.put("/{vehicle_id}", response_model=VehicleResponse, status_code=status.HTTP_200_OK)
async def update_vehicle(
vehicle_id: uuid.UUID,
body: VehicleUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update a vehicle's fields."""
updates = body.model_dump(exclude_unset=True)
if not updates:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": {"code": "NO_FIELDS", "message": "No fields to update"}},
)
try:
vehicle = await vehicle_service.update_vehicle(db, vehicle_id, updates)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"error": {"code": "DUPLICATE_FIN", "message": str(exc)}},
)
if vehicle is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": "Vehicle not found"}},
)
return VehicleResponse.model_validate(vehicle)
@router.delete("/{vehicle_id}", response_model=VehicleResponse, status_code=status.HTTP_200_OK)
async def delete_vehicle(
vehicle_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Soft-delete a vehicle (sets deleted_at)."""
vehicle = await vehicle_service.soft_delete_vehicle(db, vehicle_id)
if vehicle is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": "Vehicle not found"}},
)
return VehicleResponse.model_validate(vehicle)
@router.post(
"/{vehicle_id}/mobile-de/push",
response_model=MobileDePushResponse,
status_code=status.HTTP_202_ACCEPTED,
)
async def push_to_mobile_de(
vehicle_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Push a vehicle listing to mobile.de (async, returns 202)."""
vehicle = await vehicle_service.get_vehicle_by_id(db, vehicle_id)
if vehicle is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": "Vehicle not found"}},
)
listing = await mobilede_service.push_listing(db, vehicle)
return MobileDePushResponse(
message="Push queued",
vehicle_id=vehicle.id,
listing_id=listing.id,
)
@router.get(
"/{vehicle_id}/mobile-de/status",
response_model=MobileDeStatusResponse,
status_code=status.HTTP_200_OK,
)
async def get_mobile_de_status(
vehicle_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get the mobile.de sync status for a vehicle."""
# Verify vehicle exists
vehicle = await vehicle_service.get_vehicle_by_id(db, vehicle_id)
if vehicle is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": "Vehicle not found"}},
)
listing = await mobilede_service.get_listing_status(db, vehicle_id)
if listing is None:
return MobileDeStatusResponse(
synced=False,
ad_id=None,
synced_at=None,
sync_status="pending",
error_log=None,
)
return MobileDeStatusResponse(
synced=(listing.sync_status == "synced"),
ad_id=listing.ad_id,
synced_at=listing.synced_at,
sync_status=listing.sync_status,
error_log=listing.error_log,
)