from __future__ import annotations from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response from sqlalchemy import func, or_, 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.vehicle import Vehicle from app.schemas import ( BookingOut, BookingPageOut, CurrentUser, NextBookingRisk, RegisterReturnRequest, ReturnPreviewResult, ) from app.services.returns import preview_vehicle_return, register_vehicle_return router = APIRouter(prefix="/api/v1/bookings", tags=["bookings"]) def _to_out(booking: Booking, customer: Customer, vehicle: Vehicle) -> BookingOut: return BookingOut( public_ref=booking.public_ref, customer_ref=customer.public_ref, vehicle_ref=vehicle.public_ref, starts_at=booking.starts_at, ends_at=booking.ends_at, status=booking.status, start_odometer_km=booking.start_odometer_km, end_odometer_km=booking.end_odometer_km, requirements_complete=booking.requirements_complete, customer_name=f"{customer.first_name} {customer.last_name}", ) @router.get("", response_model=list[BookingOut] | BookingPageOut) def list_bookings( status: str | None = Query(default=None), vehicle_ref: str | None = Query(default=None), query: str | None = Query(default=None, min_length=1, max_length=100), page: int | None = Query(default=None, ge=1), page_size: int = Query(default=25, ge=1, le=25), db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> list[BookingOut] | BookingPageOut: stmt = select(Booking).order_by(Booking.starts_at.desc()) if status: stmt = stmt.where(Booking.status == status) if vehicle_ref: vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == vehicle_ref)) if vehicle is None: return [] stmt = stmt.where(Booking.vehicle_id == vehicle.id) if query: term = f"%{query.strip()}%" stmt = ( stmt.join(Customer, Booking.customer_id == Customer.id) .join(Vehicle, Booking.vehicle_id == Vehicle.id) .where( or_( Booking.public_ref.ilike(term), Customer.first_name.ilike(term), Customer.last_name.ilike(term), Vehicle.public_ref.ilike(term), ) ) ) total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0 page_number = page or 1 bookings = db.scalars( stmt if page is None else stmt.offset((page_number - 1) * page_size).limit(page_size) ).all() customers = {c.id: c for c in db.scalars(select(Customer)).all()} vehicles = {v.id: v for v in db.scalars(select(Vehicle)).all()} items = [_to_out(b, customers[b.customer_id], vehicles[b.vehicle_id]) for b in bookings] if page is None: return items total_pages = max(1, (total + page_size - 1) // page_size) return BookingPageOut( items=items, page=min(page_number, total_pages), page_size=page_size, total=total, total_pages=total_pages, ) @router.get("/{public_ref}", response_model=BookingOut) def get_booking( public_ref: str, db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> BookingOut: booking = db.scalar(select(Booking).where(Booking.public_ref == public_ref)) if booking is None: raise HTTPException(status_code=404, detail="Booking not found") customer = db.get(Customer, booking.customer_id) vehicle = db.get(Vehicle, booking.vehicle_id) if customer is None or vehicle is None: raise HTTPException(status_code=500, detail="Booking references a missing record") return _to_out(booking, customer, vehicle) @router.post("/{public_ref}/return-preview", response_model=ReturnPreviewResult) def preview_return( public_ref: str, body: RegisterReturnRequest, db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> ReturnPreviewResult: booking, vehicle, evaluation = preview_vehicle_return(db, public_ref, body) return ReturnPreviewResult( booking_ref=booking.public_ref, vehicle_ref=vehicle.public_ref, canonical_odometer_km=evaluation.canonical_odometer_km, submitted_odometer_km=evaluation.submitted_odometer_km, odometer_regression=evaluation.odometer_regression, resulting_odometer_km=evaluation.resulting_odometer_km, resulting_vehicle_status=evaluation.resulting_vehicle_status, status_reason=evaluation.status_reason, status_reason_code=evaluation.status_reason_code, status_reason_params=evaluation.status_reason_params, would_create_quality_issue=evaluation.would_create_quality_issue, attention_reasons=evaluation.attention_reasons, next_booking_risk=( NextBookingRisk(**evaluation.next_booking_risk) if evaluation.next_booking_risk is not None else None ), ) @router.post("/{public_ref}/return") def register_return( public_ref: str, body: RegisterReturnRequest, response: Response, idempotency_key: str = Header(..., alias="Idempotency-Key", min_length=8, max_length=128), db: Session = Depends(get_db), user: CurrentUser = Depends(get_current_user), ) -> dict: status_code, result = register_vehicle_return(db, public_ref, body, idempotency_key, user) response.status_code = status_code return result