"""Vehicle service: CRUD, filtering, sorting, and soft-delete operations.""" from __future__ import annotations import uuid from datetime import datetime, timezone from typing import Any from sqlalchemy import and_, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.models.vehicle import Vehicle # Fields that are safe to sort by _SORTABLE_FIELDS: set[str] = { "make", "model", "fin", "year", "price", "vehicle_type", "availability", "condition", "created_at", "updated_at", "power_kw", "mileage_km", } def _apply_filters( stmt: select, vehicle_type: str | None = None, availability: str | None = None, min_price: float | None = None, max_price: float | None = None, search: str | None = None, ) -> select: """Apply WHERE filters to a select statement (always excludes soft-deleted).""" conditions = [Vehicle.deleted_at.is_(None)] if vehicle_type: conditions.append(Vehicle.vehicle_type == vehicle_type) if availability: conditions.append(Vehicle.availability == availability) if min_price is not None: conditions.append(Vehicle.price >= min_price) if max_price is not None: conditions.append(Vehicle.price <= max_price) if search: search_pattern = f"%{search}%" conditions.append( or_( Vehicle.make.ilike(search_pattern), Vehicle.model.ilike(search_pattern), Vehicle.fin.ilike(search_pattern), Vehicle.location.ilike(search_pattern), ) ) return stmt.where(and_(*conditions)) def _apply_sort(stmt: select, sort: str | None = None) -> select: """Apply ORDER BY to a select statement based on sort param. Format: 'field' for ascending, '-field' for descending. """ if not sort: return stmt.order_by(Vehicle.created_at.desc()) descending = sort.startswith("-") field_name = sort.lstrip("-") if field_name not in _SORTABLE_FIELDS: return stmt.order_by(Vehicle.created_at.desc()) column = getattr(Vehicle, field_name) if descending: return stmt.order_by(column.desc()) return stmt.order_by(column.asc()) async def list_vehicles( db: AsyncSession, page: int = 1, page_size: int = 20, vehicle_type: str | None = None, availability: str | None = None, min_price: float | None = None, max_price: float | None = None, search: str | None = None, sort: str | None = None, ) -> tuple[list[Vehicle], int]: """List vehicles with pagination, filtering, and sorting. Returns (vehicles, total_count). """ # Build count query count_stmt = select(func.count(Vehicle.id)) count_stmt = _apply_filters( count_stmt, vehicle_type=vehicle_type, availability=availability, min_price=min_price, max_price=max_price, search=search, ) total_result = await db.execute(count_stmt) total = total_result.scalar_one() # Build data query data_stmt = select(Vehicle) data_stmt = _apply_filters( data_stmt, vehicle_type=vehicle_type, availability=availability, min_price=min_price, max_price=max_price, search=search, ) data_stmt = _apply_sort(data_stmt, sort) offset = (page - 1) * page_size data_stmt = data_stmt.offset(offset).limit(page_size) result = await db.execute(data_stmt) vehicles = list(result.scalars().all()) return vehicles, total async def get_vehicle_by_id( db: AsyncSession, vehicle_id: uuid.UUID ) -> Vehicle | None: """Get a single vehicle by ID, excluding soft-deleted.""" stmt = select(Vehicle).where( and_(Vehicle.id == vehicle_id, Vehicle.deleted_at.is_(None)) ) result = await db.execute(stmt) return result.scalar_one_or_none() async def get_vehicle_by_fin( db: AsyncSession, fin: str ) -> Vehicle | None: """Get a single vehicle by FIN, excluding soft-deleted.""" stmt = select(Vehicle).where( and_(Vehicle.fin == fin, Vehicle.deleted_at.is_(None)) ) result = await db.execute(stmt) return result.scalar_one_or_none() async def create_vehicle( db: AsyncSession, data: dict[str, Any] ) -> Vehicle: """Create a new vehicle. Raises ValueError if FIN already exists. """ existing = await get_vehicle_by_fin(db, data["fin"]) if existing is not None: raise ValueError(f"Vehicle with FIN '{data['fin']}' already exists") vehicle = Vehicle(**data) db.add(vehicle) await db.flush() await db.refresh(vehicle) return vehicle async def update_vehicle( db: AsyncSession, vehicle_id: uuid.UUID, updates: dict[str, Any] ) -> Vehicle | None: """Update a vehicle's fields. Returns None if not found or deleted.""" vehicle = await get_vehicle_by_id(db, vehicle_id) if vehicle is None: return None # If FIN is being updated, check for duplicates if "fin" in updates and updates["fin"] != vehicle.fin: existing = await get_vehicle_by_fin(db, updates["fin"]) if existing is not None: raise ValueError(f"Vehicle with FIN '{updates['fin']}' already exists") for key, value in updates.items(): if hasattr(vehicle, key): setattr(vehicle, key, value) await db.flush() await db.refresh(vehicle) return vehicle async def soft_delete_vehicle( db: AsyncSession, vehicle_id: uuid.UUID ) -> Vehicle | None: """Soft-delete a vehicle by setting deleted_at. Returns None if not found.""" vehicle = await get_vehicle_by_id(db, vehicle_id) if vehicle is None: return None vehicle.deleted_at = datetime.now(timezone.utc) await db.flush() await db.refresh(vehicle) return vehicle