from fastapi import APIRouter, Depends, HTTPException, status from fastapi.encoders import jsonable_encoder from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_telegram_user, log_audit from app.db.session import get_session from app.models.car import ( Car, CarServiceLink, ServiceCenter, ServiceVisit, VehicleAccess, VehicleDataChangeRequest, ) from app.models.user import User from app.schemas.service_center import ( VehicleAccessGrant, VehicleAccessRead, VehicleCreate, VehicleRead, VehicleUpdate, ) from app.schemas.user import UserRead from app.services.odometer import add_odometer_history, validate_odometer_change from app.services.vehicle_identity import normalize_license_plate, validate_vin router = APIRouter(tags=["my"]) @router.get("/me", response_model=UserRead) async def me(current_user: User = Depends(get_current_telegram_user)) -> User: return current_user def vehicle_data(payload: VehicleCreate | VehicleUpdate, *, partial: bool = False) -> dict: raw = payload.model_dump(exclude_unset=partial) data = { key: value for key, value in raw.items() if key not in {"license_plate", "license_plate_country", "vin"} } if "license_plate" in raw: data["license_plate_display"] = raw["license_plate"] data["license_plate_normalized"] = normalize_license_plate(raw["license_plate"]) data["plate_number"] = raw["license_plate"] if "license_plate_country" in raw: data["license_plate_country"] = ( raw["license_plate_country"].upper() if raw["license_plate_country"] else None ) if "vin" in raw: data["vin_normalized"] = validate_vin(raw["vin"]) data["vin"] = raw["vin"] return data @router.get("/my/vehicles", response_model=list[VehicleRead]) async def my_vehicles( session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> list[Car]: result = await session.execute( select(Car) .outerjoin(VehicleAccess, VehicleAccess.vehicle_id == Car.id) .where( or_( Car.owner_id == current_user.id, (VehicleAccess.user_id == current_user.id) & (VehicleAccess.status == "active"), ) ) .order_by(Car.created_at.desc()) ) return list(result.scalars()) @router.post("/my/vehicles", response_model=VehicleRead, status_code=status.HTTP_201_CREATED) async def create_vehicle( payload: VehicleCreate, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> Car: car = Car(**vehicle_data(payload), owner_id=current_user.id) session.add(car) await session.flush() session.add(VehicleAccess(vehicle_id=car.id, user_id=current_user.id, role="owner", status="active")) if car.current_odometer is not None: add_odometer_history( session, car, new_odometer=car.current_odometer, source_record_type="manual", source_record_id=None, changed_by=current_user.id, ) await log_audit(session, actor=current_user, action="vehicle.create", target_type="vehicle", target_id=car.id) await session.commit() await session.refresh(car) return car @router.patch("/my/vehicles/{vehicle_id}", response_model=VehicleRead) async def update_vehicle( vehicle_id: int, payload: VehicleUpdate, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> Car: car = await session.get(Car, vehicle_id) if car is None: raise HTTPException(status_code=404, detail="Vehicle not found") if car.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Forbidden") raw = vehicle_data(payload, partial=True) odometer_value = raw.pop("current_odometer", None) if "current_odometer" in raw else None if odometer_value is not None: validate_odometer_change(car, odometer_value, source_record_type="manual", confirm_lower_odometer=True) for field, value in raw.items(): setattr(car, field, value) if odometer_value is not None and odometer_value != car.current_odometer: add_odometer_history( session, car, new_odometer=odometer_value, source_record_type="manual", source_record_id=None, changed_by=current_user.id, confirmation_required=car.current_odometer is not None and odometer_value < car.current_odometer, user_confirmed=True, ) await log_audit(session, actor=current_user, action="vehicle.update", target_type="vehicle", target_id=car.id) await session.commit() await session.refresh(car) return car @router.get("/my/vehicles/{vehicle_id}/service-history") async def vehicle_service_history( vehicle_id: int, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> dict: car = await session.get(Car, vehicle_id) if car is None: raise HTTPException(status_code=404, detail="Vehicle not found") if car.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Forbidden") result = await session.execute( select(ServiceVisit) .where(ServiceVisit.vehicle_id == vehicle_id) .order_by(ServiceVisit.visit_date.desc()) ) visits = list(result.scalars()) return {"vehicle_id": vehicle_id, "service_visits": jsonable_encoder(visits)} @router.get("/my/confirmations") async def my_confirmations( session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> dict: owner_cars = select(Car.id).where(Car.owner_id == current_user.id) visits = list( ( await session.execute( select(ServiceVisit) .where( ServiceVisit.vehicle_id.in_(owner_cars), ServiceVisit.status == "pending_owner_confirmation", ) .order_by(ServiceVisit.updated_at.desc(), ServiceVisit.id.desc()) ) ).scalars() ) change_requests = list( ( await session.execute( select(VehicleDataChangeRequest) .where( VehicleDataChangeRequest.owner_user_id == current_user.id, VehicleDataChangeRequest.status == "pending", ) .order_by(VehicleDataChangeRequest.created_at.desc()) ) ).scalars() ) links = list( ( await session.execute( select(CarServiceLink) .where( CarServiceLink.car_id.in_(owner_cars), CarServiceLink.status == "pending", CarServiceLink.is_active.is_(False), ) .order_by(CarServiceLink.created_at.desc()) ) ).scalars() ) return { "service_visits": jsonable_encoder(visits), "change_requests": jsonable_encoder(change_requests), "service_links": jsonable_encoder(links), } @router.get("/my/service-links") async def my_service_links( session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> list[dict]: result = await session.execute( select(CarServiceLink, Car, ServiceCenter) .join(Car, Car.id == CarServiceLink.car_id) .join(ServiceCenter, ServiceCenter.id == CarServiceLink.service_center_id) .where(Car.owner_id == current_user.id) .order_by(CarServiceLink.created_at.desc()) ) return [ { "id": link.id, "status": link.status, "access_level": link.access_level, "car_id": car.id, "car_name": car.name, "service_center_id": center.id, "service_center_name": center.display_name or center.name, "created_at": link.created_at, "approved_at": link.approved_at, "revoked_at": link.revoked_at, } for link, car, center in result.all() ] @router.post("/my/vehicles/{vehicle_id}/grant-service-access", response_model=VehicleAccessRead) async def grant_vehicle_access( vehicle_id: int, payload: VehicleAccessGrant, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> VehicleAccess: car = await session.get(Car, vehicle_id) if car is None: raise HTTPException(status_code=404, detail="Vehicle not found") if car.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Forbidden") if not payload.user_id: raise HTTPException(status_code=400, detail="user_id is required for access grants") result = await session.execute( select(VehicleAccess).where( VehicleAccess.vehicle_id == vehicle_id, VehicleAccess.user_id == payload.user_id, VehicleAccess.role == payload.role, ) ) access = result.scalar_one_or_none() if access is None: access = VehicleAccess(vehicle_id=vehicle_id, user_id=payload.user_id, role=payload.role, status="active") session.add(access) else: access.status = "active" access.revoked_at = None await log_audit( session, actor=current_user, action="vehicle_access.grant", target_type="vehicle", target_id=vehicle_id, metadata={"granted_user_id": payload.user_id, "role": payload.role}, ) await session.commit() await session.refresh(access) return access