2026-07-17 09:31:28 +02:00
|
|
|
"""Price comparison service: search comparable listings and calculate average price."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import random
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.models.vehicle import Vehicle
|
|
|
|
|
from app.schemas.retouch import ComparableListing, PriceCompareResponse
|
|
|
|
|
from app.services.vehicle_service import get_vehicle_by_id
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_mock_listings(vehicle: Vehicle) -> list[ComparableListing]:
|
|
|
|
|
"""Generate mock mobile.de comparable listings based on vehicle data.
|
|
|
|
|
|
|
|
|
|
In production, this would call the mobile.de Search API.
|
|
|
|
|
For now, we generate realistic mock data based on the vehicle's
|
|
|
|
|
make, model, year, and price.
|
|
|
|
|
"""
|
|
|
|
|
listings: list[ComparableListing] = []
|
|
|
|
|
base_price = float(vehicle.price) if vehicle.price else 25000.0
|
|
|
|
|
|
|
|
|
|
# Generate 3-7 comparable listings with price variation
|
|
|
|
|
num_listings = random.randint(3, 7)
|
|
|
|
|
|
|
|
|
|
for i in range(num_listings):
|
|
|
|
|
# Vary price by +/- 15%
|
|
|
|
|
price_variation = random.uniform(-0.15, 0.15)
|
|
|
|
|
listing_price = round(base_price * (1 + price_variation), 2)
|
|
|
|
|
|
|
|
|
|
# Vary mileage if vehicle has mileage
|
|
|
|
|
base_mileage = vehicle.mileage_km or 100000
|
|
|
|
|
mileage_variation = random.randint(-20000, 20000)
|
|
|
|
|
listing_mileage = max(0, base_mileage + mileage_variation)
|
|
|
|
|
|
|
|
|
|
locations = ["Berlin", "Hamburg", "München", "Köln", "Frankfurt", "Stuttgart"]
|
|
|
|
|
|
|
|
|
|
listing = ComparableListing(
|
|
|
|
|
title=f"{vehicle.make} {vehicle.model} {vehicle.year or ''}".strip(),
|
|
|
|
|
make=vehicle.make,
|
|
|
|
|
model=vehicle.model,
|
|
|
|
|
year=vehicle.year,
|
|
|
|
|
price=listing_price,
|
|
|
|
|
mileage_km=listing_mileage,
|
|
|
|
|
location=random.choice(locations),
|
|
|
|
|
url=f"https://suchen.mobile.de/fahrzeuge/details.html?id={random.randint(1000000, 9999999)}",
|
|
|
|
|
source="mobile.de",
|
|
|
|
|
)
|
|
|
|
|
listings.append(listing)
|
|
|
|
|
|
|
|
|
|
return listings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def compare_prices(
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
vehicle_id: uuid.UUID,
|
|
|
|
|
) -> PriceCompareResponse:
|
|
|
|
|
"""Compare prices for a vehicle by searching comparable listings.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db: Async database session
|
|
|
|
|
vehicle_id: UUID of the vehicle to compare
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
PriceCompareResponse with comparable listings and average price.
|
|
|
|
|
If vehicle not found, raises ValueError.
|
|
|
|
|
If no comparable listings found, returns empty list with null average.
|
|
|
|
|
"""
|
|
|
|
|
vehicle = await get_vehicle_by_id(db, vehicle_id)
|
|
|
|
|
if vehicle is None:
|
|
|
|
|
raise ValueError(f"Vehicle {vehicle_id} not found")
|
|
|
|
|
|
|
|
|
|
# Search for comparable listings
|
|
|
|
|
# In production: call mobile.de Search API with make, model, year filters
|
|
|
|
|
# For now: generate mock listings
|
|
|
|
|
listings = _generate_mock_listings(vehicle)
|
|
|
|
|
|
|
|
|
|
# Calculate average price
|
|
|
|
|
if listings:
|
2026-07-17 21:16:38 +02:00
|
|
|
average_price = round(sum(listing.price for listing in listings) / len(listings), 2)
|
2026-07-17 09:31:28 +02:00
|
|
|
else:
|
|
|
|
|
average_price = None
|
|
|
|
|
|
|
|
|
|
return PriceCompareResponse(
|
|
|
|
|
vehicle_id=vehicle_id,
|
|
|
|
|
comparable_listings=listings,
|
|
|
|
|
average_price=average_price,
|
|
|
|
|
listing_count=len(listings),
|
|
|
|
|
)
|