# /opt/docker/dev/service_finder/backend/app/services/logbook_service.py """ Logbook Service - GPS, OBDII és előfizetési szűrő kezelése. """ import logging from typing import Optional, Tuple, Any from decimal import Decimal from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models import VehicleLogbook from app.models import UserStats from app.models.identity import User from app.models.system import SystemParameter logger = logging.getLogger("Logbook-Service-2.0") class LogbookService: """ Útnyilvántartás kezelése GPS koordinátákkal, OBDII adatokkal és előfizetési szintű jogosultságokkal. """ @staticmethod async def get_system_parameter(db: AsyncSession, key: str, default: Any = None) -> Any: """ Lekéri a rendszerparamétert a system.system_parameters táblából. Elsőként a global scope-ot keresi (scope_level='global', scope_id=NULL). Ha nem talál, visszaadja a default értéket. """ stmt = select(SystemParameter).where( SystemParameter.key == key, SystemParameter.scope_level == 'global', SystemParameter.scope_id.is_(None), SystemParameter.is_active == True ).order_by(SystemParameter.updated_at.desc()) result = await db.execute(stmt) param = result.scalar_one_or_none() if param and 'value' in param.value: return param.value['value'] return default @staticmethod async def get_user_rank(db: AsyncSession, user_id: int) -> int: """ Lekérdezi a felhasználó aktuális rankját (current_level) a UserStats táblából. Ha nincs rekord, alapértelmezett 0 (ingyenes szint). """ stmt = select(UserStats.current_level).where(UserStats.user_id == user_id) result = await db.execute(stmt) rank = result.scalar_one_or_none() return rank if rank is not None else 0 @staticmethod async def check_subscription_guard( db: AsyncSession, user_id: int, wants_gps: bool = False, wants_obd: bool = False ) -> Tuple[bool, str]: """ Ellenőrzi, hogy a felhasználó előfizetési szintje engedélyezi-e a GPS/OBDII adatok rögzítését. Szabályok: - Rank >= LOGBOOK_GPS_MIN_RANK (alapértelmezett 50): engedélyezett a GPS távolság és koordináták - Rank >= 90 (VIP/Admin): minden engedélyezett (GPS, OBDII, gyorsulás) - Rank < LOGBOOK_GPS_MIN_RANK: csak manuális distance_km és trip_type rögzíthető Visszatérés: (allowed: bool, message: str) """ rank = await LogbookService.get_user_rank(db, user_id) gps_min_rank = await LogbookService.get_system_parameter(db, 'LOGBOOK_GPS_MIN_RANK', 50) vip_min_rank = 90 # Fix VIP küszöb if rank >= vip_min_rank: return True, "VIP/Admin szint: minden adat rögzíthető" if rank >= gps_min_rank: if wants_gps or wants_obd: return True, f"PREMIUM szint (rank {rank} >= {gps_min_rank}): GPS és OBDII adatok rögzíthetők" return True, "PREMIUM szint" # Ingyenes felhasználó if wants_gps or wants_obd: return False, f"Ingyenes felhasználók (rank {rank} < {gps_min_rank}) nem rögzíthetnek GPS koordinátákat vagy OBDII adatokat. Csak manuális distance_km és trip_type engedélyezett." return True, "Ingyenes szint: csak manuális adatok" @staticmethod async def create_logbook_entry( db: AsyncSession, asset_id: str, driver_id: int, trip_type: str, start_mileage: int, end_mileage: Optional[int] = None, distance_km: Optional[float] = None, start_lat: Optional[float] = None, start_lng: Optional[float] = None, end_lat: Optional[float] = None, end_lng: Optional[float] = None, gps_calculated_distance: Optional[float] = None, obd_verified: bool = False, max_acceleration: Optional[float] = None, average_speed: Optional[float] = None, ) -> VehicleLogbook: """ Új útnyilvántartás bejegyzés létrehozása előfizetési szűrővel. Automatikusan ellenőrzi, hogy a felhasználó rankja engedélyezi-e a GPS/OBDII mezők kitöltését. Ha nem, a GPS és OBDII mezők null-ra állnak, és csak a manuális distance_km marad. """ # Ellenőrizzük a jogosultságot wants_gps = any([start_lat, start_lng, end_lat, end_lng, gps_calculated_distance]) wants_obd = obd_verified or max_acceleration is not None or average_speed is not None allowed, message = await LogbookService.check_subscription_guard( db, driver_id, wants_gps, wants_obd ) if not allowed: # Ha nem engedélyezett, nullázzuk a tiltott mezőket logger.warning(f"User {driver_id} attempted to log GPS/OBDII without permission. {message}") start_lat = start_lng = end_lat = end_lng = gps_calculated_distance = None obd_verified = False max_acceleration = average_speed = None # Új bejegyzés létrehozása new_entry = VehicleLogbook( asset_id=asset_id, driver_id=driver_id, trip_type=trip_type, start_mileage=start_mileage, end_mileage=end_mileage, distance_km=distance_km, start_lat=start_lat, start_lng=start_lng, end_lat=end_lat, end_lng=end_lng, gps_calculated_distance=gps_calculated_distance, obd_verified=obd_verified, max_acceleration=max_acceleration, average_speed=average_speed, ) db.add(new_entry) await db.commit() await db.refresh(new_entry) logger.info(f"Logbook entry created for asset {asset_id}, driver {driver_id}, trip_type {trip_type}") return new_entry @staticmethod async def calculate_official_distance( start_coords: Tuple[float, float], end_coords: Tuple[float, float] ) -> Optional[float]: """ TODO: OSRM/Google Maps API hívással számolja ki a legrövidebb útvonal távolságát. Egyelőre placeholder, később implementálandó. Visszatérés: távolság kilométerben (float) vagy None, ha nem sikerült. """ # TODO: Integrálni OSRM vagy Google Maps Distance Matrix API-t # Példa: https://project-osrm.org/docs/v5.24.0/api/#route-service # Jelenleg egyszerű haversine formula alapján számolunk from math import radians, sin, cos, sqrt, atan2 lat1, lon1 = start_coords lat2, lon2 = end_coords R = 6371.0 # Föld sugara km-ben lat1_rad = radians(lat1) lon1_rad = radians(lon1) lat2_rad = radians(lat2) lon2_rad = radians(lon2) dlon = lon2_rad - lon1_rad dlat = lat2_rad - lat1_rad a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2 c = 2 * atan2(sqrt(a), sqrt(1 - a)) distance_km = R * c return round(distance_km, 2)