imporve features

This commit is contained in:
Urtzi Alfaro
2025-11-14 07:23:56 +01:00
parent 9bc048d360
commit a8d8828935
32 changed files with 5436 additions and 271 deletions

View File

@@ -213,17 +213,17 @@ async def check_is_school_holiday(
response_model=TenantLocationContextResponse
)
async def get_tenant_location_context(
tenant_id: UUID = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get location context for a tenant including school calendar assignment (cached)"""
try:
tenant_id_str = str(tenant_id)
# Check cache first
cached = await cache.get_cached_tenant_context(tenant_id_str)
cached = await cache.get_cached_tenant_context(tenant_id)
if cached:
logger.debug("Returning cached tenant context", tenant_id=tenant_id_str)
logger.debug("Returning cached tenant context", tenant_id=tenant_id)
return TenantLocationContextResponse(**cached)
# Cache miss - fetch from database
@@ -261,11 +261,16 @@ async def get_tenant_location_context(
)
async def create_or_update_tenant_location_context(
request: TenantLocationContextCreateRequest,
tenant_id: UUID = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create or update tenant location context"""
try:
# Convert to UUID for use with repository
tenant_uuid = UUID(tenant_id)
repo = CalendarRepository(db)
# Validate calendar_id if provided
@@ -279,7 +284,7 @@ async def create_or_update_tenant_location_context(
# Create or update context
context_obj = await repo.create_or_update_tenant_location_context(
tenant_id=tenant_id,
tenant_id=tenant_uuid,
city_id=request.city_id,
school_calendar_id=request.school_calendar_id,
neighborhood=request.neighborhood,
@@ -288,13 +293,13 @@ async def create_or_update_tenant_location_context(
)
# Invalidate cache since context was updated
await cache.invalidate_tenant_context(str(tenant_id))
await cache.invalidate_tenant_context(tenant_id)
# Get full context with calendar details
context = await repo.get_tenant_with_calendar(tenant_id)
context = await repo.get_tenant_with_calendar(tenant_uuid)
# Cache the new context
await cache.set_cached_tenant_context(str(tenant_id), context)
await cache.set_cached_tenant_context(tenant_id, context)
return TenantLocationContextResponse(**context)
@@ -317,13 +322,18 @@ async def create_or_update_tenant_location_context(
status_code=204
)
async def delete_tenant_location_context(
tenant_id: UUID = Depends(get_current_user_dep),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete tenant location context"""
try:
# Convert to UUID for use with repository
tenant_uuid = UUID(tenant_id)
repo = CalendarRepository(db)
deleted = await repo.delete_tenant_location_context(tenant_id)
deleted = await repo.delete_tenant_location_context(tenant_uuid)
if not deleted:
raise HTTPException(
@@ -347,6 +357,97 @@ async def delete_tenant_location_context(
)
# ===== Calendar Suggestion Endpoint =====
@router.post(
route_builder.build_base_route("location-context/suggest-calendar")
)
async def suggest_calendar_for_tenant(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Suggest an appropriate school calendar for a tenant based on location and POI data.
This endpoint analyzes:
- Tenant's city location
- Detected schools nearby (from POI detection)
- Available calendars for the city
- Bakery-specific heuristics (primary schools = stronger morning rush)
Returns a suggestion with confidence score and reasoning.
Does NOT automatically assign - requires admin approval.
"""
try:
from app.utils.calendar_suggester import CalendarSuggester
from app.repositories.poi_context_repository import POIContextRepository
tenant_uuid = UUID(tenant_id)
# Get tenant's location context
calendar_repo = CalendarRepository(db)
location_context = await calendar_repo.get_tenant_location_context(tenant_uuid)
if not location_context:
raise HTTPException(
status_code=404,
detail="Location context not found. Create location context first."
)
city_id = location_context.city_id
# Get available calendars for city
calendars_result = await calendar_repo.get_calendars_by_city(city_id, enabled_only=True)
calendars = calendars_result.get("calendars", []) if calendars_result else []
# Get POI context if available
poi_repo = POIContextRepository(db)
poi_context = await poi_repo.get_by_tenant_id(tenant_uuid)
poi_data = poi_context.to_dict() if poi_context else None
# Generate suggestion
suggester = CalendarSuggester()
suggestion = suggester.suggest_calendar_for_tenant(
city_id=city_id,
available_calendars=calendars,
poi_context=poi_data,
tenant_data=None # Could include tenant info if needed
)
# Format for admin display
admin_message = suggester.format_suggestion_for_admin(suggestion)
logger.info(
"Calendar suggestion generated",
tenant_id=tenant_id,
city_id=city_id,
suggested_calendar=suggestion.get("suggested_calendar_id"),
confidence=suggestion.get("confidence")
)
return {
**suggestion,
"admin_message": admin_message,
"tenant_id": tenant_id,
"current_calendar_id": str(location_context.school_calendar_id) if location_context.school_calendar_id else None
}
except HTTPException:
raise
except Exception as e:
logger.error(
"Error generating calendar suggestion",
tenant_id=tenant_id,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Error generating calendar suggestion: {str(e)}"
)
# ===== Helper Endpoints =====
@router.get(

View File

@@ -21,10 +21,10 @@ from app.core.redis_client import get_redis_client
logger = structlog.get_logger()
router = APIRouter(prefix="/poi-context", tags=["POI Context"])
router = APIRouter(prefix="/tenants", tags=["POI Context"])
@router.post("/{tenant_id}/detect")
@router.post("/{tenant_id}/poi-context/detect")
async def detect_pois_for_tenant(
tenant_id: str,
latitude: float = Query(..., description="Bakery latitude"),
@@ -209,13 +209,79 @@ async def detect_pois_for_tenant(
relevant_categories=len(feature_selection.get("relevant_categories", []))
)
# Phase 3: Auto-trigger calendar suggestion after POI detection
# This helps admins by providing intelligent calendar recommendations
calendar_suggestion = None
try:
from app.utils.calendar_suggester import CalendarSuggester
from app.repositories.calendar_repository import CalendarRepository
# Get tenant's location context
calendar_repo = CalendarRepository(db)
location_context = await calendar_repo.get_tenant_location_context(tenant_uuid)
if location_context and location_context.school_calendar_id is None:
# Only suggest if no calendar assigned yet
city_id = location_context.city_id
# Get available calendars for city
calendars_result = await calendar_repo.get_calendars_by_city(city_id, enabled_only=True)
calendars = calendars_result.get("calendars", []) if calendars_result else []
if calendars:
# Generate suggestion using POI data
suggester = CalendarSuggester()
calendar_suggestion = suggester.suggest_calendar_for_tenant(
city_id=city_id,
available_calendars=calendars,
poi_context=poi_context.to_dict(),
tenant_data=None
)
logger.info(
"Calendar suggestion auto-generated after POI detection",
tenant_id=tenant_id,
suggested_calendar=calendar_suggestion.get("calendar_name"),
confidence=calendar_suggestion.get("confidence_percentage"),
should_auto_assign=calendar_suggestion.get("should_auto_assign")
)
# TODO: Send notification to admin about available suggestion
# This will be implemented when notification service is integrated
else:
logger.info(
"No calendars available for city, skipping suggestion",
tenant_id=tenant_id,
city_id=city_id
)
elif location_context and location_context.school_calendar_id:
logger.info(
"Calendar already assigned, skipping suggestion",
tenant_id=tenant_id,
calendar_id=str(location_context.school_calendar_id)
)
else:
logger.warning(
"No location context found, skipping calendar suggestion",
tenant_id=tenant_id
)
except Exception as e:
# Non-blocking: POI detection should succeed even if suggestion fails
logger.warning(
"Failed to auto-generate calendar suggestion (non-blocking)",
tenant_id=tenant_id,
error=str(e)
)
return {
"status": "success",
"source": "detection",
"poi_context": poi_context.to_dict(),
"feature_selection": feature_selection,
"competitor_analysis": competitor_analysis,
"competitive_insights": competitive_insights
"competitive_insights": competitive_insights,
"calendar_suggestion": calendar_suggestion # Include suggestion in response
}
except Exception as e:
@@ -231,7 +297,7 @@ async def detect_pois_for_tenant(
)
@router.get("/{tenant_id}")
@router.get("/{tenant_id}/poi-context")
async def get_poi_context(
tenant_id: str,
db: AsyncSession = Depends(get_db)
@@ -265,7 +331,7 @@ async def get_poi_context(
}
@router.post("/{tenant_id}/refresh")
@router.post("/{tenant_id}/poi-context/refresh")
async def refresh_poi_context(
tenant_id: str,
db: AsyncSession = Depends(get_db)
@@ -299,7 +365,7 @@ async def refresh_poi_context(
)
@router.delete("/{tenant_id}")
@router.delete("/{tenant_id}/poi-context")
async def delete_poi_context(
tenant_id: str,
db: AsyncSession = Depends(get_db)
@@ -327,7 +393,7 @@ async def delete_poi_context(
}
@router.get("/{tenant_id}/feature-importance")
@router.get("/{tenant_id}/poi-context/feature-importance")
async def get_feature_importance(
tenant_id: str,
db: AsyncSession = Depends(get_db)
@@ -364,7 +430,7 @@ async def get_feature_importance(
}
@router.get("/{tenant_id}/competitor-analysis")
@router.get("/{tenant_id}/poi-context/competitor-analysis")
async def get_competitor_analysis(
tenant_id: str,
db: AsyncSession = Depends(get_db)