feat(T02): vehicle management + mobile.de push + vehicle UI
- 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
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user