Imporve enterprise
This commit is contained in:
314
services/inventory/app/api/enterprise_inventory.py
Normal file
314
services/inventory/app/api/enterprise_inventory.py
Normal 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)}")
|
||||
Reference in New Issue
Block a user