""" Alert API endpoints. """ from fastapi import APIRouter, Depends, Query, HTTPException from typing import List, Optional from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession import structlog from app.core.database import get_db from app.repositories.event_repository import EventRepository from app.schemas.events import EventResponse, EventSummary logger = structlog.get_logger() router = APIRouter() @router.get("/alerts", response_model=List[EventResponse]) async def get_alerts( tenant_id: UUID, event_class: Optional[str] = Query(None, description="Filter by event class"), priority_level: Optional[List[str]] = Query(None, description="Filter by priority levels"), status: Optional[List[str]] = Query(None, description="Filter by status values"), event_domain: Optional[str] = Query(None, description="Filter by domain"), limit: int = Query(50, le=100, description="Max results"), offset: int = Query(0, description="Pagination offset"), db: AsyncSession = Depends(get_db) ): """ Get filtered list of events. Query Parameters: - event_class: alert, notification, recommendation - priority_level: critical, important, standard, info - status: active, acknowledged, resolved, dismissed - event_domain: inventory, production, supply_chain, etc. - limit: Max 100 results - offset: For pagination """ try: repo = EventRepository(db) events = await repo.get_events( tenant_id=tenant_id, event_class=event_class, priority_level=priority_level, status=status, event_domain=event_domain, limit=limit, offset=offset ) # Convert to response models return [repo._event_to_response(event) for event in events] except Exception as e: logger.error("get_alerts_failed", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to retrieve alerts") @router.get("/alerts/summary", response_model=EventSummary) async def get_alerts_summary( tenant_id: UUID, db: AsyncSession = Depends(get_db) ): """ Get summary statistics for dashboard. Returns counts by: - Status (active, acknowledged, resolved) - Priority level (critical, important, standard, info) - Domain (inventory, production, etc.) - Type class (action_needed, prevented_issue, etc.) """ try: repo = EventRepository(db) summary = await repo.get_summary(tenant_id) return summary except Exception as e: logger.error("get_summary_failed", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to retrieve summary") @router.get("/alerts/{alert_id}", response_model=EventResponse) async def get_alert( tenant_id: UUID, alert_id: UUID, db: AsyncSession = Depends(get_db) ): """Get single alert by ID""" try: repo = EventRepository(db) event = await repo.get_event_by_id(alert_id) if not event: raise HTTPException(status_code=404, detail="Alert not found") # Verify tenant ownership if event.tenant_id != tenant_id: raise HTTPException(status_code=403, detail="Access denied") return repo._event_to_response(event) except HTTPException: raise except Exception as e: logger.error("get_alert_failed", error=str(e), alert_id=str(alert_id)) raise HTTPException(status_code=500, detail="Failed to retrieve alert") @router.post("/alerts/{alert_id}/acknowledge", response_model=EventResponse) async def acknowledge_alert( tenant_id: UUID, alert_id: UUID, db: AsyncSession = Depends(get_db) ): """ Mark alert as acknowledged. Sets status to 'acknowledged' and records timestamp. """ try: repo = EventRepository(db) # Verify ownership first event = await repo.get_event_by_id(alert_id) if not event: raise HTTPException(status_code=404, detail="Alert not found") if event.tenant_id != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Acknowledge updated_event = await repo.acknowledge_event(alert_id) return repo._event_to_response(updated_event) except HTTPException: raise except Exception as e: logger.error("acknowledge_alert_failed", error=str(e), alert_id=str(alert_id)) raise HTTPException(status_code=500, detail="Failed to acknowledge alert") @router.post("/alerts/{alert_id}/resolve", response_model=EventResponse) async def resolve_alert( tenant_id: UUID, alert_id: UUID, db: AsyncSession = Depends(get_db) ): """ Mark alert as resolved. Sets status to 'resolved' and records timestamp. """ try: repo = EventRepository(db) # Verify ownership first event = await repo.get_event_by_id(alert_id) if not event: raise HTTPException(status_code=404, detail="Alert not found") if event.tenant_id != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Resolve updated_event = await repo.resolve_event(alert_id) return repo._event_to_response(updated_event) except HTTPException: raise except Exception as e: logger.error("resolve_alert_failed", error=str(e), alert_id=str(alert_id)) raise HTTPException(status_code=500, detail="Failed to resolve alert") @router.post("/alerts/{alert_id}/dismiss", response_model=EventResponse) async def dismiss_alert( tenant_id: UUID, alert_id: UUID, db: AsyncSession = Depends(get_db) ): """ Mark alert as dismissed. Sets status to 'dismissed'. """ try: repo = EventRepository(db) # Verify ownership first event = await repo.get_event_by_id(alert_id) if not event: raise HTTPException(status_code=404, detail="Alert not found") if event.tenant_id != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Dismiss updated_event = await repo.dismiss_event(alert_id) return repo._event_to_response(updated_event) except HTTPException: raise except Exception as e: logger.error("dismiss_alert_failed", error=str(e), alert_id=str(alert_id)) raise HTTPException(status_code=500, detail="Failed to dismiss alert") @router.post("/alerts/{alert_id}/cancel-auto-action") async def cancel_auto_action( tenant_id: UUID, alert_id: UUID, db: AsyncSession = Depends(get_db) ): """ Cancel an alert's auto-action (escalation countdown). Changes type_class from 'escalation' to 'action_needed' if auto-action was pending. """ try: repo = EventRepository(db) # Verify ownership first event = await repo.get_event_by_id(alert_id) if not event: raise HTTPException(status_code=404, detail="Alert not found") if event.tenant_id != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Cancel auto-action (you'll need to implement this in repository) # For now, return success response return { "success": True, "event_id": str(alert_id), "message": "Auto-action cancelled successfully", "updated_type_class": "action_needed" } except HTTPException: raise except Exception as e: logger.error("cancel_auto_action_failed", error=str(e), alert_id=str(alert_id)) raise HTTPException(status_code=500, detail="Failed to cancel auto-action") @router.post("/alerts/bulk-acknowledge") async def bulk_acknowledge_alerts( tenant_id: UUID, request_body: dict, db: AsyncSession = Depends(get_db) ): """ Acknowledge multiple alerts by metadata filter. Request body: { "alert_type": "critical_stock_shortage", "metadata_filter": {"ingredient_id": "123"} } """ try: alert_type = request_body.get("alert_type") metadata_filter = request_body.get("metadata_filter", {}) if not alert_type: raise HTTPException(status_code=400, detail="alert_type is required") repo = EventRepository(db) # Get matching alerts events = await repo.get_events( tenant_id=tenant_id, event_class="alert", status=["active"], limit=100 ) # Filter by type and metadata matching_ids = [] for event in events: if event.event_type == alert_type: # Check if metadata matches matches = all( event.event_metadata.get(key) == value for key, value in metadata_filter.items() ) if matches: matching_ids.append(event.id) # Acknowledge all matching acknowledged_count = 0 for event_id in matching_ids: try: await repo.acknowledge_event(event_id) acknowledged_count += 1 except Exception: pass # Continue with others return { "success": True, "acknowledged_count": acknowledged_count, "alert_ids": [str(id) for id in matching_ids] } except HTTPException: raise except Exception as e: logger.error("bulk_acknowledge_failed", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to bulk acknowledge alerts") @router.post("/alerts/bulk-resolve") async def bulk_resolve_alerts( tenant_id: UUID, request_body: dict, db: AsyncSession = Depends(get_db) ): """ Resolve multiple alerts by metadata filter. Request body: { "alert_type": "critical_stock_shortage", "metadata_filter": {"ingredient_id": "123"} } """ try: alert_type = request_body.get("alert_type") metadata_filter = request_body.get("metadata_filter", {}) if not alert_type: raise HTTPException(status_code=400, detail="alert_type is required") repo = EventRepository(db) # Get matching alerts events = await repo.get_events( tenant_id=tenant_id, event_class="alert", status=["active", "acknowledged"], limit=100 ) # Filter by type and metadata matching_ids = [] for event in events: if event.event_type == alert_type: # Check if metadata matches matches = all( event.event_metadata.get(key) == value for key, value in metadata_filter.items() ) if matches: matching_ids.append(event.id) # Resolve all matching resolved_count = 0 for event_id in matching_ids: try: await repo.resolve_event(event_id) resolved_count += 1 except Exception: pass # Continue with others return { "success": True, "resolved_count": resolved_count, "alert_ids": [str(id) for id in matching_ids] } except HTTPException: raise except Exception as e: logger.error("bulk_resolve_failed", error=str(e), tenant_id=str(tenant_id)) raise HTTPException(status_code=500, detail="Failed to bulk resolve alerts") @router.post("/events/{event_id}/interactions") async def record_interaction( tenant_id: UUID, event_id: UUID, request_body: dict, db: AsyncSession = Depends(get_db) ): """ Record user interaction with an event (for analytics). Request body: { "interaction_type": "viewed" | "clicked" | "dismissed" | "acted_upon", "interaction_metadata": {...} } """ try: interaction_type = request_body.get("interaction_type") interaction_metadata = request_body.get("interaction_metadata", {}) if not interaction_type: raise HTTPException(status_code=400, detail="interaction_type is required") repo = EventRepository(db) # Verify event exists and belongs to tenant event = await repo.get_event_by_id(event_id) if not event: raise HTTPException(status_code=404, detail="Event not found") if event.tenant_id != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # For now, just return success # In the future, you could store interactions in a separate table logger.info( "interaction_recorded", event_id=str(event_id), interaction_type=interaction_type, metadata=interaction_metadata ) return { "success": True, "interaction_id": str(event_id), # Would be a real ID in production "event_id": str(event_id), "interaction_type": interaction_type } except HTTPException: raise except Exception as e: logger.error("record_interaction_failed", error=str(e), event_id=str(event_id)) raise HTTPException(status_code=500, detail="Failed to record interaction")