# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py import uuid import logging from typing import Any, Dict, List from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from sqlalchemy.orm import selectinload from app.db.session import get_db from app.api.deps import get_current_user from app.models import Asset, AssetCost from app.models.identity import User from app.services.cost_service import cost_service from app.services.asset_service import AssetService from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse from app.schemas.asset import AssetResponse, AssetCreate router = APIRouter() @router.get("/vehicles", response_model=List[AssetResponse]) async def get_user_vehicles( skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Get all vehicles/assets belonging to the current user or their organization. This endpoint returns a paginated list of vehicles that the authenticated user has access to (either as owner or through organization membership). """ # Query assets where user is owner or organization member from sqlalchemy import or_ # First, get user's organization memberships from app.models.marketplace.organization import OrganizationMember org_stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ) org_result = await db.execute(org_stmt) user_org_ids = [row[0] for row in org_result.all()] # Build query: assets owned by user OR assets in user's organizations stmt = ( select(Asset) .where( or_( Asset.owner_person_id == current_user.id, Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False, Asset.operator_person_id == current_user.id, Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False ) ) .order_by(Asset.created_at.desc()) .offset(skip) .limit(limit) .options(selectinload(Asset.catalog)) ) result = await db.execute(stmt) assets = result.scalars().all() return assets @router.get("/{asset_id}/financial-summary", response_model=Dict[str, Any]) async def get_asset_financial_report( asset_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ MB 2.0 Dinamikus Pénzügyi Riport. Visszaadja a kategóriákra bontott és az összesített költségeket (Local/EUR). """ # 1. Jogosultság ellenőrzése (Csak a tulajdonos vagy admin láthatja) # (Itt egy gyors check, hogy az asset az övé-e) try: return await cost_service.get_asset_financial_summary(db, asset_id) except Exception as e: raise HTTPException(status_code=500, detail="Hiba a riport generálásakor") @router.get("/{asset_id}/costs", response_model=List[AssetCostResponse]) async def list_asset_costs( asset_id: uuid.UUID, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Tételes költséglista lapozással (Pagination).""" stmt = ( select(AssetCost) .where(AssetCost.asset_id == asset_id) .order_by(desc(AssetCost.date)) .offset(skip) .limit(limit) ) res = await db.execute(stmt) return res.scalars().all() @router.get("/{asset_id}", response_model=AssetResponse) async def get_asset( asset_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Get detailed information about a specific vehicle/asset. Returns the asset's full technical profile including catalog data and vehicle model definition specifications. """ # Check if user has access to this asset from sqlalchemy import or_ from app.models.marketplace.organization import OrganizationMember # Get user's organization memberships org_stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ) org_result = await db.execute(org_stmt) user_org_ids = [row[0] for row in org_result.all()] # Query asset with catalog and master definition stmt = ( select(Asset) .where( Asset.id == asset_id, or_( Asset.owner_person_id == current_user.id, Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False, Asset.operator_person_id == current_user.id, Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False ) ) .options( selectinload(Asset.catalog).selectinload(AssetCatalog.master_definition) ) ) result = await db.execute(stmt) asset = result.scalar_one_or_none() if not asset: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Asset not found or you don't have permission to access it" ) return asset @router.post("/vehicles", response_model=AssetResponse, status_code=status.HTTP_201_CREATED) async def create_or_claim_vehicle( payload: AssetCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Új jármű hozzáadása vagy meglévő jármű igénylése a flottához. A végpont a következőket végzi: - Ellenőrzi a felhasználó járműlimitjét - Ha a VIN már létezik, tulajdonjog-átvitelt kezdeményez - Ha új, létrehozza a járművet és a kapcsolódó digitális ikreket - XP jutalom adása a felhasználónak """ try: # Determine organization ID: use provided or default to user's first organization org_id = payload.organization_id if org_id is None: # Get user's organization memberships from app.models.marketplace.organization import OrganizationMember org_stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ).limit(1) org_result = await db.execute(org_stmt) user_org = org_result.scalar_one_or_none() if user_org is None: # User has no organization - create a personal organization or use default # For now, raise an error (in future, we could create a personal org) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No organization found for user. Please specify an organization_id or join/create an organization first." ) org_id = user_org asset = await AssetService.create_or_claim_vehicle( db=db, user_id=current_user.id, org_id=org_id, vin=payload.vin, license_plate=payload.license_plate, catalog_id=payload.catalog_id ) return asset except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except HTTPException: raise except Exception as e: logger = logging.getLogger(__name__) logger.error(f"Vehicle creation error: {e}") raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű létrehozásakor") @router.get("/{asset_id}/maintenance", response_model=List[AssetCostResponse]) async def list_maintenance_records( asset_id: uuid.UUID, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ List maintenance records for a specific vehicle. Returns paginated list of maintenance costs with cost_category = 'maintenance'. """ # Check if user has access to this asset from sqlalchemy import or_ from app.models.marketplace.organization import OrganizationMember # Get user's organization memberships org_stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ) org_result = await db.execute(org_stmt) user_org_ids = [row[0] for row in org_result.all()] # Check asset access asset_stmt = select(Asset).where( Asset.id == asset_id, or_( Asset.owner_person_id == current_user.id, Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False, Asset.operator_person_id == current_user.id, Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False ) ) asset_result = await db.execute(asset_stmt) asset = asset_result.scalar_one_or_none() if not asset: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Asset not found or you don't have permission to access it" ) # Query maintenance costs stmt = ( select(AssetCost) .where( AssetCost.asset_id == asset_id, AssetCost.cost_category == "maintenance" ) .order_by(desc(AssetCost.date)) .offset(skip) .limit(limit) ) res = await db.execute(stmt) return res.scalars().all() @router.post("/{asset_id}/maintenance", response_model=AssetCostResponse, status_code=status.HTTP_201_CREATED) async def create_maintenance_record( asset_id: uuid.UUID, payload: Dict[str, Any], db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Add a maintenance record for a vehicle. Expected payload fields: - date: ISO datetime string (required) - odometer: integer (optional, current mileage) - description: string (required) - cost: float (required, net amount) - currency: string (optional, default: "EUR") - invoice_number: string (optional) """ # Check if user has access to this asset from sqlalchemy import or_ from app.models.marketplace.organization import OrganizationMember # Get user's organization memberships org_stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ) org_result = await db.execute(org_stmt) user_org_ids = [row[0] for row in org_result.all()] # Check asset access and get asset asset_stmt = select(Asset).where( Asset.id == asset_id, or_( Asset.owner_person_id == current_user.id, Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False, Asset.operator_person_id == current_user.id, Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False ) ) asset_result = await db.execute(asset_stmt) asset = asset_result.scalar_one_or_none() if not asset: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Asset not found or you don't have permission to access it" ) # Validate required fields required_fields = ["date", "description", "cost"] for field in required_fields: if field not in payload: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Missing required field: {field}" ) try: # Parse date from datetime import datetime date = datetime.fromisoformat(payload["date"].replace("Z", "+00:00")) # Determine organization ID: use asset's current org, owner org, or user's active organization organization_id = asset.current_organization_id or asset.owner_org_id if not organization_id: # Get user's active organization from their scope_id if current_user.scope_id: try: organization_id = int(current_user.scope_id) except (ValueError, TypeError): # If scope_id is not a valid integer, try to get from organization memberships from sqlalchemy import select from app.models.marketplace.organization import OrganizationMember stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ).limit(1) result = await db.execute(stmt) org_row = result.first() organization_id = org_row[0] if org_row else None else: # Try to get from organization memberships from sqlalchemy import select from app.models.marketplace.organization import OrganizationMember stmt = select(OrganizationMember.organization_id).where( OrganizationMember.user_id == current_user.id ).limit(1) result = await db.execute(stmt) org_row = result.first() organization_id = org_row[0] if org_row else None if not organization_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot determine organization for this cost record. Please ensure you have an active organization." ) # Create AssetCost record maintenance_cost = AssetCost( asset_id=asset_id, organization_id=organization_id, cost_category="maintenance", amount_net=float(payload["cost"]), currency=payload.get("currency", "EUR"), date=date, invoice_number=payload.get("invoice_number"), data={ "odometer": payload.get("odometer"), "description": payload["description"], "type": "maintenance" } ) db.add(maintenance_cost) await db.commit() await db.refresh(maintenance_cost) # Also create an AssetEvent for the maintenance from app.models.vehicle import AssetEvent maintenance_event = AssetEvent( asset_id=asset_id, event_type="maintenance" ) db.add(maintenance_event) await db.commit() return maintenance_cost except ValueError as e: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid data format: {str(e)}" ) except Exception as e: await db.rollback() logger = logging.getLogger(__name__) logger.error(f"Maintenance record creation error: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error while creating maintenance record" )