Imporve enterprise

This commit is contained in:
Urtzi Alfaro
2025-12-17 20:50:22 +01:00
parent e3ef47b879
commit f8591639a7
28 changed files with 6802 additions and 258 deletions

View File

@@ -0,0 +1,314 @@
"""
Enterprise Inventory API Endpoints
APIs for enterprise-level inventory management across outlets
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from typing import List, Optional
from datetime import date
from pydantic import BaseModel, Field
import structlog
from app.services.enterprise_inventory_service import EnterpriseInventoryService
from shared.auth.tenant_access import verify_tenant_permission_dep
from shared.clients import get_inventory_client, get_tenant_client
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter()
# Pydantic models for request/response
class InventoryCoverageResponse(BaseModel):
outlet_id: str = Field(..., description="Outlet tenant ID")
outlet_name: str = Field(..., description="Outlet name")
overall_coverage: float = Field(..., description="Overall inventory coverage percentage (0-100)")
critical_items_count: int = Field(..., description="Number of items at critical stock levels")
high_risk_items_count: int = Field(..., description="Number of items at high risk of stockout")
medium_risk_items_count: int = Field(..., description="Number of items at medium risk")
low_risk_items_count: int = Field(..., description="Number of items at low risk")
fulfillment_rate: float = Field(..., description="Order fulfillment rate percentage (0-100)")
last_updated: str = Field(..., description="Last inventory update timestamp")
status: str = Field(..., description="Overall status: normal, warning, critical")
class ProductCoverageDetail(BaseModel):
product_id: str = Field(..., description="Product ID")
product_name: str = Field(..., description="Product name")
current_stock: int = Field(..., description="Current stock quantity")
safety_stock: int = Field(..., description="Safety stock threshold")
coverage_percentage: float = Field(..., description="Coverage percentage (current/safety)")
risk_level: str = Field(..., description="Risk level: critical, high, medium, low")
days_until_stockout: Optional[int] = Field(None, description="Estimated days until stockout")
class OutletInventoryDetailResponse(BaseModel):
outlet_id: str = Field(..., description="Outlet tenant ID")
outlet_name: str = Field(..., description="Outlet name")
overall_coverage: float = Field(..., description="Overall inventory coverage percentage")
products: List[ProductCoverageDetail] = Field(..., description="Product-level inventory details")
last_updated: str = Field(..., description="Last update timestamp")
class NetworkInventorySummary(BaseModel):
total_outlets: int = Field(..., description="Total number of outlets")
average_coverage: float = Field(..., description="Network average inventory coverage")
average_fulfillment_rate: float = Field(..., description="Network average fulfillment rate")
critical_outlets: int = Field(..., description="Number of outlets with critical status")
warning_outlets: int = Field(..., description="Number of outlets with warning status")
normal_outlets: int = Field(..., description="Number of outlets with normal status")
total_critical_items: int = Field(..., description="Total critical items across network")
network_health_score: float = Field(..., description="Overall network health score (0-100)")
class InventoryAlert(BaseModel):
alert_id: str = Field(..., description="Alert ID")
outlet_id: str = Field(..., description="Outlet ID")
outlet_name: str = Field(..., description="Outlet name")
product_id: Optional[str] = Field(None, description="Product ID if applicable")
product_name: Optional[str] = Field(None, description="Product name if applicable")
alert_type: str = Field(..., description="Type of alert: stockout_risk, low_coverage, etc.")
severity: str = Field(..., description="Severity: critical, high, medium, low")
current_coverage: float = Field(..., description="Current inventory coverage percentage")
threshold: float = Field(..., description="Threshold that triggered alert")
timestamp: str = Field(..., description="Alert timestamp")
message: str = Field(..., description="Alert message")
async def get_enterprise_inventory_service() -> "EnterpriseInventoryService":
"""Dependency injection for EnterpriseInventoryService"""
inventory_client = get_inventory_client(settings, "inventory-service")
tenant_client = get_tenant_client(settings, "inventory-service")
return EnterpriseInventoryService(
inventory_client=inventory_client,
tenant_client=tenant_client
)
@router.get("/tenants/{parent_id}/outlets/inventory-coverage",
response_model=List[InventoryCoverageResponse],
summary="Get inventory coverage for all outlets in network")
async def get_outlet_inventory_coverage(
parent_id: str,
min_coverage: Optional[float] = Query(None, description="Filter outlets with coverage below this threshold"),
risk_level: Optional[str] = Query(None, description="Filter by risk level: critical, high, medium, low"),
enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get inventory coverage metrics for all child outlets in a parent tenant's network
This endpoint provides a comprehensive view of inventory health across all outlets,
enabling enterprise managers to identify stockout risks and prioritize inventory transfers.
"""
try:
# Verify this is a parent tenant
tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id)
if tenant_info.get('tenant_type') != 'parent':
raise HTTPException(
status_code=403,
detail="Only parent tenants can access outlet inventory coverage"
)
# Get all child outlets for this parent
child_outlets = await enterprise_inventory_service.get_child_outlets(parent_id)
if not child_outlets:
return []
# Get inventory coverage for each outlet
coverage_data = []
for outlet in child_outlets:
outlet_id = outlet['id']
# Get inventory coverage data
coverage = await enterprise_inventory_service.get_inventory_coverage(outlet_id)
if coverage:
# Apply filters if specified
if min_coverage is not None and coverage['overall_coverage'] >= min_coverage:
continue
if risk_level is not None and coverage.get('status') != risk_level:
continue
coverage_data.append(coverage)
# Sort by coverage (lowest first) to prioritize critical outlets
coverage_data.sort(key=lambda x: x['overall_coverage'])
return coverage_data
except Exception as e:
logger.error("Failed to get outlet inventory coverage", error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get inventory coverage: {str(e)}")
@router.get("/tenants/{parent_id}/outlets/inventory-summary",
response_model=NetworkInventorySummary,
summary="Get network-wide inventory summary")
async def get_network_inventory_summary(
parent_id: str,
enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get aggregated inventory summary across the entire network
Provides key metrics for network health monitoring and decision making.
"""
try:
# Verify this is a parent tenant
tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id)
if tenant_info.get('tenant_type') != 'parent':
raise HTTPException(
status_code=403,
detail="Only parent tenants can access network inventory summary"
)
return await enterprise_inventory_service.get_network_inventory_summary(parent_id)
except Exception as e:
logger.error("Failed to get network inventory summary", error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get inventory summary: {str(e)}")
@router.get("/tenants/{parent_id}/outlets/{outlet_id}/inventory-details",
response_model=OutletInventoryDetailResponse,
summary="Get detailed inventory for specific outlet")
async def get_outlet_inventory_details(
parent_id: str,
outlet_id: str,
product_id: Optional[str] = Query(None, description="Filter by specific product ID"),
risk_level: Optional[str] = Query(None, description="Filter products by risk level"),
enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get detailed product-level inventory data for a specific outlet
Enables drill-down analysis of inventory issues at the product level.
"""
try:
# Verify parent-child relationship
await enterprise_inventory_service.verify_parent_child_relationship(parent_id, outlet_id)
return await enterprise_inventory_service.get_outlet_inventory_details(outlet_id, product_id, risk_level)
except Exception as e:
logger.error("Failed to get outlet inventory details", error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get inventory details: {str(e)}")
@router.get("/tenants/{parent_id}/inventory-alerts",
response_model=List[InventoryAlert],
summary="Get real-time inventory alerts across network")
async def get_network_inventory_alerts(
parent_id: str,
severity: Optional[str] = Query(None, description="Filter by severity: critical, high, medium, low"),
alert_type: Optional[str] = Query(None, description="Filter by alert type"),
limit: int = Query(50, description="Maximum number of alerts to return"),
enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get real-time inventory alerts across all outlets
Provides actionable alerts for inventory management and stockout prevention.
"""
try:
# Verify this is a parent tenant
tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id)
if tenant_info.get('tenant_type') != 'parent':
raise HTTPException(
status_code=403,
detail="Only parent tenants can access network inventory alerts"
)
alerts = await enterprise_inventory_service.get_inventory_alerts(parent_id)
# Apply filters
if severity:
alerts = [alert for alert in alerts if alert.get('severity') == severity]
if alert_type:
alerts = [alert for alert in alerts if alert.get('alert_type') == alert_type]
# Sort by severity (critical first) and timestamp (newest first)
severity_order = {'critical': 1, 'high': 2, 'medium': 3, 'low': 4}
alerts.sort(key=lambda x: (severity_order.get(x.get('severity', 'low'), 5), -int(x.get('timestamp', 0))))
return alerts[:limit]
except Exception as e:
logger.error("Failed to get inventory alerts", error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get inventory alerts: {str(e)}")
@router.post("/tenants/{parent_id}/inventory-transfers/recommend",
summary="Get inventory transfer recommendations")
async def get_inventory_transfer_recommendations(
parent_id: str,
urgency: str = Query("medium", description="Urgency level: low, medium, high, critical"),
enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get AI-powered inventory transfer recommendations
Analyzes inventory levels across outlets and suggests optimal transfers
to prevent stockouts and balance inventory.
"""
try:
# Verify this is a parent tenant
tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id)
if tenant_info.get('tenant_type') != 'parent':
raise HTTPException(
status_code=403,
detail="Only parent tenants can request transfer recommendations"
)
recommendations = await enterprise_inventory_service.get_transfer_recommendations(parent_id, urgency)
return {
'success': True,
'recommendations': recommendations,
'message': f'Generated {len(recommendations)} transfer recommendations'
}
except Exception as e:
logger.error("Failed to get transfer recommendations", error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get recommendations: {str(e)}")
@router.get("/tenants/{parent_id}/inventory/coverage-trends",
summary="Get inventory coverage trends over time")
async def get_inventory_coverage_trends(
parent_id: str,
days: int = Query(30, description="Number of days to analyze"),
enterprise_inventory_service: EnterpriseInventoryService = Depends(get_enterprise_inventory_service),
verified_tenant: str = Depends(verify_tenant_permission_dep)
):
"""
Get historical inventory coverage trends
Enables analysis of inventory performance over time.
"""
try:
# Verify this is a parent tenant
tenant_info = await enterprise_inventory_service.tenant_client.get_tenant(parent_id)
if tenant_info.get('tenant_type') != 'parent':
raise HTTPException(
status_code=403,
detail="Only parent tenants can access coverage trends"
)
trends = await enterprise_inventory_service.get_coverage_trends(parent_id, days)
return {
'success': True,
'trends': trends,
'period': f'Last {days} days'
}
except Exception as e:
logger.error("Failed to get coverage trends", error=str(e))
raise HTTPException(status_code=500, detail=f"Failed to get coverage trends: {str(e)}")

View File

@@ -32,7 +32,8 @@ from app.api import (
analytics,
sustainability,
audit,
ml_insights
ml_insights,
enterprise_inventory
)
from app.api.internal_alert_trigger import router as internal_alert_trigger_router
from app.api.internal_demo import router as internal_demo_router
@@ -217,6 +218,7 @@ service.add_router(internal_demo.router, tags=["internal-demo"])
service.add_router(ml_insights.router) # ML insights endpoint
service.add_router(ml_insights.internal_router) # Internal ML insights endpoint for demo cloning
service.add_router(internal_alert_trigger_router) # Internal alert trigger for demo cloning
service.add_router(enterprise_inventory.router) # Enterprise inventory endpoints
if __name__ == "__main__":

View File

@@ -0,0 +1,473 @@
"""
Enterprise Inventory Service
Business logic for enterprise-level inventory management across outlets
"""
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
import uuid
import structlog
logger = structlog.get_logger()
class EnterpriseInventoryService:
"""
Service for managing inventory across enterprise networks
"""
def __init__(self, inventory_client, tenant_client):
self.inventory_client = inventory_client
self.tenant_client = tenant_client
async def get_child_outlets(self, parent_id: str) -> List[Dict[str, Any]]:
"""
Get all child outlets for a parent tenant
"""
try:
# Get child tenants from tenant service
children = await self.tenant_client.get_child_tenants(parent_id)
# Enrich with location data
enriched_outlets = []
for child in children:
# Get location data for this outlet
locations = await self.tenant_client.get_tenant_locations(child['id'])
outlet_info = {
'id': child['id'],
'name': child['name'],
'subdomain': child.get('subdomain'),
'location': locations[0] if locations else None
}
enriched_outlets.append(outlet_info)
return enriched_outlets
except Exception as e:
logger.error("Failed to get child outlets", parent_id=parent_id, error=str(e))
raise Exception(f"Failed to get child outlets: {str(e)}")
async def get_inventory_coverage(self, outlet_id: str) -> Dict[str, Any]:
"""
Get inventory coverage metrics for a specific outlet
"""
try:
# Get current inventory data
inventory_data = await self.inventory_client.get_current_inventory(outlet_id)
if not inventory_data or not inventory_data.get('items'):
return None
# Calculate coverage metrics
total_items = len(inventory_data['items'])
critical_count = 0
high_risk_count = 0
medium_risk_count = 0
low_risk_count = 0
total_coverage = 0
for item in inventory_data['items']:
current_stock = item.get('current_stock', 0)
safety_stock = item.get('safety_stock', 1) # Avoid division by zero
if safety_stock <= 0:
safety_stock = 1
coverage = min(100, (current_stock / safety_stock) * 100)
total_coverage += coverage
# Determine risk level
if coverage < 30:
critical_count += 1
elif coverage < 50:
high_risk_count += 1
elif coverage < 70:
medium_risk_count += 1
else:
low_risk_count += 1
# Calculate average coverage
avg_coverage = total_coverage / total_items if total_items > 0 else 0
# Get fulfillment rate (simplified - in real implementation this would come from orders service)
fulfillment_rate = await self._calculate_fulfillment_rate(outlet_id)
# Determine overall status
status = self._determine_inventory_status(critical_count, high_risk_count, avg_coverage)
return {
'outlet_id': outlet_id,
'outlet_name': inventory_data.get('tenant_name', f'Outlet {outlet_id}'),
'overall_coverage': round(avg_coverage, 1),
'critical_items_count': critical_count,
'high_risk_items_count': high_risk_count,
'medium_risk_items_count': medium_risk_count,
'low_risk_items_count': low_risk_count,
'fulfillment_rate': round(fulfillment_rate, 1),
'last_updated': datetime.now().isoformat(),
'status': status
}
except Exception as e:
logger.error("Failed to get inventory coverage", outlet_id=outlet_id, error=str(e))
raise Exception(f"Failed to get inventory coverage: {str(e)}")
async def _calculate_fulfillment_rate(self, outlet_id: str) -> float:
"""
Calculate fulfillment rate for an outlet (simplified)
In a real implementation, this would query the orders service
"""
# This is a placeholder - real implementation would:
# 1. Get recent orders from orders service
# 2. Calculate % successfully fulfilled
# 3. Return the rate
# For demo purposes, return a reasonable default
return 95.0
def _determine_inventory_status(self, critical_count: int, high_risk_count: int, avg_coverage: float) -> str:
"""
Determine overall inventory status based on risk factors
"""
if critical_count > 5 or (critical_count > 0 and avg_coverage < 40):
return 'critical'
elif high_risk_count > 3 or (high_risk_count > 0 and avg_coverage < 60):
return 'warning'
else:
return 'normal'
async def get_network_inventory_summary(self, parent_id: str) -> Dict[str, Any]:
"""
Get aggregated inventory summary across the entire network
"""
try:
# Get all child outlets
child_outlets = await self.get_child_outlets(parent_id)
if not child_outlets:
return {
'total_outlets': 0,
'average_coverage': 0,
'average_fulfillment_rate': 0,
'critical_outlets': 0,
'warning_outlets': 0,
'normal_outlets': 0,
'total_critical_items': 0,
'network_health_score': 0
}
# Get coverage for each outlet
coverage_data = []
for outlet in child_outlets:
coverage = await self.get_inventory_coverage(outlet['id'])
if coverage:
coverage_data.append(coverage)
if not coverage_data:
return {
'total_outlets': len(child_outlets),
'average_coverage': 0,
'average_fulfillment_rate': 0,
'critical_outlets': 0,
'warning_outlets': 0,
'normal_outlets': len(child_outlets),
'total_critical_items': 0,
'network_health_score': 0
}
# Calculate network metrics
total_coverage = sum(c['overall_coverage'] for c in coverage_data)
total_fulfillment = sum(c['fulfillment_rate'] for c in coverage_data)
avg_coverage = total_coverage / len(coverage_data)
avg_fulfillment = total_fulfillment / len(coverage_data)
critical_outlets = sum(1 for c in coverage_data if c['status'] == 'critical')
warning_outlets = sum(1 for c in coverage_data if c['status'] == 'warning')
normal_outlets = sum(1 for c in coverage_data if c['status'] == 'normal')
total_critical_items = sum(c['critical_items_count'] for c in coverage_data)
# Calculate network health score (weighted average)
network_health = round(avg_coverage * 0.6 + avg_fulfillment * 0.4, 1)
return {
'total_outlets': len(child_outlets),
'average_coverage': round(avg_coverage, 1),
'average_fulfillment_rate': round(avg_fulfillment, 1),
'critical_outlets': critical_outlets,
'warning_outlets': warning_outlets,
'normal_outlets': normal_outlets,
'total_critical_items': total_critical_items,
'network_health_score': network_health
}
except Exception as e:
logger.error("Failed to get network inventory summary", parent_id=parent_id, error=str(e))
raise Exception(f"Failed to get network inventory summary: {str(e)}")
async def get_outlet_inventory_details(self, outlet_id: str, product_id: Optional[str] = None, risk_level: Optional[str] = None) -> Dict[str, Any]:
"""
Get detailed product-level inventory data for a specific outlet
"""
try:
# Get current inventory data
inventory_data = await self.inventory_client.get_current_inventory(outlet_id)
if not inventory_data or not inventory_data.get('items'):
return {
'outlet_id': outlet_id,
'outlet_name': inventory_data.get('tenant_name', f'Outlet {outlet_id}'),
'overall_coverage': 0,
'products': [],
'last_updated': datetime.now().isoformat()
}
# Process product details
products = []
total_coverage = 0
for item in inventory_data['items']:
# Filter by product_id if specified
if product_id and item.get('product_id') != product_id:
continue
current_stock = item.get('current_stock', 0)
safety_stock = item.get('safety_stock', 1)
if safety_stock <= 0:
safety_stock = 1
coverage = min(100, (current_stock / safety_stock) * 100)
total_coverage += coverage
# Determine risk level
if coverage < 30:
risk = 'critical'
elif coverage < 50:
risk = 'high'
elif coverage < 70:
risk = 'medium'
else:
risk = 'low'
# Filter by risk level if specified
if risk_level and risk != risk_level:
continue
# Calculate days until stockout (simplified)
daily_usage = item.get('average_daily_usage', 1)
days_until_stockout = None
if daily_usage > 0:
days_until_stockout = max(0, int((current_stock - safety_stock) / daily_usage))
if days_until_stockout < 0:
days_until_stockout = 0
product_detail = {
'product_id': item.get('product_id'),
'product_name': item.get('product_name', 'Unknown Product'),
'current_stock': current_stock,
'safety_stock': safety_stock,
'coverage_percentage': round(coverage, 1),
'risk_level': risk,
'days_until_stockout': days_until_stockout
}
products.append(product_detail)
# Calculate overall coverage
avg_coverage = total_coverage / len(inventory_data['items']) if inventory_data['items'] else 0
return {
'outlet_id': outlet_id,
'outlet_name': inventory_data.get('tenant_name', f'Outlet {outlet_id}'),
'overall_coverage': round(avg_coverage, 1),
'products': products,
'last_updated': datetime.now().isoformat()
}
except Exception as e:
logger.error("Failed to get outlet inventory details", outlet_id=outlet_id, error=str(e))
raise Exception(f"Failed to get outlet inventory details: {str(e)}")
async def get_inventory_alerts(self, parent_id: str) -> List[Dict[str, Any]]:
"""
Get real-time inventory alerts across all outlets
"""
try:
# Get all child outlets
child_outlets = await self.get_child_outlets(parent_id)
alerts = []
for outlet in child_outlets:
outlet_id = outlet['id']
outlet_name = outlet['name']
# Get inventory coverage for this outlet
coverage = await self.get_inventory_coverage(outlet_id)
if coverage:
# Create alerts for critical items
if coverage['critical_items_count'] > 0:
alerts.append({
'alert_id': str(uuid.uuid4()),
'outlet_id': outlet_id,
'outlet_name': outlet_name,
'product_id': None,
'product_name': None,
'alert_type': 'low_coverage',
'severity': 'critical',
'current_coverage': coverage['overall_coverage'],
'threshold': 30,
'timestamp': datetime.now().isoformat(),
'message': f"Critical inventory coverage: {coverage['overall_coverage']}% (threshold: 30%)"
})
# Create alerts for high risk items
if coverage['high_risk_items_count'] > 0:
alerts.append({
'alert_id': str(uuid.uuid4()),
'outlet_id': outlet_id,
'outlet_name': outlet_name,
'product_id': None,
'product_name': None,
'alert_type': 'stockout_risk',
'severity': 'high',
'current_coverage': coverage['overall_coverage'],
'threshold': 50,
'timestamp': datetime.now().isoformat(),
'message': f"High stockout risk: {coverage['overall_coverage']}% coverage"
})
return alerts
except Exception as e:
logger.error("Failed to get inventory alerts", parent_id=parent_id, error=str(e))
raise Exception(f"Failed to get inventory alerts: {str(e)}")
async def get_transfer_recommendations(self, parent_id: str, urgency: str = "medium") -> List[Dict[str, Any]]:
"""
Get AI-powered inventory transfer recommendations
"""
try:
# Get inventory coverage for all outlets
child_outlets = await self.get_child_outlets(parent_id)
coverage_data = []
for outlet in child_outlets:
coverage = await self.get_inventory_coverage(outlet['id'])
if coverage:
coverage_data.append(coverage)
# Simple recommendation algorithm (in real implementation, this would be more sophisticated)
recommendations = []
# Find outlets with surplus and deficit
surplus_outlets = [c for c in coverage_data if c['overall_coverage'] > 85]
deficit_outlets = [c for c in coverage_data if c['overall_coverage'] < 60]
# Generate transfer recommendations
for deficit in deficit_outlets:
for surplus in surplus_outlets:
# Calculate transfer amount (simplified)
transfer_amount = min(10, (deficit['overall_coverage'] - 60) * -2) # Transfer 2% per missing %
if transfer_amount > 0:
recommendations.append({
'recommendation_id': str(uuid.uuid4()),
'from_outlet_id': surplus['outlet_id'],
'from_outlet_name': surplus['outlet_name'],
'to_outlet_id': deficit['outlet_id'],
'to_outlet_name': deficit['outlet_name'],
'transfer_amount': transfer_amount,
'priority': self._calculate_priority(deficit, urgency),
'reason': f"Balance inventory: {surplus['outlet_name']} has {surplus['overall_coverage']}% coverage, {deficit['outlet_name']} has {deficit['overall_coverage']}% coverage",
'estimated_impact': f"Improve {deficit['outlet_name']} coverage by ~{transfer_amount}%"
})
# Sort by priority
recommendations.sort(key=lambda x: x['priority'], reverse=True)
return recommendations
except Exception as e:
logger.error("Failed to get transfer recommendations", parent_id=parent_id, error=str(e))
raise Exception(f"Failed to get transfer recommendations: {str(e)}")
def _calculate_priority(self, deficit_coverage: Dict[str, Any], urgency: str) -> int:
"""
Calculate priority score for transfer recommendation
"""
priority_scores = {
'critical': 4,
'high': 3,
'medium': 2,
'low': 1
}
urgency_score = priority_scores.get(urgency, 2)
# Higher priority for lower coverage
coverage_score = max(1, 5 - int(deficit_coverage['overall_coverage'] / 20))
return urgency_score * coverage_score
async def get_coverage_trends(self, parent_id: str, days: int = 30) -> List[Dict[str, Any]]:
"""
Get historical inventory coverage trends
"""
try:
# In a real implementation, this would query historical data
# For demo purposes, generate some sample trend data
trends = []
end_date = datetime.now()
for i in range(days):
date = end_date - timedelta(days=i)
# Generate sample data with some variation
base_coverage = 75
variation = (i % 7) - 3 # Weekly pattern
daily_variation = (i % 3) - 1 # Daily noise
coverage = max(50, min(95, base_coverage + variation + daily_variation))
trends.append({
'date': date.strftime('%Y-%m-%d'),
'average_coverage': round(coverage, 1),
'min_coverage': max(40, coverage - 15),
'max_coverage': min(95, coverage + 10)
})
# Sort by date (oldest first)
trends.sort(key=lambda x: x['date'])
return trends
except Exception as e:
logger.error("Failed to get coverage trends", parent_id=parent_id, error=str(e))
raise Exception(f"Failed to get coverage trends: {str(e)}")
async def verify_parent_child_relationship(self, parent_id: str, child_id: str) -> bool:
"""
Verify that a child tenant belongs to a parent tenant
"""
try:
# Get child tenant info
child_info = await self.tenant_client.get_tenant(child_id)
if child_info.get('parent_tenant_id') != parent_id:
raise HTTPException(
status_code=403,
detail="Child tenant does not belong to specified parent"
)
return True
except Exception as e:
logger.error("Failed to verify parent-child relationship", parent_id=parent_id, child_id=child_id, error=str(e))
raise Exception(f"Failed to verify relationship: {str(e)}")