M12: complete daily operations cycle

This commit is contained in:
NuklearRabbit
2026-08-10 03:15:46 +02:00
parent 4a3c3bd0a9
commit 15bdbe40ac
32 changed files with 802 additions and 18 deletions
+98 -1
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import uuid
from datetime import datetime
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response
from sqlalchemy import func, or_, select
@@ -10,12 +10,15 @@ 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.inspection import Inspection
from app.models.vehicle import Vehicle
from app.schemas import (
AvailableVehicleOut,
BookingOut,
BookingPageOut,
CancelBookingRequest,
CheckoutBookingRequest,
CheckoutBookingResult,
CreateBookingRequest,
CurrentUser,
NextBookingRisk,
@@ -154,6 +157,100 @@ def create_booking(
return _to_out(booking, customer, vehicle)
@router.post("/{public_ref}/checkout", response_model=CheckoutBookingResult)
def checkout_booking(
public_ref: str,
body: CheckoutBookingRequest,
db: Session = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
) -> CheckoutBookingResult:
booking = db.scalar(
select(Booking).where(Booking.public_ref == public_ref).with_for_update()
)
if booking is None:
raise HTTPException(status_code=404, detail="Booking not found")
if booking.status != "reserved":
raise HTTPException(status_code=409, detail="Only a reserved booking can be checked out")
if not booking.requirements_complete:
raise HTTPException(status_code=409, detail="Booking requirements are incomplete")
vehicle = db.scalar(select(Vehicle).where(Vehicle.id == booking.vehicle_id).with_for_update())
if vehicle is None:
raise HTTPException(status_code=500, detail="Booking references a missing vehicle")
if not vehicle.active or vehicle.operational_status in {"maintenance", "blocked", "rented"}:
raise HTTPException(status_code=409, detail="Vehicle is not ready for checkout")
active_conflict = db.scalar(
select(Booking.id).where(
Booking.vehicle_id == vehicle.id,
Booking.status == "active",
Booking.id != booking.id,
)
)
if active_conflict is not None:
raise HTTPException(status_code=409, detail="Vehicle already has an active booking")
attention_reasons: list[str] = []
if body.start_odometer_km < vehicle.odometer_km:
attention_reasons.append("odometer_regression")
if not body.cleanliness_ok:
attention_reasons.append("cleanliness")
if body.damage_reported:
attention_reasons.append("damage")
if body.technical_warning:
attention_reasons.append("technical_warning")
inspection = Inspection(
public_ref=f"INSP-{uuid.uuid4().hex[:10].upper()}",
booking_id=booking.id,
vehicle_id=vehicle.id,
type="checkout",
fuel_level_percent=body.fuel_level_percent,
cleanliness_ok=body.cleanliness_ok,
damage_reported=body.damage_reported,
technical_warning=body.technical_warning,
notes=body.notes,
odometer_km=body.start_odometer_km,
completed_at=datetime.now(UTC),
completed_by=user.display_name,
)
db.add(inspection)
if attention_reasons:
booking.status = "blocked"
vehicle.operational_status = (
"maintenance" if body.damage_reported or body.technical_warning else "cleaning"
)
else:
booking.status = "active"
booking.start_odometer_km = body.start_odometer_km
vehicle.odometer_km = max(vehicle.odometer_km, body.start_odometer_km)
vehicle.operational_status = "rented"
vehicle.version += 1
db.flush()
record_audit_event(
db,
actor_type="user",
actor_label=user.display_name,
action="booking_checkout_recorded",
entity_type="booking",
entity_id=booking.id,
after={
"inspection_ref": inspection.public_ref,
"booking_status": booking.status,
"vehicle_status": vehicle.operational_status,
"attention_reasons": attention_reasons,
},
)
db.commit()
return CheckoutBookingResult(
booking_ref=booking.public_ref,
vehicle_ref=vehicle.public_ref,
inspection_ref=inspection.public_ref,
booking_status=booking.status,
resulting_vehicle_status=vehicle.operational_status,
activated=booking.status == "active",
attention_reasons=attention_reasons,
)
@router.get("/availability", response_model=list[AvailableVehicleOut])
def list_available_vehicles(
starts_at: datetime,