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

View File

@@ -95,11 +95,43 @@ service.setup_standard_endpoints()
# BUSINESS: Orchestration operations
from app.api.orchestration import router as orchestration_router
from app.api.dashboard import router as dashboard_router
from app.api.enterprise_dashboard import router as enterprise_dashboard_router
from app.api.internal import router as internal_router
service.add_router(orchestration_router)
service.add_router(dashboard_router)
service.add_router(enterprise_dashboard_router)
service.add_router(internal_router)
# Add enterprise dashboard service to dependencies
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
)
def get_enterprise_dashboard_service() -> EnterpriseDashboardService:
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
)
# INTERNAL: Service-to-service endpoints
from app.api import internal_demo
service.add_router(internal_demo.router)

View File

@@ -0,0 +1,645 @@
"""
Enterprise Dashboard Service for Orchestrator
Handles aggregated metrics and data for enterprise tier parent tenants
"""
import asyncio
from typing import Dict, Any, List
from datetime import date, datetime, timedelta
import structlog
from decimal import Decimal
# Import clients
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
from shared.clients.procurement_client import ProcurementServiceClient
logger = structlog.get_logger()
class EnterpriseDashboardService:
"""
Service for providing enterprise dashboard data for parent tenants
"""
def __init__(
self,
tenant_client: TenantServiceClient,
forecast_client: ForecastServiceClient,
production_client: ProductionServiceClient,
sales_client: SalesServiceClient,
inventory_client: InventoryServiceClient,
distribution_client: DistributionServiceClient,
procurement_client: ProcurementServiceClient
):
self.tenant_client = tenant_client
self.forecast_client = forecast_client
self.production_client = production_client
self.sales_client = sales_client
self.inventory_client = inventory_client
self.distribution_client = distribution_client
self.procurement_client = procurement_client
async def get_network_summary(
self,
parent_tenant_id: str
) -> Dict[str, Any]:
"""
Get network summary metrics for enterprise dashboard
Args:
parent_tenant_id: Parent tenant ID
Returns:
Dict with aggregated network metrics
"""
logger.info("Getting network summary for parent tenant", parent_tenant_id=parent_tenant_id)
# Get child tenants
child_tenants = await self.tenant_client.get_child_tenants(parent_tenant_id)
child_tenant_ids = [child['id'] for child in (child_tenants or [])]
# Fetch metrics in parallel
tasks = [
self._get_child_count(parent_tenant_id),
self._get_network_sales(parent_tenant_id, child_tenant_ids),
self._get_production_volume(parent_tenant_id),
self._get_pending_internal_transfers(parent_tenant_id),
self._get_active_shipments(parent_tenant_id)
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Handle results and errors
child_count = results[0] if not isinstance(results[0], Exception) else 0
network_sales = results[1] if not isinstance(results[1], Exception) else 0
production_volume = results[2] if not isinstance(results[2], Exception) else 0
pending_transfers = results[3] if not isinstance(results[3], Exception) else 0
active_shipments = results[4] if not isinstance(results[4], Exception) else 0
return {
'parent_tenant_id': parent_tenant_id,
'child_tenant_count': child_count,
'network_sales_30d': float(network_sales),
'production_volume_30d': float(production_volume),
'pending_internal_transfers_count': pending_transfers,
'active_shipments_count': active_shipments,
'last_updated': datetime.utcnow().isoformat()
}
async def _get_child_count(self, parent_tenant_id: str) -> int:
"""Get count of child tenants"""
try:
child_tenants = await self.tenant_client.get_child_tenants(parent_tenant_id)
return len(child_tenants)
except Exception as e:
logger.warning(f"Could not get child count for parent tenant {parent_tenant_id}: {e}")
return 0
async def _get_network_sales(self, parent_tenant_id: str, child_tenant_ids: List[str]) -> float:
"""Get total network sales for the last 30 days"""
try:
total_sales = Decimal("0.00")
start_date = date.today() - timedelta(days=30)
end_date = date.today()
# Include parent tenant sales
try:
parent_sales = await self.sales_client.get_sales_summary(
tenant_id=parent_tenant_id,
start_date=start_date,
end_date=end_date
)
total_sales += Decimal(str(parent_sales.get('total_revenue', 0)))
except Exception as e:
logger.warning(f"Could not get sales for parent tenant {parent_tenant_id}: {e}")
# Add child tenant sales
for child_id in child_tenant_ids:
try:
child_sales = await self.sales_client.get_sales_summary(
tenant_id=child_id,
start_date=start_date,
end_date=end_date
)
total_sales += Decimal(str(child_sales.get('total_revenue', 0)))
except Exception as e:
logger.warning(f"Could not get sales for child tenant {child_id}: {e}")
return float(total_sales)
except Exception as e:
logger.error(f"Error getting network sales: {e}")
return 0.0
async def _get_production_volume(self, parent_tenant_id: str) -> float:
"""Get total production volume for the parent tenant (central production)"""
try:
start_date = date.today() - timedelta(days=30)
end_date = date.today()
production_summary = await self.production_client.get_production_summary(
tenant_id=parent_tenant_id,
start_date=start_date,
end_date=end_date
)
# Return total production value
return float(production_summary.get('total_value', 0))
except Exception as e:
logger.warning(f"Could not get production volume for parent tenant {parent_tenant_id}: {e}")
return 0.0
async def _get_pending_internal_transfers(self, parent_tenant_id: str) -> int:
"""Get count of pending internal transfer orders from parent to children"""
try:
# Get pending internal purchase orders for parent tenant
pending_pos = await self.procurement_client.get_approved_internal_purchase_orders(
parent_tenant_id=parent_tenant_id,
status="pending" # or whatever status indicates pending delivery
)
return len(pending_pos) if pending_pos else 0
except Exception as e:
logger.warning(f"Could not get pending internal transfers for parent tenant {parent_tenant_id}: {e}")
return 0
async def _get_active_shipments(self, parent_tenant_id: str) -> int:
"""Get count of active shipments for today"""
try:
today = date.today()
shipments = await self.distribution_client.get_shipments_for_date(
parent_tenant_id,
today
)
# Filter for active shipments (not delivered/cancelled)
active_statuses = ['pending', 'in_transit', 'packed']
active_shipments = [s for s in shipments if s.get('status') in active_statuses]
return len(active_shipments)
except Exception as e:
logger.warning(f"Could not get active shipments for parent tenant {parent_tenant_id}: {e}")
return 0
async def get_children_performance(
self,
parent_tenant_id: str,
metric: str = "sales",
period_days: int = 30
) -> Dict[str, Any]:
"""
Get anonymized performance ranking of child tenants
Args:
parent_tenant_id: Parent tenant ID
metric: Metric to rank by ('sales', 'inventory_value', 'order_frequency')
period_days: Number of days to look back
Returns:
Dict with anonymized ranking data
"""
logger.info("Getting children performance",
parent_tenant_id=parent_tenant_id,
metric=metric,
period_days=period_days)
child_tenants = await self.tenant_client.get_child_tenants(parent_tenant_id)
# Gather performance data for each child
performance_data = []
for child in (child_tenants or []):
child_id = child['id']
child_name = child['name']
metric_value = 0
try:
if metric == 'sales':
start_date = date.today() - timedelta(days=period_days)
end_date = date.today()
sales_summary = await self.sales_client.get_sales_summary(
tenant_id=child_id,
start_date=start_date,
end_date=end_date
)
metric_value = float(sales_summary.get('total_revenue', 0))
elif metric == 'inventory_value':
inventory_summary = await self.inventory_client.get_inventory_summary(
tenant_id=child_id
)
metric_value = float(inventory_summary.get('total_value', 0))
elif metric == 'order_frequency':
# Count orders placed in the period
orders = await self.sales_client.get_sales_orders(
tenant_id=child_id,
start_date=start_date,
end_date=end_date
)
metric_value = len(orders) if orders else 0
except Exception as e:
logger.warning(f"Could not get performance data for child {child_id}: {e}")
continue
performance_data.append({
'tenant_id': child_id,
'original_name': child_name,
'metric_value': metric_value
})
# Sort by metric value and anonymize
performance_data.sort(key=lambda x: x['metric_value'], reverse=True)
# Anonymize data (no tenant names, just ranks)
anonymized_data = []
for rank, data in enumerate(performance_data, 1):
anonymized_data.append({
'rank': rank,
'tenant_id': data['tenant_id'],
'anonymized_name': f"Outlet {rank}",
'metric_value': data['metric_value']
})
return {
'parent_tenant_id': parent_tenant_id,
'metric': metric,
'period_days': period_days,
'rankings': anonymized_data,
'total_children': len(performance_data),
'last_updated': datetime.utcnow().isoformat()
}
async def get_distribution_overview(
self,
parent_tenant_id: str,
target_date: date = None
) -> Dict[str, Any]:
"""
Get distribution overview for enterprise dashboard
Args:
parent_tenant_id: Parent tenant ID
target_date: Date to get distribution data for (default: today)
Returns:
Dict with distribution metrics and route information
"""
if target_date is None:
target_date = date.today()
logger.info("Getting distribution overview",
parent_tenant_id=parent_tenant_id,
date=target_date)
try:
# Get all routes for the target date
routes = await self.distribution_client.get_delivery_routes(
parent_tenant_id=parent_tenant_id,
date_from=target_date,
date_to=target_date
)
# Get all shipments for the target date
shipments = await self.distribution_client.get_shipments_for_date(
parent_tenant_id,
target_date
)
# Aggregate by status
status_counts = {}
for shipment in shipments:
status = shipment.get('status', 'unknown')
status_counts[status] = status_counts.get(status, 0) + 1
# Prepare route sequences for map visualization
route_sequences = []
for route in routes:
route_sequences.append({
'route_id': route.get('id'),
'route_number': route.get('route_number'),
'status': route.get('status', 'unknown'),
'total_distance_km': route.get('total_distance_km', 0),
'stops': route.get('route_sequence', []),
'estimated_duration_minutes': route.get('estimated_duration_minutes', 0)
})
return {
'parent_tenant_id': parent_tenant_id,
'target_date': target_date.isoformat(),
'route_count': len(routes),
'shipment_count': len(shipments),
'status_counts': status_counts,
'route_sequences': route_sequences,
'last_updated': datetime.utcnow().isoformat()
}
except Exception as e:
logger.error(f"Error getting distribution overview: {e}", exc_info=True)
return {
'parent_tenant_id': parent_tenant_id,
'target_date': target_date.isoformat(),
'route_count': 0,
'shipment_count': 0,
'status_counts': {},
'route_sequences': [],
'last_updated': datetime.utcnow().isoformat(),
'error': str(e)
}
async def get_enterprise_forecast_summary(
self,
parent_tenant_id: str,
days_ahead: int = 7
) -> Dict[str, Any]:
"""
Get aggregated forecast summary for the enterprise network
Args:
parent_tenant_id: Parent tenant ID
days_ahead: Number of days ahead to forecast
Returns:
Dict with aggregated forecast data
"""
try:
end_date = date.today() + timedelta(days=days_ahead)
start_date = date.today()
# Get aggregated forecast from the forecasting service
forecast_data = await self.forecast_client.get_aggregated_forecast(
parent_tenant_id=parent_tenant_id,
start_date=start_date,
end_date=end_date
)
# Aggregate the forecast data for the summary
total_demand = 0
daily_summary = {}
for forecast_date_str, products in forecast_data.get('aggregated_forecasts', {}).items():
day_total = sum(item.get('predicted_demand', 0) for item in products.values())
total_demand += day_total
daily_summary[forecast_date_str] = {
'predicted_demand': day_total,
'product_count': len(products)
}
return {
'parent_tenant_id': parent_tenant_id,
'days_forecast': days_ahead,
'total_predicted_demand': total_demand,
'daily_summary': daily_summary,
'last_updated': datetime.utcnow().isoformat()
}
except Exception as e:
logger.error(f"Error getting enterprise forecast summary: {e}", exc_info=True)
return {
'parent_tenant_id': parent_tenant_id,
'days_forecast': days_ahead,
'total_predicted_demand': 0,
'daily_summary': {},
'last_updated': datetime.utcnow().isoformat(),
'error': str(e)
}
async def get_network_performance_metrics(
self,
parent_tenant_id: str,
start_date: date,
end_date: date
) -> Dict[str, Any]:
"""
Get aggregated performance metrics across the enterprise network
Args:
parent_tenant_id: Parent tenant ID
start_date: Start date for metrics
end_date: End date for metrics
Returns:
Dict with aggregated network metrics
"""
try:
# Get all child tenants
child_tenants = await self.tenant_client.get_child_tenants(parent_tenant_id)
child_tenant_ids = [child['id'] for child in (child_tenants or [])]
# Include parent in tenant list for complete network metrics
all_tenant_ids = [parent_tenant_id] + child_tenant_ids
# Parallel fetch of metrics for all tenants
tasks = []
for tenant_id in all_tenant_ids:
# Create individual tasks for each metric
sales_task = self._get_tenant_sales(tenant_id, start_date, end_date)
production_task = self._get_tenant_production(tenant_id, start_date, end_date)
inventory_task = self._get_tenant_inventory(tenant_id)
# Gather all tasks for this tenant
tenant_tasks = asyncio.gather(sales_task, production_task, inventory_task, return_exceptions=True)
tasks.append(tenant_tasks)
results = await asyncio.gather(*tasks, return_exceptions=True)
# Aggregate metrics
total_network_sales = Decimal("0.00")
total_network_production = Decimal("0.00")
total_network_inventory_value = Decimal("0.00")
metrics_error_count = 0
for i, result in enumerate(results):
if isinstance(result, Exception):
logger.error(f"Error getting metrics for tenant {all_tenant_ids[i]}: {result}")
metrics_error_count += 1
continue
if isinstance(result, list) and len(result) == 3:
sales, production, inventory = result
total_network_sales += Decimal(str(sales or 0))
total_network_production += Decimal(str(production or 0))
total_network_inventory_value += Decimal(str(inventory or 0))
return {
'parent_tenant_id': parent_tenant_id,
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat(),
'total_network_sales': float(total_network_sales),
'total_network_production': float(total_network_production),
'total_network_inventory_value': float(total_network_inventory_value),
'included_tenant_count': len(all_tenant_ids),
'child_tenant_count': len(child_tenant_ids),
'metrics_error_count': metrics_error_count,
'coverage_percentage': (
(len(all_tenant_ids) - metrics_error_count) / len(all_tenant_ids) * 100
if all_tenant_ids else 0
)
}
except Exception as e:
logger.error(f"Error getting network performance metrics: {e}", exc_info=True)
raise
async def _get_tenant_sales(self, tenant_id: str, start_date: date, end_date: date) -> float:
"""Helper to get sales for a specific tenant"""
try:
sales_data = await self.sales_client.get_sales_summary(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
return float(sales_data.get('total_revenue', 0))
except Exception as e:
logger.warning(f"Could not get sales for tenant {tenant_id}: {e}")
return 0
async def _get_tenant_production(self, tenant_id: str, start_date: date, end_date: date) -> float:
"""Helper to get production for a specific tenant"""
try:
production_data = await self.production_client.get_production_summary(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
return float(production_data.get('total_value', 0))
except Exception as e:
logger.warning(f"Could not get production for tenant {tenant_id}: {e}")
return 0
async def _get_tenant_inventory(self, tenant_id: str) -> float:
"""Helper to get inventory value for a specific tenant"""
try:
inventory_data = await self.inventory_client.get_inventory_summary(tenant_id=tenant_id)
return float(inventory_data.get('total_value', 0))
except Exception as e:
logger.warning(f"Could not get inventory for tenant {tenant_id}: {e}")
return 0
async def initialize_enterprise_demo(
self,
parent_tenant_id: str,
child_tenant_ids: List[str],
session_id: str
) -> Dict[str, Any]:
"""
Initialize enterprise demo data including parent-child relationships and distribution setup
Args:
parent_tenant_id: Parent tenant ID
child_tenant_ids: List of child tenant IDs
session_id: Demo session ID
Returns:
Dict with initialization results
"""
logger.info("Initializing enterprise demo",
parent_tenant_id=parent_tenant_id,
child_tenant_ids=child_tenant_ids)
try:
# Step 1: Set up parent-child tenant relationships
await self._setup_parent_child_relationships(
parent_tenant_id=parent_tenant_id,
child_tenant_ids=child_tenant_ids
)
# Step 2: Initialize distribution for the parent
await self._setup_distribution_for_enterprise(
parent_tenant_id=parent_tenant_id,
child_tenant_ids=child_tenant_ids
)
# Step 3: Generate initial internal transfer orders
await self._generate_initial_internal_transfers(
parent_tenant_id=parent_tenant_id,
child_tenant_ids=child_tenant_ids
)
logger.info("Enterprise demo initialized successfully",
parent_tenant_id=parent_tenant_id)
return {
'status': 'success',
'parent_tenant_id': parent_tenant_id,
'child_tenant_count': len(child_tenant_ids),
'session_id': session_id,
'initialized_at': datetime.utcnow().isoformat()
}
except Exception as e:
logger.error(f"Error initializing enterprise demo: {e}", exc_info=True)
raise
async def _setup_parent_child_relationships(
self,
parent_tenant_id: str,
child_tenant_ids: List[str]
):
"""Set up parent-child tenant relationships"""
try:
for child_id in child_tenant_ids:
# Update child tenant to have parent reference
await self.tenant_client.update_tenant(
tenant_id=child_id,
updates={
'parent_tenant_id': parent_tenant_id,
'tenant_type': 'child',
'hierarchy_path': f"{parent_tenant_id}.{child_id}"
}
)
# Update parent tenant
await self.tenant_client.update_tenant(
tenant_id=parent_tenant_id,
updates={
'tenant_type': 'parent',
'hierarchy_path': parent_tenant_id # Root path
}
)
logger.info("Parent-child relationships established",
parent_tenant_id=parent_tenant_id,
child_count=len(child_tenant_ids))
except Exception as e:
logger.error(f"Error setting up parent-child relationships: {e}", exc_info=True)
raise
async def _setup_distribution_for_enterprise(
self,
parent_tenant_id: str,
child_tenant_ids: List[str]
):
"""Set up distribution routes and schedules for the enterprise network"""
try:
# In a real implementation, this would call the distribution service
# to set up default delivery routes and schedules between parent and children
logger.info("Setting up distribution for enterprise network",
parent_tenant_id=parent_tenant_id,
child_count=len(child_tenant_ids))
except Exception as e:
logger.error(f"Error setting up distribution: {e}", exc_info=True)
raise
async def _generate_initial_internal_transfers(
self,
parent_tenant_id: str,
child_tenant_ids: List[str]
):
"""Generate initial internal transfer orders for demo"""
try:
for child_id in child_tenant_ids:
# Generate initial internal purchase orders from parent to child
# This would typically be done through the procurement service
logger.info("Generated initial internal transfer order",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_id)
except Exception as e:
logger.error(f"Error generating initial internal transfers: {e}", exc_info=True)
raise