New enterprise feature
This commit is contained in:
@@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import structlog
|
||||
import asyncio
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -27,7 +27,7 @@ from shared.clients import (
|
||||
)
|
||||
from shared.clients.procurement_client import ProcurementServiceClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Initialize service clients
|
||||
inventory_client = get_inventory_client(settings, "orchestrator")
|
||||
@@ -598,10 +598,39 @@ async def get_execution_progress(
|
||||
|
||||
async def fetch_pending_approvals():
|
||||
try:
|
||||
po_data = await procurement_client.get_pending_purchase_orders(tenant_id, limit=100) or []
|
||||
return len(po_data) if isinstance(po_data, list) else 0
|
||||
po_data = await procurement_client.get_pending_purchase_orders(tenant_id, limit=100)
|
||||
|
||||
if po_data is None:
|
||||
logger.error(
|
||||
"Procurement client returned None for pending POs",
|
||||
tenant_id=tenant_id,
|
||||
context="likely HTTP 404 error - check URL construction"
|
||||
)
|
||||
return 0
|
||||
|
||||
if not isinstance(po_data, list):
|
||||
logger.error(
|
||||
"Unexpected response format from procurement client",
|
||||
tenant_id=tenant_id,
|
||||
response_type=type(po_data).__name__,
|
||||
response_value=str(po_data)[:200]
|
||||
)
|
||||
return 0
|
||||
|
||||
logger.info(
|
||||
"Successfully fetched pending purchase orders",
|
||||
tenant_id=tenant_id,
|
||||
count=len(po_data)
|
||||
)
|
||||
return len(po_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch pending approvals: {e}")
|
||||
logger.error(
|
||||
"Exception while fetching pending approvals",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
return 0
|
||||
|
||||
# Execute in parallel
|
||||
|
||||
201
services/orchestrator/app/api/enterprise_dashboard.py
Normal file
201
services/orchestrator/app/api/enterprise_dashboard.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Enterprise Dashboard API Endpoints for Orchestrator Service
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import date
|
||||
import structlog
|
||||
|
||||
from app.services.enterprise_dashboard_service import EnterpriseDashboardService
|
||||
from shared.auth.tenant_access import verify_tenant_access_dep
|
||||
from shared.clients.tenant_client import TenantServiceClient
|
||||
from shared.clients.forecast_client import ForecastServiceClient
|
||||
from shared.clients.production_client import ProductionServiceClient
|
||||
from shared.clients.sales_client import SalesServiceClient
|
||||
from shared.clients.inventory_client import InventoryServiceClient
|
||||
from shared.clients.distribution_client import DistributionServiceClient
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/api/v1/tenants/{tenant_id}/enterprise", tags=["enterprise"])
|
||||
|
||||
|
||||
# Add dependency injection function
|
||||
from app.services.enterprise_dashboard_service import EnterpriseDashboardService
|
||||
from shared.clients import (
|
||||
get_tenant_client,
|
||||
get_forecast_client,
|
||||
get_production_client,
|
||||
get_sales_client,
|
||||
get_inventory_client,
|
||||
get_procurement_client
|
||||
)
|
||||
# TODO: Add distribution client when available
|
||||
# from shared.clients import get_distribution_client
|
||||
|
||||
def get_enterprise_dashboard_service() -> EnterpriseDashboardService:
|
||||
from app.core.config import settings
|
||||
tenant_client = get_tenant_client(settings)
|
||||
forecast_client = get_forecast_client(settings)
|
||||
production_client = get_production_client(settings)
|
||||
sales_client = get_sales_client(settings)
|
||||
inventory_client = get_inventory_client(settings)
|
||||
distribution_client = None # TODO: Add when distribution service is ready
|
||||
procurement_client = get_procurement_client(settings)
|
||||
|
||||
return EnterpriseDashboardService(
|
||||
tenant_client=tenant_client,
|
||||
forecast_client=forecast_client,
|
||||
production_client=production_client,
|
||||
sales_client=sales_client,
|
||||
inventory_client=inventory_client,
|
||||
distribution_client=distribution_client,
|
||||
procurement_client=procurement_client
|
||||
)
|
||||
|
||||
@router.get("/network-summary")
|
||||
async def get_network_summary(
|
||||
tenant_id: str,
|
||||
enterprise_service: EnterpriseDashboardService = Depends(get_enterprise_dashboard_service),
|
||||
verified_tenant: str = Depends(verify_tenant_access_dep)
|
||||
):
|
||||
"""
|
||||
Get network summary metrics for enterprise dashboard
|
||||
"""
|
||||
try:
|
||||
# Verify user has network access
|
||||
tenant_info = await enterprise_service.tenant_client.get_tenant(tenant_id)
|
||||
if not tenant_info:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
if tenant_info.get('tenant_type') != 'parent':
|
||||
raise HTTPException(status_code=403, detail="Only parent tenants can access enterprise dashboard")
|
||||
|
||||
result = await enterprise_service.get_network_summary(parent_tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting network summary: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get network summary")
|
||||
|
||||
|
||||
@router.get("/children-performance")
|
||||
async def get_children_performance(
|
||||
tenant_id: str,
|
||||
metric: str = "sales",
|
||||
period_days: int = 30,
|
||||
enterprise_service: EnterpriseDashboardService = Depends(get_enterprise_dashboard_service),
|
||||
verified_tenant: str = Depends(verify_tenant_access_dep)
|
||||
):
|
||||
"""
|
||||
Get anonymized performance ranking of child tenants
|
||||
"""
|
||||
try:
|
||||
# Verify user has network access
|
||||
tenant_info = await enterprise_service.tenant_client.get_tenant(tenant_id)
|
||||
if not tenant_info:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
if tenant_info.get('tenant_type') != 'parent':
|
||||
raise HTTPException(status_code=403, detail="Only parent tenants can access enterprise dashboard")
|
||||
|
||||
result = await enterprise_service.get_children_performance(
|
||||
parent_tenant_id=tenant_id,
|
||||
metric=metric,
|
||||
period_days=period_days
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting children performance: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get children performance")
|
||||
|
||||
|
||||
@router.get("/distribution-overview")
|
||||
async def get_distribution_overview(
|
||||
tenant_id: str,
|
||||
target_date: Optional[date] = None,
|
||||
enterprise_service: EnterpriseDashboardService = Depends(get_enterprise_dashboard_service),
|
||||
verified_tenant: str = Depends(verify_tenant_access_dep)
|
||||
):
|
||||
"""
|
||||
Get distribution overview for enterprise dashboard
|
||||
"""
|
||||
try:
|
||||
# Verify user has network access
|
||||
tenant_info = await enterprise_service.tenant_client.get_tenant(tenant_id)
|
||||
if not tenant_info:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
if tenant_info.get('tenant_type') != 'parent':
|
||||
raise HTTPException(status_code=403, detail="Only parent tenants can access enterprise dashboard")
|
||||
|
||||
if target_date is None:
|
||||
target_date = date.today()
|
||||
|
||||
result = await enterprise_service.get_distribution_overview(
|
||||
parent_tenant_id=tenant_id,
|
||||
target_date=target_date
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting distribution overview: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get distribution overview")
|
||||
|
||||
|
||||
@router.get("/forecast-summary")
|
||||
async def get_enterprise_forecast_summary(
|
||||
tenant_id: str,
|
||||
days_ahead: int = 7,
|
||||
enterprise_service: EnterpriseDashboardService = Depends(get_enterprise_dashboard_service),
|
||||
verified_tenant: str = Depends(verify_tenant_access_dep)
|
||||
):
|
||||
"""
|
||||
Get aggregated forecast summary for the enterprise network
|
||||
"""
|
||||
try:
|
||||
# Verify user has network access
|
||||
tenant_info = await enterprise_service.tenant_client.get_tenant(tenant_id)
|
||||
if not tenant_info:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
if tenant_info.get('tenant_type') != 'parent':
|
||||
raise HTTPException(status_code=403, detail="Only parent tenants can access enterprise dashboard")
|
||||
|
||||
result = await enterprise_service.get_enterprise_forecast_summary(
|
||||
parent_tenant_id=tenant_id,
|
||||
days_ahead=days_ahead
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting enterprise forecast summary: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get enterprise forecast summary")
|
||||
|
||||
|
||||
@router.get("/network-performance")
|
||||
async def get_network_performance_metrics(
|
||||
tenant_id: str,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
enterprise_service: EnterpriseDashboardService = Depends(get_enterprise_dashboard_service),
|
||||
verified_tenant: str = Depends(verify_tenant_access_dep)
|
||||
):
|
||||
"""
|
||||
Get aggregated performance metrics across the tenant network
|
||||
"""
|
||||
try:
|
||||
# Verify user has network access
|
||||
tenant_info = await enterprise_service.tenant_client.get_tenant(tenant_id)
|
||||
if not tenant_info:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
if tenant_info.get('tenant_type') != 'parent':
|
||||
raise HTTPException(status_code=403, detail="Only parent tenants can access enterprise dashboard")
|
||||
|
||||
if not start_date:
|
||||
start_date = date.today()
|
||||
if not end_date:
|
||||
end_date = date.today()
|
||||
|
||||
result = await enterprise_service.get_network_performance_metrics(
|
||||
parent_tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting network performance metrics: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get network performance metrics")
|
||||
@@ -23,12 +23,11 @@ from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Internal API key for service-to-service communication
|
||||
INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production")
|
||||
|
||||
|
||||
async def ensure_unique_run_number(db: AsyncSession, base_run_number: str) -> str:
|
||||
"""Ensure the run number is unique by appending a suffix if needed"""
|
||||
@@ -53,7 +52,7 @@ async def ensure_unique_run_number(db: AsyncSession, base_run_number: str) -> st
|
||||
|
||||
def verify_internal_api_key(x_internal_api_key: str = Header(...)):
|
||||
"""Verify internal API key for service-to-service communication"""
|
||||
if x_internal_api_key != INTERNAL_API_KEY:
|
||||
if x_internal_api_key != settings.INTERNAL_API_KEY:
|
||||
raise HTTPException(status_code=403, detail="Invalid internal API key")
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user