wrongmove/api/poi_routes.py

238 lines
7.1 KiB
Python
Raw Normal View History

import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from api.auth import User, get_current_user
from database import engine
from models.listing import ListingType
from repositories.poi_repository import POIRepository
from repositories.user_repository import UserRepository
from services import poi_service, task_service
logger = logging.getLogger("uvicorn")
poi_router = APIRouter(prefix="/api/poi", tags=["poi"])
class CreatePOIRequest(BaseModel):
name: str = Field(max_length=200)
address: str = Field(max_length=500)
latitude: float = Field(ge=-90, le=90)
longitude: float = Field(ge=-180, le=180)
class UpdatePOIRequest(BaseModel):
name: str | None = Field(default=None, max_length=200)
address: str | None = Field(default=None, max_length=500)
latitude: float | None = Field(default=None, ge=-90, le=90)
longitude: float | None = Field(default=None, ge=-180, le=180)
class POIResponse(BaseModel):
id: int
name: str
address: str
latitude: float
longitude: float
created_at: str
class CalculateRequest(BaseModel):
travel_modes: list[str] # WALK, BICYCLE, TRANSIT
listing_type: ListingType = ListingType.RENT
listing_ids: list[int] | None = None
class POIDistanceResponse(BaseModel):
poi_id: int
poi_name: str
travel_mode: str
duration_seconds: int
distance_meters: int
def _get_user_id(user: User) -> int:
"""Resolve auth User to database user ID."""
user_repo = UserRepository(engine)
db_user = user_repo.get_user_by_email(user.email)
if db_user is None:
# Auto-create user on first POI interaction
db_user = user_repo.create_user(user.email)
if db_user.id is None:
raise HTTPException(status_code=500, detail="Failed to create user")
return db_user.id
def _poi_to_response(poi: "poi_service.PointOfInterest") -> POIResponse:
return POIResponse(
id=poi.id, # type: ignore[arg-type]
name=poi.name,
address=poi.address,
latitude=poi.latitude,
longitude=poi.longitude,
created_at=poi.created_at.isoformat(),
)
@poi_router.get("", response_model=list[POIResponse])
async def list_pois(
user: Annotated[User, Depends(get_current_user)],
) -> list[POIResponse]:
"""List all POIs for the current user."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
pois = poi_service.get_user_pois(repo, user_id)
return [_poi_to_response(p) for p in pois]
@poi_router.post("", response_model=POIResponse)
async def create_poi(
user: Annotated[User, Depends(get_current_user)],
body: CreatePOIRequest,
) -> POIResponse:
"""Create a new POI."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
result = poi_service.create_poi(
repo,
user_id=user_id,
name=body.name,
address=body.address,
latitude=body.latitude,
longitude=body.longitude,
)
return _poi_to_response(result.poi)
@poi_router.put("/{poi_id}", response_model=POIResponse)
async def update_poi(
user: Annotated[User, Depends(get_current_user)],
poi_id: int,
body: UpdatePOIRequest,
) -> POIResponse:
"""Update an existing POI."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
result = poi_service.update_poi(
repo,
poi_id=poi_id,
user_id=user_id,
name=body.name,
address=body.address,
latitude=body.latitude,
longitude=body.longitude,
)
if result is None:
raise HTTPException(status_code=404, detail="POI not found")
return _poi_to_response(result.poi)
@poi_router.delete("/{poi_id}")
async def delete_poi(
user: Annotated[User, Depends(get_current_user)],
poi_id: int,
) -> dict[str, bool]:
"""Delete a POI and its associated distances."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
deleted = poi_service.delete_poi(repo, poi_id, user_id)
if not deleted:
raise HTTPException(status_code=404, detail="POI not found")
return {"success": True}
@poi_router.post("/{poi_id}/calculate")
async def calculate_distances(
user: Annotated[User, Depends(get_current_user)],
poi_id: int,
body: CalculateRequest,
) -> dict[str, str]:
"""Trigger distance calculation for a POI."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
# Verify POI exists and belongs to user
poi = poi_service.get_poi(repo, poi_id)
if poi is None or poi.user_id != user_id:
raise HTTPException(status_code=404, detail="POI not found")
result = poi_service.trigger_calculation(
poi_id=poi_id,
travel_modes=body.travel_modes,
listing_type=body.listing_type,
user_email=user.email,
listing_ids=body.listing_ids,
)
if result.task_id:
task_service.add_task_for_user(user.email, result.task_id)
return {"task_id": result.task_id or "", "message": result.message}
@poi_router.get("/distances/bulk")
async def get_bulk_distances(
user: Annotated[User, Depends(get_current_user)],
listing_type: ListingType = ListingType.RENT,
) -> dict[int, list[POIDistanceResponse]]:
"""Get all POI distances for the current user, keyed by listing ID."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
pois = {p.id: p for p in poi_service.get_user_pois(repo, user_id)}
if not pois:
return {}
from repositories.listing_repository import ListingRepository
from database import engine as db_engine
listing_repo = ListingRepository(db_engine)
all_ids = list(listing_repo.get_listing_ids(listing_type))
if not all_ids:
return {}
distances = repo.get_distances_for_listings(all_ids, listing_type, user_id)
result: dict[int, list[POIDistanceResponse]] = {}
for d in distances:
poi_name = pois[d.poi_id].name if d.poi_id in pois else "Unknown"
result.setdefault(d.listing_id, []).append(
POIDistanceResponse(
poi_id=d.poi_id,
poi_name=poi_name,
travel_mode=d.travel_mode,
duration_seconds=d.duration_seconds,
distance_meters=d.distance_meters,
)
)
return result
@poi_router.get("/distances")
async def get_distances(
user: Annotated[User, Depends(get_current_user)],
listing_id: int,
listing_type: ListingType = ListingType.RENT,
) -> list[POIDistanceResponse]:
"""Get POI distances for a specific listing."""
user_id = _get_user_id(user)
repo = POIRepository(engine)
poi_repo_pois = {
p.id: p for p in poi_service.get_user_pois(repo, user_id)
}
distances = poi_service.get_distances_for_listing(
repo, listing_id, listing_type, user_id
)
return [
POIDistanceResponse(
poi_id=d.poi_id,
poi_name=poi_repo_pois[d.poi_id].name if d.poi_id in poi_repo_pois else "Unknown",
travel_mode=d.travel_mode,
duration_seconds=d.duration_seconds,
distance_meters=d.distance_meters,
)
for d in distances
]