New alert service
This commit is contained in:
@@ -11,6 +11,7 @@ from typing import Optional
|
||||
import uuid
|
||||
import httpx
|
||||
import structlog
|
||||
import json
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -40,19 +41,21 @@ DEMO_ALLOWED_OPERATIONS = {
|
||||
|
||||
# Limited write operations for realistic testing
|
||||
"POST": [
|
||||
"/api/pos/sales",
|
||||
"/api/pos/sessions",
|
||||
"/api/orders",
|
||||
"/api/inventory/adjustments",
|
||||
"/api/sales",
|
||||
"/api/production/batches",
|
||||
"/api/v1/pos/sales",
|
||||
"/api/v1/pos/sessions",
|
||||
"/api/v1/orders",
|
||||
"/api/v1/inventory/adjustments",
|
||||
"/api/v1/sales",
|
||||
"/api/v1/production/batches",
|
||||
"/api/v1/tenants/batch/sales-summary",
|
||||
"/api/v1/tenants/batch/production-summary",
|
||||
# Note: Forecast generation is explicitly blocked (see DEMO_BLOCKED_PATHS)
|
||||
],
|
||||
|
||||
"PUT": [
|
||||
"/api/pos/sales/*",
|
||||
"/api/orders/*",
|
||||
"/api/inventory/stock/*",
|
||||
"/api/v1/pos/sales/*",
|
||||
"/api/v1/orders/*",
|
||||
"/api/v1/inventory/stock/*",
|
||||
],
|
||||
|
||||
# Blocked operations
|
||||
@@ -63,9 +66,9 @@ DEMO_ALLOWED_OPERATIONS = {
|
||||
# Explicitly blocked paths for demo accounts (even if method would be allowed)
|
||||
# These require trained AI models which demo tenants don't have
|
||||
DEMO_BLOCKED_PATHS = [
|
||||
"/api/forecasts/single",
|
||||
"/api/forecasts/multi-day",
|
||||
"/api/forecasts/batch",
|
||||
"/api/v1/forecasts/single",
|
||||
"/api/v1/forecasts/multi-day",
|
||||
"/api/v1/forecasts/batch",
|
||||
]
|
||||
|
||||
DEMO_BLOCKED_PATH_MESSAGE = {
|
||||
@@ -79,11 +82,59 @@ DEMO_BLOCKED_PATH_MESSAGE = {
|
||||
|
||||
|
||||
class DemoMiddleware(BaseHTTPMiddleware):
|
||||
"""Middleware to handle demo session logic"""
|
||||
"""Middleware to handle demo session logic with Redis caching"""
|
||||
|
||||
def __init__(self, app, demo_session_url: str = "http://demo-session-service:8000"):
|
||||
super().__init__(app)
|
||||
self.demo_session_url = demo_session_url
|
||||
self._redis_client = None
|
||||
|
||||
async def _get_redis_client(self):
|
||||
"""Get or lazily initialize Redis client"""
|
||||
if self._redis_client is None:
|
||||
try:
|
||||
from shared.redis_utils import get_redis_client
|
||||
self._redis_client = await get_redis_client()
|
||||
logger.debug("Demo middleware: Redis client initialized")
|
||||
except Exception as e:
|
||||
logger.warning(f"Demo middleware: Failed to get Redis client: {e}. Caching disabled.")
|
||||
self._redis_client = False # Sentinel value to avoid retrying
|
||||
|
||||
return self._redis_client if self._redis_client is not False else None
|
||||
|
||||
async def _get_cached_session(self, session_id: str) -> Optional[dict]:
|
||||
"""Get session info from Redis cache"""
|
||||
try:
|
||||
redis_client = await self._get_redis_client()
|
||||
if not redis_client:
|
||||
return None
|
||||
|
||||
cache_key = f"demo_session:{session_id}"
|
||||
cached_data = await redis_client.get(cache_key)
|
||||
|
||||
if cached_data:
|
||||
logger.debug("Demo middleware: Cache HIT", session_id=session_id)
|
||||
return json.loads(cached_data)
|
||||
else:
|
||||
logger.debug("Demo middleware: Cache MISS", session_id=session_id)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Demo middleware: Redis cache read error: {e}")
|
||||
return None
|
||||
|
||||
async def _cache_session(self, session_id: str, session_info: dict, ttl: int = 30):
|
||||
"""Cache session info in Redis with TTL"""
|
||||
try:
|
||||
redis_client = await self._get_redis_client()
|
||||
if not redis_client:
|
||||
return
|
||||
|
||||
cache_key = f"demo_session:{session_id}"
|
||||
serialized = json.dumps(session_info)
|
||||
await redis_client.setex(cache_key, ttl, serialized)
|
||||
logger.debug(f"Demo middleware: Cached session {session_id} (TTL: {ttl}s)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Demo middleware: Redis cache write error: {e}")
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
"""Process request through demo middleware"""
|
||||
@@ -113,8 +164,17 @@ class DemoMiddleware(BaseHTTPMiddleware):
|
||||
# Check if this is a demo session request
|
||||
if session_id:
|
||||
try:
|
||||
# Get session info from demo service
|
||||
session_info = await self._get_session_info(session_id)
|
||||
# PERFORMANCE OPTIMIZATION: Check Redis cache first before HTTP call
|
||||
session_info = await self._get_cached_session(session_id)
|
||||
|
||||
if not session_info:
|
||||
# Cache miss - fetch from demo service
|
||||
logger.debug("Demo middleware: Fetching from demo service", session_id=session_id)
|
||||
session_info = await self._get_session_info(session_id)
|
||||
|
||||
# Cache the result if successful (30s TTL to balance freshness vs performance)
|
||||
if session_info:
|
||||
await self._cache_session(session_id, session_info, ttl=30)
|
||||
|
||||
# Accept pending, ready, partial, failed (if data exists), and active (deprecated) statuses
|
||||
# Even "failed" sessions can be usable if some services succeeded
|
||||
|
||||
@@ -43,6 +43,23 @@ async def get_tenant_hierarchy(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get tenant hierarchy information"""
|
||||
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/hierarchy")
|
||||
|
||||
@router.api_route("/{tenant_id}/children", methods=["GET", "OPTIONS"])
|
||||
async def get_tenant_children(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get tenant children"""
|
||||
return await _proxy_to_tenant_service(request, f"/api/v1/tenants/{tenant_id}/children")
|
||||
|
||||
@router.api_route("/{tenant_id}/children/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_tenant_children(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy tenant children requests to tenant service"""
|
||||
target_path = f"/api/v1/tenants/{tenant_id}/children/{path}".rstrip("/")
|
||||
return await _proxy_to_tenant_service(request, target_path)
|
||||
|
||||
@router.api_route("/{tenant_id}/access/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_tenant_access(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy tenant access requests to tenant service"""
|
||||
target_path = f"/api/v1/tenants/{tenant_id}/access/{path}".rstrip("/")
|
||||
return await _proxy_to_tenant_service(request, target_path)
|
||||
|
||||
@router.get("/{tenant_id}/my-access")
|
||||
async def get_tenant_my_access(request: Request, tenant_id: str = Path(...)):
|
||||
"""Get current user's access level for a tenant"""
|
||||
@@ -108,6 +125,70 @@ async def proxy_available_plans(request: Request):
|
||||
target_path = "/api/v1/plans/available"
|
||||
return await _proxy_to_tenant_service(request, target_path)
|
||||
|
||||
# ================================================================
|
||||
# BATCH OPERATIONS ENDPOINTS
|
||||
# IMPORTANT: Route order matters! Keep specific routes before wildcards:
|
||||
# 1. Exact matches first (/batch/sales-summary)
|
||||
# 2. Wildcard paths second (/batch{path:path})
|
||||
# 3. Tenant-scoped wildcards last (/{tenant_id}/batch{path:path})
|
||||
# ================================================================
|
||||
|
||||
@router.api_route("/batch/sales-summary", methods=["POST"])
|
||||
async def proxy_batch_sales_summary(request: Request):
|
||||
"""Proxy batch sales summary request to sales service"""
|
||||
target_path = "/api/v1/batch/sales-summary"
|
||||
return await _proxy_to_sales_service(request, target_path)
|
||||
|
||||
|
||||
@router.api_route("/batch/production-summary", methods=["POST"])
|
||||
async def proxy_batch_production_summary(request: Request):
|
||||
"""Proxy batch production summary request to production service"""
|
||||
target_path = "/api/v1/batch/production-summary"
|
||||
return await _proxy_to_production_service(request, target_path)
|
||||
|
||||
|
||||
@router.api_route("/batch{path:path}", methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_batch_operations(request: Request, path: str = ""):
|
||||
"""Proxy batch operations that span multiple tenants to appropriate services"""
|
||||
# For batch operations, route based on the path after /batch/
|
||||
if path.startswith("/sales-summary"):
|
||||
# Route batch sales summary to sales service
|
||||
# The sales service batch endpoints are at /api/v1/batch/... not /api/v1/sales/batch/...
|
||||
target_path = f"/api/v1/batch{path}"
|
||||
return await _proxy_to_sales_service(request, target_path)
|
||||
elif path.startswith("/production-summary"):
|
||||
# Route batch production summary to production service
|
||||
# The production service batch endpoints are at /api/v1/batch/... not /api/v1/production/batch/...
|
||||
target_path = f"/api/v1/batch{path}"
|
||||
return await _proxy_to_production_service(request, target_path)
|
||||
else:
|
||||
# Default to sales service for other batch operations
|
||||
# The service batch endpoints are at /api/v1/batch/... not /api/v1/sales/batch/...
|
||||
target_path = f"/api/v1/batch{path}"
|
||||
return await _proxy_to_sales_service(request, target_path)
|
||||
|
||||
|
||||
@router.api_route("/{tenant_id}/batch{path:path}", methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_tenant_batch_operations(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy tenant-scoped batch operations to appropriate services"""
|
||||
# For tenant-scoped batch operations, route based on the path after /batch/
|
||||
if path.startswith("/sales-summary"):
|
||||
# Route tenant batch sales summary to sales service
|
||||
# The sales service batch endpoints are at /api/v1/batch/... not /api/v1/sales/batch/...
|
||||
target_path = f"/api/v1/batch{path}"
|
||||
return await _proxy_to_sales_service(request, target_path)
|
||||
elif path.startswith("/production-summary"):
|
||||
# Route tenant batch production summary to production service
|
||||
# The production service batch endpoints are at /api/v1/batch/... not /api/v1/production/batch/...
|
||||
target_path = f"/api/v1/batch{path}"
|
||||
return await _proxy_to_production_service(request, target_path)
|
||||
else:
|
||||
# Default to sales service for other tenant batch operations
|
||||
# The service batch endpoints are at /api/v1/batch/... not /api/v1/sales/batch/...
|
||||
target_path = f"/api/v1/batch{path}"
|
||||
return await _proxy_to_sales_service(request, target_path)
|
||||
|
||||
|
||||
# ================================================================
|
||||
# TENANT-SCOPED DATA SERVICE ENDPOINTS
|
||||
# ================================================================
|
||||
@@ -116,7 +197,7 @@ async def proxy_available_plans(request: Request):
|
||||
async def proxy_all_tenant_sales_alternative(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy all tenant sales requests - handles both base and sub-paths"""
|
||||
base_path = f"/api/v1/tenants/{tenant_id}/sales"
|
||||
|
||||
|
||||
# If path is empty or just "/", use base path
|
||||
if not path or path == "/" or path == "":
|
||||
target_path = base_path
|
||||
@@ -125,9 +206,17 @@ async def proxy_all_tenant_sales_alternative(request: Request, tenant_id: str =
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
target_path = base_path + path
|
||||
|
||||
|
||||
return await _proxy_to_sales_service(request, target_path)
|
||||
|
||||
|
||||
@router.api_route("/{tenant_id}/enterprise/batch{path:path}", methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_tenant_enterprise_batch(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy enterprise batch requests (spanning multiple tenants within an enterprise) to appropriate services"""
|
||||
# Forward to orchestrator service for enterprise-level operations
|
||||
target_path = f"/api/v1/tenants/{tenant_id}/enterprise/batch{path}".rstrip("/")
|
||||
return await _proxy_to_orchestrator_service(request, target_path, tenant_id=tenant_id)
|
||||
|
||||
@router.api_route("/{tenant_id}/weather/{path:path}", methods=["GET", "POST", "OPTIONS"])
|
||||
async def proxy_tenant_weather(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy tenant weather requests to external service"""
|
||||
@@ -226,6 +315,12 @@ async def proxy_tenant_forecasting(request: Request, tenant_id: str = Path(...),
|
||||
target_path = f"/api/v1/tenants/{tenant_id}/forecasting/{path}".rstrip("/")
|
||||
return await _proxy_to_forecasting_service(request, target_path, tenant_id=tenant_id)
|
||||
|
||||
@router.api_route("/{tenant_id}/forecasting/enterprise/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_tenant_forecasting_enterprise(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy tenant forecasting enterprise requests to forecasting service"""
|
||||
target_path = f"/api/v1/tenants/{tenant_id}/forecasting/enterprise/{path}".rstrip("/")
|
||||
return await _proxy_to_forecasting_service(request, target_path, tenant_id=tenant_id)
|
||||
|
||||
@router.api_route("/{tenant_id}/forecasts/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
||||
async def proxy_tenant_forecasts(request: Request, tenant_id: str = Path(...), path: str = ""):
|
||||
"""Proxy tenant forecast requests to forecasting service"""
|
||||
|
||||
Reference in New Issue
Block a user