74b5e6afb8
- Vehicle model: 25+ fields, 5 vehicle types, soft-delete - Vehicle CRUD: 7 API endpoints with JWT auth, filter/sort/paginate - mobile.de: push/update/delete listings, field mapping, retry logic - MobileDeListing model for sync status tracking - Frontend: VehicleList, VehicleForm, VehicleDetail, MobileDeStatus - 73 backend tests (82% coverage), 16 frontend tests
148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
"""Pydantic schemas for vehicle-related request and response bodies."""
|
|
|
|
import uuid
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Literal, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
|
|
VEHICLE_TYPES = Literal["lkw", "pkw", "baumaschine", "stapler", "transporter"]
|
|
CONDITIONS = Literal["new", "used"]
|
|
AVAILABILITY = Literal["available", "reserved", "sold"]
|
|
HOURS_UNITS = Literal["h", "min"]
|
|
|
|
|
|
class VehicleBase(BaseModel):
|
|
"""Base vehicle fields shared across schemas."""
|
|
|
|
make: str = Field(..., min_length=1, max_length=100)
|
|
model: str = Field(..., min_length=1, max_length=100)
|
|
fin: str = Field(..., min_length=17, max_length=17)
|
|
year: Optional[int] = Field(None, ge=1900, le=2100)
|
|
first_registration: Optional[date] = None
|
|
power_kw: Optional[int] = Field(None, ge=0)
|
|
power_hp: Optional[int] = Field(None, ge=0)
|
|
fuel_type: Optional[str] = Field(None, max_length=50)
|
|
transmission: Optional[str] = Field(None, max_length=20)
|
|
color: Optional[str] = Field(None, max_length=50)
|
|
condition: CONDITIONS = "used"
|
|
location: Optional[str] = Field(None, max_length=255)
|
|
availability: AVAILABILITY = "available"
|
|
price: Decimal = Field(..., ge=0)
|
|
vehicle_type: VEHICLE_TYPES
|
|
lkw_type: Optional[str] = Field(None, max_length=50)
|
|
machine_type: Optional[str] = Field(None, max_length=50)
|
|
body_type: Optional[str] = Field(None, max_length=100)
|
|
operating_hours: Optional[Decimal] = Field(None, ge=0)
|
|
operating_hours_unit: Optional[HOURS_UNITS] = None
|
|
mileage_km: Optional[int] = Field(None, ge=0)
|
|
description: Optional[str] = None
|
|
|
|
@model_validator(mode="after")
|
|
def compute_power_hp(self) -> "VehicleBase":
|
|
"""Auto-compute power_hp from power_kw if not provided."""
|
|
if self.power_kw is not None and self.power_hp is None:
|
|
self.power_hp = round(self.power_kw * 1.35962)
|
|
return self
|
|
|
|
|
|
class VehicleCreate(VehicleBase):
|
|
"""POST /api/v1/vehicles request body."""
|
|
pass
|
|
|
|
|
|
class VehicleUpdate(BaseModel):
|
|
"""PUT /api/v1/vehicles/:id request body (all fields optional)."""
|
|
|
|
make: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
model: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
fin: Optional[str] = Field(None, min_length=17, max_length=17)
|
|
year: Optional[int] = Field(None, ge=1900, le=2100)
|
|
first_registration: Optional[date] = None
|
|
power_kw: Optional[int] = Field(None, ge=0)
|
|
power_hp: Optional[int] = Field(None, ge=0)
|
|
fuel_type: Optional[str] = Field(None, max_length=50)
|
|
transmission: Optional[str] = Field(None, max_length=20)
|
|
color: Optional[str] = Field(None, max_length=50)
|
|
condition: Optional[CONDITIONS] = None
|
|
location: Optional[str] = Field(None, max_length=255)
|
|
availability: Optional[AVAILABILITY] = None
|
|
price: Optional[Decimal] = Field(None, ge=0)
|
|
vehicle_type: Optional[VEHICLE_TYPES] = None
|
|
lkw_type: Optional[str] = Field(None, max_length=50)
|
|
machine_type: Optional[str] = Field(None, max_length=50)
|
|
body_type: Optional[str] = Field(None, max_length=100)
|
|
operating_hours: Optional[Decimal] = Field(None, ge=0)
|
|
operating_hours_unit: Optional[HOURS_UNITS] = None
|
|
mileage_km: Optional[int] = Field(None, ge=0)
|
|
description: Optional[str] = None
|
|
|
|
@model_validator(mode="after")
|
|
def compute_power_hp(self) -> "VehicleUpdate":
|
|
"""Auto-compute power_hp from power_kw if not provided."""
|
|
if self.power_kw is not None and self.power_hp is None:
|
|
self.power_hp = round(self.power_kw * 1.35962)
|
|
return self
|
|
|
|
|
|
class VehicleResponse(BaseModel):
|
|
"""Vehicle response schema."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
make: str
|
|
model: str
|
|
fin: str
|
|
year: Optional[int] = None
|
|
first_registration: Optional[date] = None
|
|
power_kw: Optional[int] = None
|
|
power_hp: Optional[int] = None
|
|
fuel_type: Optional[str] = None
|
|
transmission: Optional[str] = None
|
|
color: Optional[str] = None
|
|
condition: str
|
|
location: Optional[str] = None
|
|
availability: str
|
|
price: Decimal
|
|
vehicle_type: str
|
|
lkw_type: Optional[str] = None
|
|
machine_type: Optional[str] = None
|
|
body_type: Optional[str] = None
|
|
operating_hours: Optional[Decimal] = None
|
|
operating_hours_unit: Optional[str] = None
|
|
mileage_km: Optional[int] = None
|
|
description: Optional[str] = None
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
deleted_at: Optional[datetime] = None
|
|
|
|
|
|
class VehicleListResponse(BaseModel):
|
|
"""Paginated vehicle list response."""
|
|
|
|
items: list[VehicleResponse]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
|
|
|
|
class MobileDeStatusResponse(BaseModel):
|
|
"""mobile.de sync status for a vehicle."""
|
|
|
|
synced: bool
|
|
ad_id: Optional[str] = None
|
|
synced_at: Optional[datetime] = None
|
|
sync_status: str = "pending"
|
|
error_log: Optional[str] = None
|
|
|
|
|
|
class MobileDePushResponse(BaseModel):
|
|
"""Response for mobile.de push request."""
|
|
|
|
message: str = "Push queued"
|
|
vehicle_id: uuid.UUID
|
|
listing_id: Optional[uuid.UUID] = None
|