New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -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

View 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")

View File

@@ -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