Files
MobilityOps/backend/app/api/routers/vehicles.py
T
NuklearRabbit 03c5b60235 M1: implement operational core
Demo auth, seed import/reset, dashboard, vehicle/booking list+detail, audit trail. Backend: 19 tests passing, ruff clean. Frontend: React Router shell, typed API client, responsive pages. Verified end-to-end via curl and browser.
2026-08-01 21:20:53 +02:00

165 lines
5.5 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_db
from app.models.booking import Booking
from app.models.customer import Customer
from app.models.data_quality import DataQualityIssue
from app.models.inspection import Inspection
from app.models.maintenance import MaintenanceRecord
from app.models.vehicle import Vehicle
from app.schemas import (
BookingSummaryOut,
CurrentUser,
DataQualityIssueOut,
InspectionOut,
MaintenanceOut,
VehicleDetailOut,
VehicleOut,
)
router = APIRouter(prefix="/api/v1/vehicles", tags=["vehicles"])
def _attention_vehicle_ids(db: Session) -> set:
rows = db.scalars(
select(DataQualityIssue.entity_id).where(
DataQualityIssue.entity_type == "vehicle",
DataQualityIssue.status == "open",
)
).all()
return set(rows)
@router.get("", response_model=list[VehicleOut])
def list_vehicles(
status: str | None = Query(default=None),
attention_only: bool = Query(default=False),
db: Session = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
) -> list[VehicleOut]:
stmt = select(Vehicle).order_by(Vehicle.public_ref)
if status:
stmt = stmt.where(Vehicle.operational_status == status)
vehicles = db.scalars(stmt).all()
attention_ids = _attention_vehicle_ids(db)
out = [
VehicleOut(
public_ref=v.public_ref,
make=v.make,
model=v.model,
model_year=v.model_year,
registration_number=v.registration_number,
location=v.location,
operational_status=v.operational_status,
odometer_km=v.odometer_km,
next_service_km=v.next_service_km,
active=v.active,
attention=v.id in attention_ids or v.operational_status == "blocked",
)
for v in vehicles
]
if attention_only:
out = [v for v in out if v.attention]
return out
@router.get("/{public_ref}", response_model=VehicleDetailOut)
def get_vehicle(
public_ref: str,
db: Session = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
) -> VehicleDetailOut:
vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == public_ref))
if vehicle is None:
raise HTTPException(status_code=404, detail="Vehicle not found")
bookings = db.scalars(
select(Booking).where(Booking.vehicle_id == vehicle.id).order_by(Booking.starts_at.desc())
).all()
customer_ref_by_id = {c.id: c.public_ref for c in db.scalars(select(Customer)).all()}
inspections = db.scalars(
select(Inspection)
.where(Inspection.vehicle_id == vehicle.id)
.order_by(Inspection.completed_at.desc())
).all()
maintenance = db.scalars(
select(MaintenanceRecord)
.where(MaintenanceRecord.vehicle_id == vehicle.id)
.order_by(MaintenanceRecord.occurred_at.desc())
).all()
issues = db.scalars(
select(DataQualityIssue)
.where(DataQualityIssue.entity_type == "vehicle", DataQualityIssue.entity_id == vehicle.id)
.order_by(DataQualityIssue.detected_at.desc())
).all()
booking_by_id = {b.id: b.public_ref for b in bookings}
attention_ids = _attention_vehicle_ids(db)
return VehicleDetailOut(
public_ref=vehicle.public_ref,
make=vehicle.make,
model=vehicle.model,
model_year=vehicle.model_year,
registration_number=vehicle.registration_number,
location=vehicle.location,
operational_status=vehicle.operational_status,
odometer_km=vehicle.odometer_km,
next_service_km=vehicle.next_service_km,
active=vehicle.active,
attention=vehicle.id in attention_ids or vehicle.operational_status == "blocked",
bookings=[
BookingSummaryOut(
public_ref=b.public_ref,
customer_ref=customer_ref_by_id.get(b.customer_id, ""),
vehicle_ref=vehicle.public_ref,
starts_at=b.starts_at,
ends_at=b.ends_at,
status=b.status,
)
for b in bookings
],
inspections=[
InspectionOut(
public_ref=i.public_ref,
booking_ref=booking_by_id.get(i.booking_id, ""),
type=i.type,
fuel_level_percent=i.fuel_level_percent,
cleanliness_ok=i.cleanliness_ok,
damage_reported=i.damage_reported,
technical_warning=i.technical_warning,
odometer_km=i.odometer_km,
completed_at=i.completed_at,
)
for i in inspections
],
maintenance=[
MaintenanceOut(
public_ref=m.public_ref,
occurred_at=m.occurred_at,
odometer_km=m.odometer_km,
category=m.category,
summary=m.summary,
)
for m in maintenance
],
quality_issues=[
DataQualityIssueOut(
public_ref=q.public_ref,
rule_type=q.rule_type,
entity_type=q.entity_type,
entity_ref=vehicle.public_ref,
severity=q.severity,
status=q.status,
evidence=q.evidence_json,
detected_at=q.detected_at,
resolved_at=q.resolved_at,
)
for q in issues
],
)