REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View File

@@ -0,0 +1,286 @@
"""
Notification Service Analytics API Endpoints
Professional/Enterprise tier analytics for notification performance and delivery metrics
"""
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from typing import Optional, Dict, Any
from uuid import UUID
from datetime import datetime
import structlog
from app.core.database import get_db
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import analytics_tier_required
from shared.routing.route_builder import RouteBuilder
router = APIRouter()
logger = structlog.get_logger()
route_builder = RouteBuilder('notification')
@router.get(
route_builder.build_analytics_route("delivery-stats"),
response_model=dict
)
@analytics_tier_required
async def get_delivery_statistics(
tenant_id: UUID = Path(...),
days: int = Query(30, ge=1, le=365),
notification_type: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Analyze notification delivery statistics (Professional/Enterprise)"""
try:
return {
"period_days": days,
"notification_type": notification_type,
"total_sent": 0,
"delivery_stats": {
"delivered": 0,
"failed": 0,
"pending": 0,
"cancelled": 0,
"delivery_rate": 0.0
},
"channel_breakdown": {
"email": {"sent": 0, "delivered": 0, "failed": 0},
"whatsapp": {"sent": 0, "delivered": 0, "failed": 0},
"push": {"sent": 0, "delivered": 0, "failed": 0}
},
"daily_trends": [],
"average_delivery_time_seconds": 0.0
}
except Exception as e:
logger.error("Failed to get delivery stats", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get delivery stats: {str(e)}")
@router.get(
route_builder.build_analytics_route("engagement-metrics"),
response_model=dict
)
@analytics_tier_required
async def get_engagement_metrics(
tenant_id: UUID = Path(...),
days: int = Query(30, ge=1, le=365),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Analyze user engagement with notifications (Professional/Enterprise)"""
try:
return {
"period_days": days,
"engagement_stats": {
"total_notifications": 0,
"read_notifications": 0,
"unread_notifications": 0,
"read_rate": 0.0,
"average_time_to_read_minutes": 0.0
},
"user_engagement": {
"active_users": 0,
"inactive_users": 0,
"highly_engaged_users": 0
},
"type_engagement": [],
"hourly_engagement_pattern": [],
"day_of_week_pattern": []
}
except Exception as e:
logger.error("Failed to get engagement metrics", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get engagement metrics: {str(e)}")
@router.get(
route_builder.build_analytics_route("failure-analysis"),
response_model=dict
)
@analytics_tier_required
async def get_failure_analysis(
tenant_id: UUID = Path(...),
days: int = Query(7, ge=1, le=90),
channel: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Analyze notification failures and errors (Professional/Enterprise)"""
try:
return {
"period_days": days,
"channel": channel,
"total_failures": 0,
"failure_rate": 0.0,
"failure_breakdown": {
"invalid_recipient": 0,
"service_error": 0,
"network_error": 0,
"rate_limit": 0,
"authentication_error": 0,
"other": 0
},
"common_errors": [],
"error_trends": [],
"retry_success_rate": 0.0,
"recommendations": []
}
except Exception as e:
logger.error("Failed to get failure analysis", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get failure analysis: {str(e)}")
@router.get(
route_builder.build_analytics_route("template-performance"),
response_model=dict
)
@analytics_tier_required
async def get_template_performance(
tenant_id: UUID = Path(...),
days: int = Query(30, ge=1, le=365),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Analyze notification template performance (Professional/Enterprise)"""
try:
return {
"period_days": days,
"templates": [],
"performance_metrics": {
"best_performing_template": None,
"worst_performing_template": None,
"average_delivery_rate": 0.0,
"average_read_rate": 0.0
},
"optimization_suggestions": []
}
except Exception as e:
logger.error("Failed to get template performance", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get template performance: {str(e)}")
@router.get(
route_builder.build_analytics_route("channel-comparison"),
response_model=dict
)
@analytics_tier_required
async def get_channel_comparison(
tenant_id: UUID = Path(...),
days: int = Query(30, ge=1, le=365),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Compare performance across notification channels (Professional/Enterprise)"""
try:
return {
"period_days": days,
"channels": {
"email": {
"sent": 0,
"delivered": 0,
"read": 0,
"failed": 0,
"delivery_rate": 0.0,
"read_rate": 0.0,
"avg_delivery_time_seconds": 0.0,
"cost_per_notification": 0.0
},
"whatsapp": {
"sent": 0,
"delivered": 0,
"read": 0,
"failed": 0,
"delivery_rate": 0.0,
"read_rate": 0.0,
"avg_delivery_time_seconds": 0.0,
"cost_per_notification": 0.0
},
"push": {
"sent": 0,
"delivered": 0,
"read": 0,
"failed": 0,
"delivery_rate": 0.0,
"read_rate": 0.0,
"avg_delivery_time_seconds": 0.0,
"cost_per_notification": 0.0
}
},
"recommendations": {
"most_reliable_channel": None,
"fastest_channel": None,
"most_cost_effective_channel": None,
"best_engagement_channel": None
}
}
except Exception as e:
logger.error("Failed to get channel comparison", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get channel comparison: {str(e)}")
@router.get(
route_builder.build_analytics_route("user-preferences-insights"),
response_model=dict
)
@analytics_tier_required
async def get_user_preferences_insights(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Analyze user notification preferences patterns (Professional/Enterprise)"""
try:
return {
"total_users": 0,
"preference_distribution": {
"email_enabled": 0,
"whatsapp_enabled": 0,
"push_enabled": 0,
"all_channels_enabled": 0,
"no_channels_enabled": 0
},
"popular_notification_types": [],
"opt_out_trends": [],
"channel_preferences": {
"email_only": 0,
"whatsapp_only": 0,
"push_only": 0,
"multi_channel": 0
},
"recommendations": []
}
except Exception as e:
logger.error("Failed to get preferences insights", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get preferences insights: {str(e)}")
@router.get(
route_builder.build_analytics_route("cost-analysis"),
response_model=dict
)
@analytics_tier_required
async def get_cost_analysis(
tenant_id: UUID = Path(...),
days: int = Query(30, ge=1, le=365),
current_user: dict = Depends(get_current_user_dep),
db=Depends(get_db)
):
"""Analyze notification delivery costs (Professional/Enterprise)"""
try:
return {
"period_days": days,
"total_cost": 0.0,
"cost_by_channel": {
"email": 0.0,
"whatsapp": 0.0,
"push": 0.0
},
"cost_by_type": [],
"volume_vs_cost_trends": [],
"cost_optimization_suggestions": [],
"projected_monthly_cost": 0.0,
"cost_per_user": 0.0
}
except Exception as e:
logger.error("Failed to get cost analysis", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get cost analysis: {str(e)}")

View File

@@ -0,0 +1,723 @@
"""
Notification Business Operations API
Handles sending, marking read, scheduling, retry, and SSE streaming
"""
import asyncio
import json
import structlog
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Request, BackgroundTasks
from typing import List, Optional, Dict, Any
from uuid import UUID
from sse_starlette.sse import EventSourceResponse
from app.schemas.notifications import (
NotificationResponse, NotificationType, NotificationStatus, NotificationPriority
)
from app.services.notification_service import EnhancedNotificationService
from app.models.notifications import NotificationType as ModelNotificationType
from shared.auth.decorators import get_current_user_dep, get_current_user
from shared.auth.access_control import require_user_role, admin_role_required
from shared.routing.route_builder import RouteBuilder
from shared.database.base import create_database_manager
from shared.monitoring.metrics import track_endpoint_metrics
logger = structlog.get_logger()
router = APIRouter()
route_builder = RouteBuilder("notification")
# Dependency injection for enhanced notification service
def get_enhanced_notification_service():
database_manager = create_database_manager()
return EnhancedNotificationService(database_manager)
# ============================================================================
# BUSINESS OPERATIONS - Send, Schedule, Retry, Mark Read
# ============================================================================
@router.post(
route_builder.build_base_route("send", include_tenant_prefix=False),
response_model=NotificationResponse,
status_code=201
)
@require_user_role(["member", "admin", "owner"])
@track_endpoint_metrics("notification_send")
async def send_notification(
notification_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Send a single notification with enhanced validation and features"""
try:
# Check permissions for broadcast notifications
if notification_data.get("broadcast", False) and current_user.get("role") not in ["admin", "manager"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins and managers can send broadcast notifications"
)
# Validate required fields
if not notification_data.get("message"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Message is required"
)
if not notification_data.get("type"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Notification type is required"
)
# Convert string type to enum
try:
notification_type = ModelNotificationType(notification_data["type"])
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid notification type: {notification_data['type']}"
)
# Convert priority if provided
priority = NotificationPriority.NORMAL
if "priority" in notification_data:
try:
priority = NotificationPriority(notification_data["priority"])
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid priority: {notification_data['priority']}"
)
# Create notification using enhanced service
notification = await notification_service.create_notification(
tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"],
notification_type=notification_type,
message=notification_data["message"],
recipient_id=notification_data.get("recipient_id"),
recipient_email=notification_data.get("recipient_email"),
recipient_phone=notification_data.get("recipient_phone"),
subject=notification_data.get("subject"),
html_content=notification_data.get("html_content"),
template_key=notification_data.get("template_key"),
template_data=notification_data.get("template_data"),
priority=priority,
scheduled_at=notification_data.get("scheduled_at"),
broadcast=notification_data.get("broadcast", False)
)
logger.info("Notification sent successfully",
notification_id=notification.id,
tenant_id=current_user.get("tenant_id"),
type=notification_type.value,
priority=priority.value)
return NotificationResponse.from_orm(notification)
except HTTPException:
raise
except Exception as e:
logger.error("Failed to send notification",
tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send notification"
)
@router.patch(
route_builder.build_operations_route("{notification_id}/read", include_tenant_prefix=False)
)
@require_user_role(["viewer", "member", "admin", "owner"])
@track_endpoint_metrics("notification_mark_read")
async def mark_notification_read(
notification_id: UUID = Path(..., description="Notification ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Mark a notification as read"""
try:
success = await notification_service.mark_notification_as_read(
str(notification_id),
current_user["user_id"]
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found or access denied"
)
return {"success": True, "message": "Notification marked as read"}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to mark notification as read",
notification_id=str(notification_id),
user_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to mark notification as read"
)
@router.patch(
route_builder.build_base_route("mark-multiple-read", include_tenant_prefix=False)
)
@require_user_role(["viewer", "member", "admin", "owner"])
@track_endpoint_metrics("notification_mark_multiple_read")
async def mark_multiple_notifications_read(
request_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Mark multiple notifications as read with batch processing"""
try:
notification_ids = request_data.get("notification_ids")
tenant_id = request_data.get("tenant_id")
if not notification_ids and not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Either notification_ids or tenant_id must be provided"
)
# Convert UUID strings to strings if needed
if notification_ids:
notification_ids = [str(nid) for nid in notification_ids]
marked_count = await notification_service.mark_multiple_as_read(
user_id=current_user["user_id"],
notification_ids=notification_ids,
tenant_id=tenant_id
)
return {
"success": True,
"marked_count": marked_count,
"message": f"Marked {marked_count} notifications as read"
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to mark multiple notifications as read",
user_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to mark notifications as read"
)
@router.patch(
route_builder.build_operations_route("{notification_id}/status", include_tenant_prefix=False)
)
@require_user_role(["admin", "owner"])
@track_endpoint_metrics("notification_update_status")
async def update_notification_status(
notification_id: UUID = Path(..., description="Notification ID"),
status_data: Dict[str, Any] = ...,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Update notification status (admin/system only)"""
# Only system users or admins can update notification status
if (current_user.get("type") != "service" and
current_user.get("role") not in ["admin", "system"]):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only system services or admins can update notification status"
)
try:
new_status = status_data.get("status")
if not new_status:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Status is required"
)
# Convert string status to enum
try:
from app.models.notifications import NotificationStatus as ModelStatus
model_status = ModelStatus(new_status)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status: {new_status}"
)
updated_notification = await notification_service.update_notification_status(
notification_id=str(notification_id),
new_status=model_status,
error_message=status_data.get("error_message"),
provider_message_id=status_data.get("provider_message_id"),
metadata=status_data.get("metadata"),
response_time_ms=status_data.get("response_time_ms"),
provider=status_data.get("provider")
)
if not updated_notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
return NotificationResponse.from_orm(updated_notification)
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update notification status",
notification_id=str(notification_id),
status=status_data.get("status"),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update notification status"
)
@router.get(
route_builder.build_base_route("pending", include_tenant_prefix=False),
response_model=List[NotificationResponse]
)
@require_user_role(["admin", "owner"])
@track_endpoint_metrics("notification_get_pending")
async def get_pending_notifications(
limit: int = Query(100, ge=1, le=1000, description="Maximum number of notifications"),
notification_type: Optional[NotificationType] = Query(None, description="Filter by type"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Get pending notifications for processing (system/admin only)"""
if (current_user.get("type") != "service" and
current_user.get("role") not in ["admin", "system"]):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only system services or admins can access pending notifications"
)
try:
model_notification_type = None
if notification_type:
try:
model_notification_type = ModelNotificationType(notification_type.value)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid notification type: {notification_type.value}"
)
notifications = await notification_service.get_pending_notifications(
limit=limit,
notification_type=model_notification_type
)
return [NotificationResponse.from_orm(notification) for notification in notifications]
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get pending notifications",
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get pending notifications"
)
@router.post(
route_builder.build_operations_route("{notification_id}/schedule", include_tenant_prefix=False)
)
@require_user_role(["member", "admin", "owner"])
@track_endpoint_metrics("notification_schedule")
async def schedule_notification(
notification_id: UUID = Path(..., description="Notification ID"),
schedule_data: Dict[str, Any] = ...,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Schedule a notification for future delivery"""
try:
scheduled_at = schedule_data.get("scheduled_at")
if not scheduled_at:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="scheduled_at is required"
)
# Parse datetime if it's a string
if isinstance(scheduled_at, str):
try:
scheduled_at = datetime.fromisoformat(scheduled_at.replace('Z', '+00:00'))
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid datetime format. Use ISO format."
)
# Check that the scheduled time is in the future
if scheduled_at <= datetime.utcnow():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Scheduled time must be in the future"
)
success = await notification_service.schedule_notification(
str(notification_id),
scheduled_at
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found or cannot be scheduled"
)
return {
"success": True,
"message": "Notification scheduled successfully",
"scheduled_at": scheduled_at.isoformat()
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to schedule notification",
notification_id=str(notification_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to schedule notification"
)
@router.post(
route_builder.build_operations_route("{notification_id}/cancel", include_tenant_prefix=False)
)
@require_user_role(["member", "admin", "owner"])
@track_endpoint_metrics("notification_cancel")
async def cancel_notification(
notification_id: UUID = Path(..., description="Notification ID"),
cancel_data: Optional[Dict[str, Any]] = None,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Cancel a pending notification"""
try:
reason = None
if cancel_data:
reason = cancel_data.get("reason", "Cancelled by user")
else:
reason = "Cancelled by user"
success = await notification_service.cancel_notification(
str(notification_id),
reason
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found or cannot be cancelled"
)
return {
"success": True,
"message": "Notification cancelled successfully",
"reason": reason
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to cancel notification",
notification_id=str(notification_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to cancel notification"
)
@router.post(
route_builder.build_operations_route("{notification_id}/retry", include_tenant_prefix=False)
)
@require_user_role(["admin", "owner"])
@track_endpoint_metrics("notification_retry")
async def retry_failed_notification(
notification_id: UUID = Path(..., description="Notification ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Retry a failed notification (admin only)"""
# Only admins can retry notifications
if current_user.get("role") not in ["admin", "system"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can retry failed notifications"
)
try:
success = await notification_service.retry_failed_notification(str(notification_id))
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found, not failed, or max retries exceeded"
)
return {
"success": True,
"message": "Notification queued for retry"
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to retry notification",
notification_id=str(notification_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retry notification"
)
@router.get(
route_builder.build_base_route("statistics", include_tenant_prefix=False)
)
@admin_role_required
@track_endpoint_metrics("notification_get_statistics")
async def get_notification_statistics(
tenant_id: Optional[str] = Query(None, description="Filter by tenant ID"),
days_back: int = Query(30, ge=1, le=365, description="Number of days to look back"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Get comprehensive notification statistics"""
try:
stats = await notification_service.get_notification_statistics(
tenant_id=tenant_id,
days_back=days_back
)
return stats
except Exception as e:
logger.error("Failed to get notification statistics",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get notification statistics"
)
# ============================================================================
# SSE STREAMING ENDPOINTS
# ============================================================================
@router.get(route_builder.build_operations_route("sse/stream/{tenant_id}", include_tenant_prefix=False))
async def stream_notifications(
request: Request,
background_tasks: BackgroundTasks,
tenant_id: str = Path(..., description="Tenant ID"),
token: Optional[str] = None
):
"""
SSE endpoint for real-time notification streaming
Supports alerts and recommendations through unified stream
"""
# Validate token and get user (skip for now to test connection)
# TODO: Add proper token validation in production
current_user = None
if token:
try:
# In a real implementation, validate the JWT token here
# For now, skip validation to test the connection
pass
except Exception:
raise HTTPException(401, "Invalid token")
# Skip tenant access validation for testing
# TODO: Add tenant access validation in production
# Get SSE service from app state
sse_service = getattr(request.app.state, 'sse_service', None)
if not sse_service:
raise HTTPException(500, "SSE service not available")
async def event_generator():
"""Generate SSE events for the client"""
client_queue = asyncio.Queue(maxsize=100) # Limit queue size
try:
# Register client
await sse_service.add_client(tenant_id, client_queue)
logger.info("SSE client connected",
tenant_id=tenant_id,
user_id=getattr(current_user, 'id', 'unknown'))
# Stream events
while True:
# Check if client disconnected
if await request.is_disconnected():
logger.info("SSE client disconnected", tenant_id=tenant_id)
break
try:
# Wait for events with timeout for keepalive
event = await asyncio.wait_for(
client_queue.get(),
timeout=30.0
)
yield event
except asyncio.TimeoutError:
# Send keepalive ping
yield {
"event": "ping",
"data": json.dumps({
"timestamp": datetime.utcnow().isoformat(),
"status": "keepalive"
}),
"id": f"ping_{int(datetime.now().timestamp())}"
}
except Exception as e:
logger.error("Error in SSE event generator",
tenant_id=tenant_id,
error=str(e))
break
except Exception as e:
logger.error("SSE connection error",
tenant_id=tenant_id,
error=str(e))
finally:
# Clean up on disconnect
try:
await sse_service.remove_client(tenant_id, client_queue)
logger.info("SSE client cleanup completed", tenant_id=tenant_id)
except Exception as e:
logger.error("Error cleaning up SSE client",
tenant_id=tenant_id,
error=str(e))
return EventSourceResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Disable nginx buffering
}
)
@router.post(route_builder.build_operations_route("sse/items/{item_id}/acknowledge", include_tenant_prefix=False))
async def acknowledge_item(
item_id: str,
current_user = Depends(get_current_user)
):
"""Acknowledge an alert or recommendation"""
try:
# This would update the database
# For now, just return success
logger.info("Item acknowledged",
item_id=item_id,
user_id=getattr(current_user, 'id', 'unknown'))
return {
"status": "success",
"item_id": item_id,
"acknowledged_by": getattr(current_user, 'id', 'unknown'),
"acknowledged_at": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error("Failed to acknowledge item", item_id=item_id, error=str(e))
raise HTTPException(500, "Failed to acknowledge item")
@router.post(route_builder.build_operations_route("sse/items/{item_id}/resolve", include_tenant_prefix=False))
async def resolve_item(
item_id: str,
current_user = Depends(get_current_user)
):
"""Resolve an alert or recommendation"""
try:
# This would update the database
# For now, just return success
logger.info("Item resolved",
item_id=item_id,
user_id=getattr(current_user, 'id', 'unknown'))
return {
"status": "success",
"item_id": item_id,
"resolved_by": getattr(current_user, 'id', 'unknown'),
"resolved_at": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error("Failed to resolve item", item_id=item_id, error=str(e))
raise HTTPException(500, "Failed to resolve item")
@router.get(route_builder.build_operations_route("sse/status/{tenant_id}", include_tenant_prefix=False))
async def get_sse_status(
request: Request,
tenant_id: str = Path(..., description="Tenant ID"),
current_user = Depends(get_current_user)
):
"""Get SSE connection status for a tenant"""
# Verify user has access to this tenant
if not hasattr(current_user, 'has_access_to_tenant') or not current_user.has_access_to_tenant(tenant_id):
raise HTTPException(403, "Access denied to this tenant")
try:
# Get SSE service from app state
sse_service = getattr(request.app.state, 'sse_service', None)
if not sse_service:
return {"status": "unavailable", "message": "SSE service not initialized"}
metrics = sse_service.get_metrics()
tenant_connections = len(sse_service.active_connections.get(tenant_id, set()))
return {
"status": "available",
"tenant_id": tenant_id,
"connections": tenant_connections,
"total_connections": metrics["total_connections"],
"active_tenants": metrics["active_tenants"]
}
except Exception as e:
logger.error("Failed to get SSE status", tenant_id=tenant_id, error=str(e))
raise HTTPException(500, "Failed to get SSE status")

View File

@@ -1,129 +1,46 @@
"""
Enhanced Notification API endpoints using repository pattern and dependency injection
Notification CRUD API endpoints (ATOMIC operations only)
Handles basic notification retrieval and listing
"""
import structlog
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, BackgroundTasks
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path
from typing import List, Optional, Dict, Any
from uuid import UUID
from app.schemas.notifications import (
NotificationCreate, NotificationResponse, NotificationHistory,
NotificationStats, NotificationPreferences, PreferencesUpdate,
BulkNotificationCreate, TemplateCreate, TemplateResponse,
DeliveryWebhook, ReadReceiptWebhook, NotificationType,
NotificationStatus, NotificationPriority
NotificationResponse, NotificationType, NotificationStatus
)
from app.services.notification_service import EnhancedNotificationService
from app.models.notifications import NotificationType as ModelNotificationType
from shared.auth.decorators import (
get_current_user_dep,
require_role
)
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
from shared.routing.route_builder import RouteBuilder
from shared.database.base import create_database_manager
from shared.monitoring.metrics import track_endpoint_metrics
logger = structlog.get_logger()
router = APIRouter()
route_builder = RouteBuilder("notification")
# Dependency injection for enhanced notification service
def get_enhanced_notification_service():
database_manager = create_database_manager()
return EnhancedNotificationService(database_manager)
@router.post("/send", response_model=NotificationResponse)
@track_endpoint_metrics("notification_send")
async def send_notification_enhanced(
notification_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Send a single notification with enhanced validation and features"""
try:
# Check permissions for broadcast notifications
if notification_data.get("broadcast", False) and current_user.get("role") not in ["admin", "manager"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins and managers can send broadcast notifications"
)
# Validate required fields
if not notification_data.get("message"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Message is required"
)
if not notification_data.get("type"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Notification type is required"
)
# Convert string type to enum
try:
notification_type = ModelNotificationType(notification_data["type"])
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid notification type: {notification_data['type']}"
)
# Convert priority if provided
priority = NotificationPriority.NORMAL
if "priority" in notification_data:
try:
priority = NotificationPriority(notification_data["priority"])
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid priority: {notification_data['priority']}"
)
# Create notification using enhanced service
notification = await notification_service.create_notification(
tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"],
notification_type=notification_type,
message=notification_data["message"],
recipient_id=notification_data.get("recipient_id"),
recipient_email=notification_data.get("recipient_email"),
recipient_phone=notification_data.get("recipient_phone"),
subject=notification_data.get("subject"),
html_content=notification_data.get("html_content"),
template_key=notification_data.get("template_key"),
template_data=notification_data.get("template_data"),
priority=priority,
scheduled_at=notification_data.get("scheduled_at"),
broadcast=notification_data.get("broadcast", False)
)
logger.info("Notification sent successfully",
notification_id=notification.id,
tenant_id=current_user.get("tenant_id"),
type=notification_type.value,
priority=priority.value)
return NotificationResponse.from_orm(notification)
except HTTPException:
raise
except Exception as e:
logger.error("Failed to send notification",
tenant_id=current_user.get("tenant_id"),
sender_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send notification"
)
# ============================================================================
# ATOMIC CRUD ENDPOINTS - Get/List notifications only
# ============================================================================
@router.get("/notifications/{notification_id}", response_model=NotificationResponse)
@router.get(
route_builder.build_resource_detail_route("{notification_id}"),
response_model=NotificationResponse
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@track_endpoint_metrics("notification_get")
async def get_notification_enhanced(
notification_id: UUID = Path(..., description="Notification ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
@@ -161,11 +78,15 @@ async def get_notification_enhanced(
detail="Failed to get notification"
)
@router.get("/notifications/user/{user_id}", response_model=List[NotificationResponse])
@router.get(
route_builder.build_base_route("user/{user_id}"),
response_model=List[NotificationResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@track_endpoint_metrics("notification_get_user_notifications")
async def get_user_notifications_enhanced(
user_id: str = Path(..., description="User ID"),
tenant_id: Optional[str] = Query(None, description="Filter by tenant ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
unread_only: bool = Query(False, description="Only return unread notifications"),
notification_type: Optional[NotificationType] = Query(None, description="Filter by notification type"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
@@ -216,7 +137,11 @@ async def get_user_notifications_enhanced(
detail="Failed to get user notifications"
)
@router.get("/notifications/tenant/{tenant_id}", response_model=List[NotificationResponse])
@router.get(
route_builder.build_base_route("list"),
response_model=List[NotificationResponse]
)
@require_user_role(["viewer", "member", "admin", "owner"])
@track_endpoint_metrics("notification_get_tenant_notifications")
async def get_tenant_notifications_enhanced(
tenant_id: str = Path(..., description="Tenant ID"),
@@ -279,370 +204,3 @@ async def get_tenant_notifications_enhanced(
detail="Failed to get tenant notifications"
)
@router.patch("/notifications/{notification_id}/read")
@track_endpoint_metrics("notification_mark_read")
async def mark_notification_read_enhanced(
notification_id: UUID = Path(..., description="Notification ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Mark a notification as read with enhanced validation"""
try:
success = await notification_service.mark_notification_as_read(
str(notification_id),
current_user["user_id"]
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found or access denied"
)
return {"success": True, "message": "Notification marked as read"}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to mark notification as read",
notification_id=str(notification_id),
user_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to mark notification as read"
)
@router.patch("/notifications/mark-multiple-read")
@track_endpoint_metrics("notification_mark_multiple_read")
async def mark_multiple_notifications_read_enhanced(
request_data: Dict[str, Any],
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Mark multiple notifications as read with enhanced batch processing"""
try:
notification_ids = request_data.get("notification_ids")
tenant_id = request_data.get("tenant_id")
if not notification_ids and not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Either notification_ids or tenant_id must be provided"
)
# Convert UUID strings to strings if needed
if notification_ids:
notification_ids = [str(nid) for nid in notification_ids]
marked_count = await notification_service.mark_multiple_as_read(
user_id=current_user["user_id"],
notification_ids=notification_ids,
tenant_id=tenant_id
)
return {
"success": True,
"marked_count": marked_count,
"message": f"Marked {marked_count} notifications as read"
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to mark multiple notifications as read",
user_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to mark notifications as read"
)
@router.patch("/notifications/{notification_id}/status")
@track_endpoint_metrics("notification_update_status")
async def update_notification_status_enhanced(
notification_id: UUID = Path(..., description="Notification ID"),
status_data: Dict[str, Any] = ...,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Update notification status with enhanced logging and validation"""
# Only system users or admins can update notification status
if (current_user.get("type") != "service" and
current_user.get("role") not in ["admin", "system"]):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only system services or admins can update notification status"
)
try:
new_status = status_data.get("status")
if not new_status:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Status is required"
)
# Convert string status to enum
try:
from app.models.notifications import NotificationStatus as ModelStatus
model_status = ModelStatus(new_status)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status: {new_status}"
)
updated_notification = await notification_service.update_notification_status(
notification_id=str(notification_id),
new_status=model_status,
error_message=status_data.get("error_message"),
provider_message_id=status_data.get("provider_message_id"),
metadata=status_data.get("metadata"),
response_time_ms=status_data.get("response_time_ms"),
provider=status_data.get("provider")
)
if not updated_notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
return NotificationResponse.from_orm(updated_notification)
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update notification status",
notification_id=str(notification_id),
status=status_data.get("status"),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update notification status"
)
@router.get("/notifications/pending", response_model=List[NotificationResponse])
@track_endpoint_metrics("notification_get_pending")
async def get_pending_notifications_enhanced(
limit: int = Query(100, ge=1, le=1000, description="Maximum number of notifications"),
notification_type: Optional[NotificationType] = Query(None, description="Filter by type"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Get pending notifications for processing (system/admin only)"""
if (current_user.get("type") != "service" and
current_user.get("role") not in ["admin", "system"]):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only system services or admins can access pending notifications"
)
try:
model_notification_type = None
if notification_type:
try:
model_notification_type = ModelNotificationType(notification_type.value)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid notification type: {notification_type.value}"
)
notifications = await notification_service.get_pending_notifications(
limit=limit,
notification_type=model_notification_type
)
return [NotificationResponse.from_orm(notification) for notification in notifications]
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get pending notifications",
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get pending notifications"
)
@router.post("/notifications/{notification_id}/schedule")
@track_endpoint_metrics("notification_schedule")
async def schedule_notification_enhanced(
notification_id: UUID = Path(..., description="Notification ID"),
schedule_data: Dict[str, Any] = ...,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Schedule a notification for future delivery with enhanced validation"""
try:
scheduled_at = schedule_data.get("scheduled_at")
if not scheduled_at:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="scheduled_at is required"
)
# Parse datetime if it's a string
if isinstance(scheduled_at, str):
try:
scheduled_at = datetime.fromisoformat(scheduled_at.replace('Z', '+00:00'))
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid datetime format. Use ISO format."
)
# Check that the scheduled time is in the future
if scheduled_at <= datetime.utcnow():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Scheduled time must be in the future"
)
success = await notification_service.schedule_notification(
str(notification_id),
scheduled_at
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found or cannot be scheduled"
)
return {
"success": True,
"message": "Notification scheduled successfully",
"scheduled_at": scheduled_at.isoformat()
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to schedule notification",
notification_id=str(notification_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to schedule notification"
)
@router.post("/notifications/{notification_id}/cancel")
@track_endpoint_metrics("notification_cancel")
async def cancel_notification_enhanced(
notification_id: UUID = Path(..., description="Notification ID"),
cancel_data: Optional[Dict[str, Any]] = None,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Cancel a pending notification with enhanced validation"""
try:
reason = None
if cancel_data:
reason = cancel_data.get("reason", "Cancelled by user")
else:
reason = "Cancelled by user"
success = await notification_service.cancel_notification(
str(notification_id),
reason
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found or cannot be cancelled"
)
return {
"success": True,
"message": "Notification cancelled successfully",
"reason": reason
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to cancel notification",
notification_id=str(notification_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to cancel notification"
)
@router.post("/notifications/{notification_id}/retry")
@track_endpoint_metrics("notification_retry")
async def retry_failed_notification_enhanced(
notification_id: UUID = Path(..., description="Notification ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Retry a failed notification with enhanced validation"""
# Only admins can retry notifications
if current_user.get("role") not in ["admin", "system"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can retry failed notifications"
)
try:
success = await notification_service.retry_failed_notification(str(notification_id))
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found, not failed, or max retries exceeded"
)
return {
"success": True,
"message": "Notification queued for retry"
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to retry notification",
notification_id=str(notification_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retry notification"
)
@router.get("/statistics", dependencies=[Depends(require_role(["admin", "manager"]))])
@track_endpoint_metrics("notification_get_statistics")
async def get_notification_statistics_enhanced(
tenant_id: Optional[str] = Query(None, description="Filter by tenant ID"),
days_back: int = Query(30, ge=1, le=365, description="Number of days to look back"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
notification_service: EnhancedNotificationService = Depends(get_enhanced_notification_service)
):
"""Get comprehensive notification statistics with enhanced analytics"""
try:
stats = await notification_service.get_notification_statistics(
tenant_id=tenant_id,
days_back=days_back
)
return stats
except Exception as e:
logger.error("Failed to get notification statistics",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get notification statistics"
)

View File

@@ -1,199 +0,0 @@
# services/notification/app/api/sse_routes.py
"""
SSE routes for real-time alert and recommendation streaming
"""
import asyncio
import json
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Request, Depends, HTTPException, BackgroundTasks
from sse_starlette.sse import EventSourceResponse
import structlog
from shared.auth.decorators import get_current_user
router = APIRouter(prefix="/sse", tags=["sse"])
logger = structlog.get_logger()
@router.get("/alerts/stream/{tenant_id}")
async def stream_alerts(
tenant_id: str,
request: Request,
background_tasks: BackgroundTasks,
token: Optional[str] = None
):
"""
SSE endpoint for real-time alert and recommendation streaming
Supports both alerts and recommendations through unified stream
"""
# Validate token and get user (skip for now to test connection)
# TODO: Add proper token validation in production
current_user = None
if token:
try:
# In a real implementation, validate the JWT token here
# For now, skip validation to test the connection
pass
except Exception:
raise HTTPException(401, "Invalid token")
# Skip tenant access validation for testing
# TODO: Add tenant access validation in production
# Get SSE service from app state
sse_service = getattr(request.app.state, 'sse_service', None)
if not sse_service:
raise HTTPException(500, "SSE service not available")
async def event_generator():
"""Generate SSE events for the client"""
client_queue = asyncio.Queue(maxsize=100) # Limit queue size
try:
# Register client
await sse_service.add_client(tenant_id, client_queue)
logger.info("SSE client connected",
tenant_id=tenant_id,
user_id=getattr(current_user, 'id', 'unknown'))
# Stream events
while True:
# Check if client disconnected
if await request.is_disconnected():
logger.info("SSE client disconnected", tenant_id=tenant_id)
break
try:
# Wait for events with timeout for keepalive
event = await asyncio.wait_for(
client_queue.get(),
timeout=30.0
)
yield event
except asyncio.TimeoutError:
# Send keepalive ping
yield {
"event": "ping",
"data": json.dumps({
"timestamp": datetime.utcnow().isoformat(),
"status": "keepalive"
}),
"id": f"ping_{int(datetime.now().timestamp())}"
}
except Exception as e:
logger.error("Error in SSE event generator",
tenant_id=tenant_id,
error=str(e))
break
except Exception as e:
logger.error("SSE connection error",
tenant_id=tenant_id,
error=str(e))
finally:
# Clean up on disconnect
try:
await sse_service.remove_client(tenant_id, client_queue)
logger.info("SSE client cleanup completed", tenant_id=tenant_id)
except Exception as e:
logger.error("Error cleaning up SSE client",
tenant_id=tenant_id,
error=str(e))
return EventSourceResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Disable nginx buffering
}
)
@router.post("/items/{item_id}/acknowledge")
async def acknowledge_item(
item_id: str,
current_user = Depends(get_current_user)
):
"""Acknowledge an alert or recommendation"""
try:
# This would update the database
# For now, just return success
logger.info("Item acknowledged",
item_id=item_id,
user_id=getattr(current_user, 'id', 'unknown'))
return {
"status": "success",
"item_id": item_id,
"acknowledged_by": getattr(current_user, 'id', 'unknown'),
"acknowledged_at": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error("Failed to acknowledge item", item_id=item_id, error=str(e))
raise HTTPException(500, "Failed to acknowledge item")
@router.post("/items/{item_id}/resolve")
async def resolve_item(
item_id: str,
current_user = Depends(get_current_user)
):
"""Resolve an alert or recommendation"""
try:
# This would update the database
# For now, just return success
logger.info("Item resolved",
item_id=item_id,
user_id=getattr(current_user, 'id', 'unknown'))
return {
"status": "success",
"item_id": item_id,
"resolved_by": getattr(current_user, 'id', 'unknown'),
"resolved_at": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error("Failed to resolve item", item_id=item_id, error=str(e))
raise HTTPException(500, "Failed to resolve item")
@router.get("/status/{tenant_id}")
async def get_sse_status(
tenant_id: str,
current_user = Depends(get_current_user)
):
"""Get SSE connection status for a tenant"""
# Verify user has access to this tenant
if not hasattr(current_user, 'has_access_to_tenant') or not current_user.has_access_to_tenant(tenant_id):
raise HTTPException(403, "Access denied to this tenant")
try:
# Get SSE service from app state
sse_service = getattr(request.app.state, 'sse_service', None)
if not sse_service:
return {"status": "unavailable", "message": "SSE service not initialized"}
metrics = sse_service.get_metrics()
tenant_connections = len(sse_service.active_connections.get(tenant_id, set()))
return {
"status": "available",
"tenant_id": tenant_id,
"connections": tenant_connections,
"total_connections": metrics["total_connections"],
"active_tenants": metrics["active_tenants"]
}
except Exception as e:
logger.error("Failed to get SSE status", tenant_id=tenant_id, error=str(e))
raise HTTPException(500, "Failed to get SSE status")

View File

@@ -11,7 +11,8 @@ from sqlalchemy import text
from app.core.config import settings
from app.core.database import database_manager
from app.api.notifications import router as notification_router
from app.api.sse_routes import router as sse_router
from app.api.notification_operations import router as notification_operations_router
from app.api.analytics import router as analytics_router
from app.services.messaging import setup_messaging, cleanup_messaging
from app.services.sse_service import SSEService
from app.services.notification_orchestrator import NotificationOrchestrator
@@ -141,7 +142,7 @@ class NotificationService(StandardFastAPIService):
version="2.0.0",
log_level=settings.LOG_LEVEL,
cors_origins=getattr(settings, 'CORS_ORIGINS', ["*"]),
api_prefix="/api/v1",
api_prefix="", # Empty because RouteBuilder already includes /api/v1
database_manager=database_manager,
expected_tables=notification_expected_tables,
custom_health_checks={
@@ -251,7 +252,8 @@ service.setup_custom_endpoints()
# Include routers
service.add_router(notification_router, tags=["notifications"])
service.add_router(sse_router, tags=["sse"])
service.add_router(notification_operations_router, tags=["notification-operations"])
service.add_router(analytics_router, tags=["notifications-analytics"])
if __name__ == "__main__":
import uvicorn