""" POI Refresh Jobs API Endpoints REST API for managing POI refresh background jobs. """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from typing import List, Optional from datetime import datetime, timezone from pydantic import BaseModel, Field import structlog import uuid from app.core.database import get_db from app.services.poi_refresh_service import POIRefreshService from app.services.poi_scheduler import get_scheduler from app.models.poi_refresh_job import POIRefreshJob logger = structlog.get_logger() router = APIRouter(prefix="/poi-refresh-jobs", tags=["POI Refresh Jobs"]) # Response Models class POIRefreshJobResponse(BaseModel): """POI refresh job response""" id: str tenant_id: str status: str scheduled_at: datetime started_at: Optional[datetime] = None completed_at: Optional[datetime] = None attempt_count: int max_attempts: int pois_detected: Optional[int] = None changes_detected: bool = False change_summary: Optional[dict] = None error_message: Optional[str] = None next_scheduled_at: Optional[datetime] = None duration_seconds: Optional[float] = None is_overdue: bool can_retry: bool class Config: from_attributes = True class ScheduleJobRequest(BaseModel): """Schedule POI refresh job request""" tenant_id: str = Field(..., description="Tenant UUID") latitude: float = Field(..., ge=-90, le=90, description="Bakery latitude") longitude: float = Field(..., ge=-180, le=180, description="Bakery longitude") scheduled_at: Optional[datetime] = Field(None, description="When to run (default: 180 days from now)") class JobExecutionResult(BaseModel): """Job execution result""" status: str job_id: str message: Optional[str] = None pois_detected: Optional[int] = None changes_detected: Optional[bool] = None change_summary: Optional[dict] = None duration_seconds: Optional[float] = None next_scheduled_at: Optional[str] = None error: Optional[str] = None attempt: Optional[int] = None can_retry: Optional[bool] = None # Endpoints @router.post( "/schedule", response_model=POIRefreshJobResponse, summary="Schedule POI refresh job", description="Schedule a background job to refresh POI context for a tenant" ) async def schedule_refresh_job( request: ScheduleJobRequest, db: AsyncSession = Depends(get_db) ): """ Schedule a POI refresh job for a tenant. Creates a background job that will detect POIs for the tenant's location at the scheduled time. Default schedule is 180 days from now. """ try: tenant_uuid = uuid.UUID(request.tenant_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid tenant_id format") try: poi_refresh_service = POIRefreshService() job = await poi_refresh_service.schedule_refresh_job( tenant_id=request.tenant_id, latitude=request.latitude, longitude=request.longitude, scheduled_at=request.scheduled_at, session=db ) logger.info( "POI refresh job scheduled via API", tenant_id=request.tenant_id, job_id=str(job.id), scheduled_at=job.scheduled_at ) return POIRefreshJobResponse( id=str(job.id), tenant_id=str(job.tenant_id), status=job.status, scheduled_at=job.scheduled_at, started_at=job.started_at, completed_at=job.completed_at, attempt_count=job.attempt_count, max_attempts=job.max_attempts, pois_detected=job.pois_detected, changes_detected=job.changes_detected, change_summary=job.change_summary, error_message=job.error_message, next_scheduled_at=job.next_scheduled_at, duration_seconds=job.duration_seconds, is_overdue=job.is_overdue, can_retry=job.can_retry ) except Exception as e: logger.error( "Failed to schedule POI refresh job", tenant_id=request.tenant_id, error=str(e), exc_info=True ) raise HTTPException( status_code=500, detail=f"Failed to schedule refresh job: {str(e)}" ) @router.get( "/{job_id}", response_model=POIRefreshJobResponse, summary="Get refresh job by ID", description="Retrieve details of a specific POI refresh job" ) async def get_refresh_job( job_id: str, db: AsyncSession = Depends(get_db) ): """Get POI refresh job by ID""" try: job_uuid = uuid.UUID(job_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid job_id format") result = await db.execute( select(POIRefreshJob).where(POIRefreshJob.id == job_uuid) ) job = result.scalar_one_or_none() if not job: raise HTTPException(status_code=404, detail=f"Job not found: {job_id}") return POIRefreshJobResponse( id=str(job.id), tenant_id=str(job.tenant_id), status=job.status, scheduled_at=job.scheduled_at, started_at=job.started_at, completed_at=job.completed_at, attempt_count=job.attempt_count, max_attempts=job.max_attempts, pois_detected=job.pois_detected, changes_detected=job.changes_detected, change_summary=job.change_summary, error_message=job.error_message, next_scheduled_at=job.next_scheduled_at, duration_seconds=job.duration_seconds, is_overdue=job.is_overdue, can_retry=job.can_retry ) @router.get( "/tenant/{tenant_id}", response_model=List[POIRefreshJobResponse], summary="Get refresh jobs for tenant", description="Retrieve all POI refresh jobs for a specific tenant" ) async def get_tenant_refresh_jobs( tenant_id: str, status: Optional[str] = Query(None, description="Filter by status"), limit: int = Query(50, ge=1, le=200, description="Maximum number of results"), db: AsyncSession = Depends(get_db) ): """Get all POI refresh jobs for a tenant""" try: tenant_uuid = uuid.UUID(tenant_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid tenant_id format") query = select(POIRefreshJob).where(POIRefreshJob.tenant_id == tenant_uuid) if status: query = query.where(POIRefreshJob.status == status) query = query.order_by(desc(POIRefreshJob.scheduled_at)).limit(limit) result = await db.execute(query) jobs = result.scalars().all() return [ POIRefreshJobResponse( id=str(job.id), tenant_id=str(job.tenant_id), status=job.status, scheduled_at=job.scheduled_at, started_at=job.started_at, completed_at=job.completed_at, attempt_count=job.attempt_count, max_attempts=job.max_attempts, pois_detected=job.pois_detected, changes_detected=job.changes_detected, change_summary=job.change_summary, error_message=job.error_message, next_scheduled_at=job.next_scheduled_at, duration_seconds=job.duration_seconds, is_overdue=job.is_overdue, can_retry=job.can_retry ) for job in jobs ] @router.post( "/{job_id}/execute", response_model=JobExecutionResult, summary="Execute refresh job", description="Manually trigger execution of a pending POI refresh job" ) async def execute_refresh_job( job_id: str, db: AsyncSession = Depends(get_db) ): """Manually execute a POI refresh job""" try: job_uuid = uuid.UUID(job_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid job_id format") try: poi_refresh_service = POIRefreshService() result = await poi_refresh_service.execute_refresh_job( job_id=job_id, session=db ) logger.info( "POI refresh job executed via API", job_id=job_id, status=result["status"] ) return JobExecutionResult(**result) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error( "Failed to execute POI refresh job", job_id=job_id, error=str(e), exc_info=True ) raise HTTPException( status_code=500, detail=f"Failed to execute refresh job: {str(e)}" ) @router.post( "/process-pending", summary="Process all pending jobs", description="Manually trigger processing of all pending POI refresh jobs" ) async def process_pending_jobs( max_concurrent: int = Query(5, ge=1, le=20, description="Max concurrent executions"), db: AsyncSession = Depends(get_db) ): """Process all pending POI refresh jobs""" try: poi_refresh_service = POIRefreshService() result = await poi_refresh_service.process_pending_jobs( max_concurrent=max_concurrent, session=db ) logger.info( "Pending POI refresh jobs processed via API", total_jobs=result["total_jobs"], successful=result["successful"], failed=result["failed"] ) return result except Exception as e: logger.error( "Failed to process pending POI refresh jobs", error=str(e), exc_info=True ) raise HTTPException( status_code=500, detail=f"Failed to process pending jobs: {str(e)}" ) @router.get( "/pending", response_model=List[POIRefreshJobResponse], summary="Get pending jobs", description="Retrieve all pending POI refresh jobs that are due for execution" ) async def get_pending_jobs( limit: int = Query(100, ge=1, le=500, description="Maximum number of results"), db: AsyncSession = Depends(get_db) ): """Get all pending POI refresh jobs""" try: poi_refresh_service = POIRefreshService() jobs = await poi_refresh_service.get_pending_jobs( limit=limit, session=db ) return [ POIRefreshJobResponse( id=str(job.id), tenant_id=str(job.tenant_id), status=job.status, scheduled_at=job.scheduled_at, started_at=job.started_at, completed_at=job.completed_at, attempt_count=job.attempt_count, max_attempts=job.max_attempts, pois_detected=job.pois_detected, changes_detected=job.changes_detected, change_summary=job.change_summary, error_message=job.error_message, next_scheduled_at=job.next_scheduled_at, duration_seconds=job.duration_seconds, is_overdue=job.is_overdue, can_retry=job.can_retry ) for job in jobs ] except Exception as e: logger.error( "Failed to get pending POI refresh jobs", error=str(e), exc_info=True ) raise HTTPException( status_code=500, detail=f"Failed to get pending jobs: {str(e)}" ) @router.post( "/trigger-scheduler", summary="Trigger scheduler immediately", description="Trigger an immediate check for pending jobs (bypasses schedule)" ) async def trigger_scheduler(): """Trigger POI refresh scheduler immediately""" try: scheduler = get_scheduler() if not scheduler.is_running: raise HTTPException( status_code=503, detail="POI refresh scheduler is not running" ) result = await scheduler.trigger_immediate_check() logger.info( "POI refresh scheduler triggered via API", total_jobs=result["total_jobs"], successful=result["successful"], failed=result["failed"] ) return result except HTTPException: raise except Exception as e: logger.error( "Failed to trigger POI refresh scheduler", error=str(e), exc_info=True ) raise HTTPException( status_code=500, detail=f"Failed to trigger scheduler: {str(e)}" ) @router.get( "/scheduler/status", summary="Get scheduler status", description="Check if POI refresh scheduler is running" ) async def get_scheduler_status(): """Get POI refresh scheduler status""" try: scheduler = get_scheduler() return { "is_running": scheduler.is_running, "check_interval_seconds": scheduler.check_interval_seconds, "max_concurrent_jobs": scheduler.max_concurrent_jobs } except Exception as e: logger.error( "Failed to get scheduler status", error=str(e), exc_info=True ) raise HTTPException( status_code=500, detail=f"Failed to get scheduler status: {str(e)}" )