""" Smart Odometer Service - Adminisztrátor által paraméterezhető kilométeróra becslés. A szolgáltatás a járművek kilométeróra állását becsüli a költségbejegyzések alapján, figyelembe véve a rendszerparamétereket (ODOMETER_MIN_DAYS_FOR_AVG, ODOMETER_CONFIDENCE_THRESHOLD). Ha az admin beállított manuális átlagot (manual_override_avg), akkor azt használja. """ from datetime import datetime, timedelta from typing import Optional, Tuple from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from sqlalchemy.orm import selectinload from app.models.vehicle import VehicleOdometerState, VehicleCost from app.models.system import SystemParameter from app.models import VehicleModelDefinition class OdometerService: """Kilométeróra becslési szolgáltatás adminisztrációs kontrollal.""" @staticmethod async def get_system_param(db: AsyncSession, key: str, default_value): """Rendszerparaméter lekérése a system.system_parameters táblából.""" stmt = select(SystemParameter).where( SystemParameter.key == key, SystemParameter.scope_level == 'global', SystemParameter.is_active == True ) result = await db.execute(stmt) param = result.scalars().first() if param and 'value' in param.value: return param.value['value'] return default_value @staticmethod async def update_vehicle_stats(db: AsyncSession, vehicle_id: int) -> Optional[VehicleOdometerState]: """ Frissíti a jármű kilométeróra statisztikáit. Algoritmus: 1. Ha van manual_override_avg, használja azt. 2. Különben számol átlagot a vehicle.costs bejegyzésekből. 3. Figyelembe veszi az ODOMETER_MIN_DAYS_FOR_AVG paramétert. 4. Kiszámolja a confidence_score-t a minták száma alapján. 5. Frissíti vagy létrehozza a VehicleOdometerState rekordot. """ # Rendszerparaméterek lekérése min_days = await OdometerService.get_system_param(db, 'ODOMETER_MIN_DAYS_FOR_AVG', 7) confidence_threshold = await OdometerService.get_system_param(db, 'ODOMETER_CONFIDENCE_THRESHOLD', 0.5) # Meglévő állapot lekérése stmt = select(VehicleOdometerState).where(VehicleOdometerState.vehicle_id == vehicle_id) result = await db.execute(stmt) odometer_state = result.scalars().first() # Költségbejegyzések lekérése dátum és odometer szerint rendezve cost_stmt = select(VehicleCost).where( VehicleCost.vehicle_id == vehicle_id, VehicleCost.odometer.isnot(None) ).order_by(VehicleCost.date.asc()) cost_result = await db.execute(cost_stmt) costs = cost_result.scalars().all() if not costs: # Nincs adat, alapértelmezett értékek if odometer_state: odometer_state.daily_avg_distance = 0 odometer_state.confidence_score = 0 odometer_state.estimated_current_odometer = odometer_state.last_recorded_odometer else: # Jármű alapadatok lekérése vehicle_stmt = select(VehicleModelDefinition).where(VehicleModelDefinition.id == vehicle_id) vehicle_result = await db.execute(vehicle_stmt) vehicle = vehicle_result.scalars().first() if not vehicle: return None odometer_state = VehicleOdometerState( vehicle_id=vehicle_id, last_recorded_odometer=0, last_recorded_date=datetime.now(), daily_avg_distance=0, estimated_current_odometer=0, confidence_score=0, manual_override_avg=None ) db.add(odometer_state) await db.commit() await db.refresh(odometer_state) return odometer_state # Utolsó rögzített adatok last_cost = costs[-1] last_recorded_odometer = last_cost.odometer last_recorded_date = last_cost.date # Manuális átlag ellenőrzése if odometer_state and odometer_state.manual_override_avg is not None: daily_avg = float(odometer_state.manual_override_avg) confidence = 1.0 # Manuális beállítás esetén teljes bizalom else: # Átlag számítása a költségbejegyzésekből valid_pairs = [] for i in range(1, len(costs)): prev = costs[i-1] curr = costs[i] days_diff = (curr.date - prev.date).days km_diff = curr.odometer - prev.odometer if days_diff >= min_days and km_diff > 0: daily_avg = km_diff / days_diff valid_pairs.append((daily_avg, days_diff)) if valid_pairs: # Súlyozott átlag (hosszabb időszakok nagyobb súllyal) total_weighted = sum(avg * weight for avg, weight in valid_pairs) total_days = sum(weight for _, weight in valid_pairs) daily_avg = total_weighted / total_days if total_days > 0 else 0 # Confidence score: érvényes párok száma / összes lehetséges párok confidence = min(len(valid_pairs) / max(len(costs) - 1, 1), 1.0) else: daily_avg = 0 confidence = 0 # Becsült jelenlegi kilométer days_since_last = (datetime.now(last_recorded_date.tzinfo) - last_recorded_date).days estimated_odometer = last_recorded_odometer + (daily_avg * max(days_since_last, 0)) # Állapot frissítése vagy létrehozása if odometer_state: odometer_state.last_recorded_odometer = last_recorded_odometer odometer_state.last_recorded_date = last_recorded_date odometer_state.daily_avg_distance = daily_avg odometer_state.estimated_current_odometer = estimated_odometer odometer_state.confidence_score = confidence else: odometer_state = VehicleOdometerState( vehicle_id=vehicle_id, last_recorded_odometer=last_recorded_odometer, last_recorded_date=last_recorded_date, daily_avg_distance=daily_avg, estimated_current_odometer=estimated_odometer, confidence_score=confidence, manual_override_avg=None ) db.add(odometer_state) await db.commit() await db.refresh(odometer_state) return odometer_state @staticmethod async def get_estimated_odometer(db: AsyncSession, vehicle_id: int) -> Tuple[Optional[float], float]: """ Visszaadja a jármű becsült jelenlegi kilométeróra állását és a bizalom pontszámot. Returns: Tuple[estimated_odometer, confidence_score] """ stmt = select(VehicleOdometerState).where(VehicleOdometerState.vehicle_id == vehicle_id) result = await db.execute(stmt) odometer_state = result.scalars().first() if not odometer_state: # Ha nincs állapot, frissítsük odometer_state = await OdometerService.update_vehicle_stats(db, vehicle_id) if not odometer_state: return None, 0.0 return odometer_state.estimated_current_odometer, odometer_state.confidence_score @staticmethod async def set_manual_override(db: AsyncSession, vehicle_id: int, daily_avg: Optional[float]) -> Optional[VehicleOdometerState]: """ Adminisztrátori manuális átlag beállítása. Args: daily_avg: Napi átlagos kilométer (km/nap). Ha None, törli a manuális beállítást. """ stmt = select(VehicleOdometerState).where(VehicleOdometerState.vehicle_id == vehicle_id) result = await db.execute(stmt) odometer_state = result.scalars().first() if not odometer_state: # Ha nincs állapot, hozzuk létre odometer_state = VehicleOdometerState( vehicle_id=vehicle_id, last_recorded_odometer=0, last_recorded_date=datetime.now(), daily_avg_distance=0, estimated_current_odometer=0, confidence_score=0, manual_override_avg=daily_avg ) db.add(odometer_state) else: odometer_state.manual_override_avg = daily_avg # Frissítsük a becslést a manuális átlaggal if daily_avg is not None: days_since_last = (datetime.now(odometer_state.last_recorded_date.tzinfo) - odometer_state.last_recorded_date).days odometer_state.estimated_current_odometer = odometer_state.last_recorded_odometer + (daily_avg * max(days_since_last, 0)) odometer_state.confidence_score = 1.0 await db.commit() await db.refresh(odometer_state) return odometer_state