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,145 @@
|
||||
"""Field mapping utilities for mobile.de listing format conversion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from app.models.vehicle import Vehicle
|
||||
|
||||
|
||||
# mobile.de category mapping based on vehicle_type
|
||||
_CATEGORY_MAP: dict[str, str] = {
|
||||
"lkw": "Truck",
|
||||
"pkw": "Car",
|
||||
"baumaschine": "ConstructionMachine",
|
||||
"stapler": "ForkliftTruck",
|
||||
"transporter": "Van",
|
||||
}
|
||||
|
||||
# LKW sub-type mapping for mobile.de category refinement
|
||||
_LKW_TYPE_MAP: dict[str, str] = {
|
||||
"sattelzugmaschine": "SemiTractor",
|
||||
"sattelauflieger": "SemiTrailer",
|
||||
"kipper": "Tipper",
|
||||
"kuehlmobil": "RefrigeratedVehicle",
|
||||
"tieflader": "LowLoader",
|
||||
"silofahrzeug": "SiloVehicle",
|
||||
"tankwagen": "TankVehicle",
|
||||
"kranwagen": "CraneVehicle",
|
||||
"muldenkipper": "DumperTruck",
|
||||
"sattelkipper": "SemiTipper",
|
||||
"schwertransporter": "HeavyTransporter",
|
||||
"sonderkonstruktion": "SpecialConstruction",
|
||||
"pritsche": "Flatbed",
|
||||
"planenlkw": "TarpaulinTruck",
|
||||
"boxenlkw": "BoxBodyTruck",
|
||||
"iso_lkw": "IsoTruck",
|
||||
}
|
||||
|
||||
|
||||
def _format_first_registration(reg_date: date | None) -> str | None:
|
||||
"""Convert date to YYYY-MM format for mobile.de."""
|
||||
if reg_date is None:
|
||||
return None
|
||||
return reg_date.strftime("%Y-%m")
|
||||
|
||||
|
||||
def _format_mileage(vehicle: Vehicle) -> dict[str, Any] | None:
|
||||
"""Map mileage or operating hours to mobile.de mileage field."""
|
||||
if vehicle.mileage_km is not None:
|
||||
return {"value": vehicle.mileage_km, "unit": "km"}
|
||||
if vehicle.operating_hours is not None:
|
||||
unit = vehicle.operating_hours_unit or "h"
|
||||
return {"value": float(vehicle.operating_hours), "unit": unit}
|
||||
return None
|
||||
|
||||
|
||||
def _format_price(price: Decimal) -> dict[str, Any]:
|
||||
"""Map price to mobile.de price format."""
|
||||
return {
|
||||
"amount": float(price),
|
||||
"currency": "EUR",
|
||||
}
|
||||
|
||||
|
||||
def _format_power(power_kw: int | None, power_hp: int | None) -> dict[str, Any] | None:
|
||||
"""Map power to mobile.de power format."""
|
||||
if power_kw is None and power_hp is None:
|
||||
return None
|
||||
return {
|
||||
"powerKw": power_kw,
|
||||
"powerHp": power_hp,
|
||||
}
|
||||
|
||||
|
||||
def _format_category(vehicle: Vehicle) -> str:
|
||||
"""Determine mobile.de category from vehicle type and lkw_type."""
|
||||
base = _CATEGORY_MAP.get(vehicle.vehicle_type, "Other")
|
||||
if vehicle.vehicle_type == "lkw" and vehicle.lkw_type:
|
||||
# Try direct match, then strip lkw_ prefix for lookup
|
||||
lkw_key = vehicle.lkw_type
|
||||
if lkw_key in _LKW_TYPE_MAP:
|
||||
return _LKW_TYPE_MAP[lkw_key]
|
||||
stripped = lkw_key.removeprefix("lkw_")
|
||||
if stripped in _LKW_TYPE_MAP:
|
||||
return _LKW_TYPE_MAP[stripped]
|
||||
return base
|
||||
return base
|
||||
|
||||
|
||||
def map_fields(vehicle: Vehicle) -> dict[str, Any]:
|
||||
"""Map a Vehicle model to the mobile.de listing Ad format.
|
||||
|
||||
Converts internal vehicle fields to the mobile.de REST API listing format.
|
||||
See: https://developer.mobile.de/api/seller-listings
|
||||
"""
|
||||
mileage = _format_mileage(vehicle)
|
||||
power = _format_power(vehicle.power_kw, vehicle.power_hp)
|
||||
|
||||
ad: dict[str, Any] = {
|
||||
"vin": vehicle.fin,
|
||||
"make": vehicle.make,
|
||||
"model": vehicle.model,
|
||||
"category": _format_category(vehicle),
|
||||
"price": _format_price(vehicle.price),
|
||||
"availabilityStatus": vehicle.availability,
|
||||
"condition": vehicle.condition,
|
||||
}
|
||||
|
||||
if vehicle.first_registration is not None:
|
||||
ad["firstRegistration"] = _format_first_registration(
|
||||
vehicle.first_registration
|
||||
)
|
||||
|
||||
if mileage is not None:
|
||||
ad["mileage"] = mileage
|
||||
|
||||
if power is not None:
|
||||
ad["power"] = power
|
||||
|
||||
if vehicle.fuel_type is not None:
|
||||
ad["fuelType"] = vehicle.fuel_type
|
||||
|
||||
if vehicle.transmission is not None:
|
||||
ad["transmission"] = vehicle.transmission
|
||||
|
||||
if vehicle.color is not None:
|
||||
ad["color"] = vehicle.color
|
||||
|
||||
if vehicle.year is not None:
|
||||
ad["year"] = vehicle.year
|
||||
|
||||
if vehicle.location is not None:
|
||||
ad["sellerLocation"] = vehicle.location
|
||||
|
||||
if vehicle.machine_type is not None:
|
||||
ad["bodyType"] = vehicle.machine_type
|
||||
elif vehicle.body_type is not None:
|
||||
ad["bodyType"] = vehicle.body_type
|
||||
|
||||
if vehicle.description is not None:
|
||||
ad["description"] = vehicle.description
|
||||
|
||||
return ad
|
||||
Reference in New Issue
Block a user