REFACTOR ALL APIs
This commit is contained in:
338
shared/auth/access_control.py
Normal file
338
shared/auth/access_control.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Subscription Tier and Role-Based Access Control Decorators
|
||||
Provides unified access control across all microservices
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
from typing import List, Callable, Dict, Any, Optional
|
||||
from fastapi import HTTPException, status, Request, Depends
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class SubscriptionTier(Enum):
|
||||
"""
|
||||
Subscription tier hierarchy
|
||||
Matches project-wide subscription plans in tenant service
|
||||
"""
|
||||
STARTER = "starter"
|
||||
PROFESSIONAL = "professional"
|
||||
ENTERPRISE = "enterprise"
|
||||
|
||||
|
||||
class UserRole(Enum):
|
||||
"""
|
||||
User role hierarchy
|
||||
Matches project-wide role definitions in tenant member model
|
||||
"""
|
||||
VIEWER = "viewer"
|
||||
MEMBER = "member"
|
||||
ADMIN = "admin"
|
||||
OWNER = "owner"
|
||||
|
||||
|
||||
# Tier hierarchy for comparison (higher number = higher tier)
|
||||
TIER_HIERARCHY = {
|
||||
SubscriptionTier.STARTER: 1,
|
||||
SubscriptionTier.PROFESSIONAL: 2,
|
||||
SubscriptionTier.ENTERPRISE: 3,
|
||||
}
|
||||
|
||||
# Role hierarchy for comparison (higher number = more permissions)
|
||||
ROLE_HIERARCHY = {
|
||||
UserRole.VIEWER: 1,
|
||||
UserRole.MEMBER: 2,
|
||||
UserRole.ADMIN: 3,
|
||||
UserRole.OWNER: 4,
|
||||
}
|
||||
|
||||
|
||||
def check_tier_access(user_tier: str, required_tiers: List[str]) -> bool:
|
||||
"""
|
||||
Check if user's subscription tier meets the requirement
|
||||
|
||||
Args:
|
||||
user_tier: Current user's subscription tier
|
||||
required_tiers: List of allowed tiers
|
||||
|
||||
Returns:
|
||||
bool: True if access is allowed
|
||||
"""
|
||||
try:
|
||||
user_tier_enum = SubscriptionTier(user_tier.lower())
|
||||
user_tier_level = TIER_HIERARCHY.get(user_tier_enum, 0)
|
||||
|
||||
# Get minimum required tier level
|
||||
min_required_level = min(
|
||||
TIER_HIERARCHY.get(SubscriptionTier(tier.lower()), 999)
|
||||
for tier in required_tiers
|
||||
)
|
||||
|
||||
return user_tier_level >= min_required_level
|
||||
except (ValueError, KeyError) as e:
|
||||
logger.warning("Invalid tier comparison", user_tier=user_tier, required=required_tiers, error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
def check_role_access(user_role: str, required_roles: List[str]) -> bool:
|
||||
"""
|
||||
Check if user's role meets the requirement
|
||||
|
||||
Args:
|
||||
user_role: Current user's role
|
||||
required_roles: List of allowed roles
|
||||
|
||||
Returns:
|
||||
bool: True if access is allowed
|
||||
"""
|
||||
try:
|
||||
user_role_enum = UserRole(user_role.lower())
|
||||
user_role_level = ROLE_HIERARCHY.get(user_role_enum, 0)
|
||||
|
||||
# Get minimum required role level
|
||||
min_required_level = min(
|
||||
ROLE_HIERARCHY.get(UserRole(role.lower()), 999)
|
||||
for role in required_roles
|
||||
)
|
||||
|
||||
return user_role_level >= min_required_level
|
||||
except (ValueError, KeyError) as e:
|
||||
logger.warning("Invalid role comparison", user_role=user_role, required=required_roles, error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
def require_subscription_tier(allowed_tiers: List[str]):
|
||||
"""
|
||||
Decorator to enforce subscription tier access control
|
||||
|
||||
Usage:
|
||||
@router.get("/analytics/advanced")
|
||||
@require_subscription_tier(['professional', 'enterprise'])
|
||||
async def get_advanced_analytics(...):
|
||||
...
|
||||
|
||||
Args:
|
||||
allowed_tiers: List of subscription tiers allowed to access this endpoint
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Get current user from kwargs (injected by get_current_user_dep)
|
||||
current_user = kwargs.get('current_user')
|
||||
|
||||
if not current_user:
|
||||
# Try to find in args
|
||||
for arg in args:
|
||||
if isinstance(arg, dict) and 'user_id' in arg:
|
||||
current_user = arg
|
||||
break
|
||||
|
||||
if not current_user:
|
||||
logger.error("Current user not found in request context")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
# Get tenant's subscription tier from user context
|
||||
# The gateway should inject this information
|
||||
subscription_tier = current_user.get('subscription_tier')
|
||||
|
||||
if not subscription_tier:
|
||||
logger.warning("Subscription tier not found in user context", user_id=current_user.get('user_id'))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Subscription information unavailable"
|
||||
)
|
||||
|
||||
# Check tier access
|
||||
has_access = check_tier_access(subscription_tier, allowed_tiers)
|
||||
|
||||
if not has_access:
|
||||
logger.warning(
|
||||
"Subscription tier access denied",
|
||||
user_tier=subscription_tier,
|
||||
required_tiers=allowed_tiers,
|
||||
user_id=current_user.get('user_id')
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
detail={
|
||||
"error": "subscription_tier_insufficient",
|
||||
"message": f"This feature requires a {' or '.join(allowed_tiers)} subscription plan",
|
||||
"current_plan": subscription_tier,
|
||||
"required_plans": allowed_tiers,
|
||||
"upgrade_url": "/app/settings/profile"
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug("Subscription tier check passed", tier=subscription_tier, required=allowed_tiers)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def require_user_role(allowed_roles: List[str]):
|
||||
"""
|
||||
Decorator to enforce role-based access control
|
||||
|
||||
Usage:
|
||||
@router.delete("/ingredients/{id}")
|
||||
@require_user_role(['admin', 'manager'])
|
||||
async def delete_ingredient(...):
|
||||
...
|
||||
|
||||
Args:
|
||||
allowed_roles: List of user roles allowed to access this endpoint
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Get current user from kwargs
|
||||
current_user = kwargs.get('current_user')
|
||||
|
||||
if not current_user:
|
||||
# Try to find in args
|
||||
for arg in args:
|
||||
if isinstance(arg, dict) and 'user_id' in arg:
|
||||
current_user = arg
|
||||
break
|
||||
|
||||
if not current_user:
|
||||
logger.error("Current user not found in request context")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
# Get user's role
|
||||
user_role = current_user.get('role', 'user')
|
||||
|
||||
# Check role access
|
||||
has_access = check_role_access(user_role, allowed_roles)
|
||||
|
||||
if not has_access:
|
||||
logger.warning(
|
||||
"Role-based access denied",
|
||||
user_role=user_role,
|
||||
required_roles=allowed_roles,
|
||||
user_id=current_user.get('user_id')
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"error": "insufficient_permissions",
|
||||
"message": f"This action requires {' or '.join(allowed_roles)} role",
|
||||
"current_role": user_role,
|
||||
"required_roles": allowed_roles
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug("Role check passed", role=user_role, required=allowed_roles)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def require_tier_and_role(
|
||||
allowed_tiers: List[str],
|
||||
allowed_roles: List[str]
|
||||
):
|
||||
"""
|
||||
Combined decorator for both tier and role enforcement
|
||||
|
||||
Usage:
|
||||
@router.post("/analytics/custom-report")
|
||||
@require_tier_and_role(['professional', 'enterprise'], ['admin', 'manager'])
|
||||
async def create_custom_report(...):
|
||||
...
|
||||
|
||||
Args:
|
||||
allowed_tiers: List of subscription tiers allowed
|
||||
allowed_roles: List of user roles allowed
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Get current user from kwargs
|
||||
current_user = kwargs.get('current_user')
|
||||
|
||||
if not current_user:
|
||||
# Try to find in args
|
||||
for arg in args:
|
||||
if isinstance(arg, dict) and 'user_id' in arg:
|
||||
current_user = arg
|
||||
break
|
||||
|
||||
if not current_user:
|
||||
logger.error("Current user not found in request context")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
# Check subscription tier
|
||||
subscription_tier = current_user.get('subscription_tier')
|
||||
if subscription_tier:
|
||||
tier_access = check_tier_access(subscription_tier, allowed_tiers)
|
||||
if not tier_access:
|
||||
logger.warning(
|
||||
"Combined access control: tier check failed",
|
||||
user_tier=subscription_tier,
|
||||
required_tiers=allowed_tiers
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||
detail={
|
||||
"error": "subscription_tier_insufficient",
|
||||
"message": f"This feature requires a {' or '.join(allowed_tiers)} subscription plan",
|
||||
"current_plan": subscription_tier,
|
||||
"required_plans": allowed_tiers,
|
||||
"upgrade_url": "/app/settings/profile"
|
||||
}
|
||||
)
|
||||
|
||||
# Check user role
|
||||
user_role = current_user.get('role', 'member')
|
||||
role_access = check_role_access(user_role, allowed_roles)
|
||||
|
||||
if not role_access:
|
||||
logger.warning(
|
||||
"Combined access control: role check failed",
|
||||
user_role=user_role,
|
||||
required_roles=allowed_roles
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"error": "insufficient_permissions",
|
||||
"message": f"This action requires {' or '.join(allowed_roles)} role",
|
||||
"current_role": user_role,
|
||||
"required_roles": allowed_roles
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Combined access control passed",
|
||||
tier=subscription_tier,
|
||||
role=user_role,
|
||||
required_tiers=allowed_tiers,
|
||||
required_roles=allowed_roles
|
||||
)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
# Convenience decorators for common patterns
|
||||
analytics_tier_required = require_subscription_tier(['professional', 'enterprise'])
|
||||
enterprise_tier_required = require_subscription_tier(['enterprise'])
|
||||
admin_role_required = require_user_role(['admin', 'owner'])
|
||||
owner_role_required = require_user_role(['owner'])
|
||||
@@ -1,7 +1,12 @@
|
||||
# shared/clients/forecast_client.py
|
||||
"""
|
||||
Forecast Service Client
|
||||
Forecast Service Client - Updated for refactored backend structure
|
||||
Handles all API calls to the forecasting service
|
||||
|
||||
Backend structure:
|
||||
- ATOMIC: /forecasting/forecasts (CRUD)
|
||||
- BUSINESS: /forecasting/operations/* (single, multi-day, batch, etc.)
|
||||
- ANALYTICS: /forecasting/analytics/* (predictions-performance)
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
@@ -12,17 +17,172 @@ from shared.config.base import BaseServiceSettings
|
||||
|
||||
class ForecastServiceClient(BaseServiceClient):
|
||||
"""Client for communicating with the forecasting service"""
|
||||
|
||||
|
||||
def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"):
|
||||
super().__init__(calling_service_name, config)
|
||||
|
||||
|
||||
def get_service_base_path(self) -> str:
|
||||
return "/api/v1"
|
||||
|
||||
|
||||
# ================================================================
|
||||
# FORECASTS
|
||||
# ATOMIC: Forecast CRUD Operations
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_forecast(self, tenant_id: str, forecast_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get forecast details by ID"""
|
||||
return await self.get(f"forecasting/forecasts/{forecast_id}", tenant_id=tenant_id)
|
||||
|
||||
async def list_forecasts(
|
||||
self,
|
||||
tenant_id: str,
|
||||
inventory_product_id: Optional[str] = None,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""List forecasts for a tenant with optional filters"""
|
||||
params = {"limit": limit, "offset": offset}
|
||||
if inventory_product_id:
|
||||
params["inventory_product_id"] = inventory_product_id
|
||||
if start_date:
|
||||
params["start_date"] = start_date.isoformat()
|
||||
if end_date:
|
||||
params["end_date"] = end_date.isoformat()
|
||||
|
||||
return await self.get("forecasting/forecasts", tenant_id=tenant_id, params=params)
|
||||
|
||||
async def delete_forecast(self, tenant_id: str, forecast_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Delete a forecast"""
|
||||
return await self.delete(f"forecasting/forecasts/{forecast_id}", tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# BUSINESS: Forecasting Operations
|
||||
# ================================================================
|
||||
|
||||
async def generate_single_forecast(
|
||||
self,
|
||||
tenant_id: str,
|
||||
inventory_product_id: str,
|
||||
forecast_date: date,
|
||||
include_recommendations: bool = False
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Generate a single product forecast"""
|
||||
data = {
|
||||
"inventory_product_id": inventory_product_id,
|
||||
"forecast_date": forecast_date.isoformat(),
|
||||
"include_recommendations": include_recommendations
|
||||
}
|
||||
return await self.post("forecasting/operations/single", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def generate_multi_day_forecast(
|
||||
self,
|
||||
tenant_id: str,
|
||||
inventory_product_id: str,
|
||||
forecast_date: date,
|
||||
forecast_days: int = 7,
|
||||
include_recommendations: bool = False
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Generate multiple daily forecasts for the specified period"""
|
||||
data = {
|
||||
"inventory_product_id": inventory_product_id,
|
||||
"forecast_date": forecast_date.isoformat(),
|
||||
"forecast_days": forecast_days,
|
||||
"include_recommendations": include_recommendations
|
||||
}
|
||||
return await self.post("forecasting/operations/multi-day", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def generate_batch_forecast(
|
||||
self,
|
||||
tenant_id: str,
|
||||
inventory_product_ids: List[str],
|
||||
forecast_date: date,
|
||||
forecast_days: int = 1
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Generate forecasts for multiple products in batch"""
|
||||
data = {
|
||||
"inventory_product_ids": inventory_product_ids,
|
||||
"forecast_date": forecast_date.isoformat(),
|
||||
"forecast_days": forecast_days
|
||||
}
|
||||
return await self.post("forecasting/operations/batch", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def generate_realtime_prediction(
|
||||
self,
|
||||
tenant_id: str,
|
||||
inventory_product_id: str,
|
||||
model_id: str,
|
||||
features: Dict[str, Any],
|
||||
model_path: Optional[str] = None,
|
||||
confidence_level: float = 0.8
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Generate real-time prediction"""
|
||||
data = {
|
||||
"inventory_product_id": inventory_product_id,
|
||||
"model_id": model_id,
|
||||
"features": features,
|
||||
"confidence_level": confidence_level
|
||||
}
|
||||
if model_path:
|
||||
data["model_path"] = model_path
|
||||
|
||||
return await self.post("forecasting/operations/realtime", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def validate_predictions(
|
||||
self,
|
||||
tenant_id: str,
|
||||
start_date: date,
|
||||
end_date: date
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Validate predictions against actual sales data"""
|
||||
params = {
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat()
|
||||
}
|
||||
return await self.post("forecasting/operations/validate-predictions", params=params, tenant_id=tenant_id)
|
||||
|
||||
async def get_forecast_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get forecast statistics"""
|
||||
params = {}
|
||||
if start_date:
|
||||
params["start_date"] = start_date.isoformat()
|
||||
if end_date:
|
||||
params["end_date"] = end_date.isoformat()
|
||||
|
||||
return await self.get("forecasting/operations/statistics", tenant_id=tenant_id, params=params)
|
||||
|
||||
async def clear_prediction_cache(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Clear prediction cache"""
|
||||
return await self.delete("forecasting/operations/cache", tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# ANALYTICS: Forecasting Analytics
|
||||
# ================================================================
|
||||
|
||||
async def get_predictions_performance(
|
||||
self,
|
||||
tenant_id: str,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get predictions performance analytics"""
|
||||
params = {}
|
||||
if start_date:
|
||||
params["start_date"] = start_date.isoformat()
|
||||
if end_date:
|
||||
params["end_date"] = end_date.isoformat()
|
||||
|
||||
return await self.get("forecasting/analytics/predictions-performance", tenant_id=tenant_id, params=params)
|
||||
|
||||
# ================================================================
|
||||
# Legacy/Compatibility Methods (deprecated)
|
||||
# ================================================================
|
||||
|
||||
async def create_forecast(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -33,180 +193,16 @@ class ForecastServiceClient(BaseServiceClient):
|
||||
include_confidence_intervals: bool = True,
|
||||
**kwargs
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new forecast"""
|
||||
data = {
|
||||
"model_id": model_id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"include_confidence_intervals": include_confidence_intervals,
|
||||
**kwargs
|
||||
}
|
||||
"""
|
||||
DEPRECATED: Use generate_single_forecast or generate_batch_forecast instead
|
||||
Legacy method for backward compatibility
|
||||
"""
|
||||
# Map to new batch forecast operation
|
||||
if product_ids:
|
||||
data["product_ids"] = product_ids
|
||||
|
||||
return await self.post("forecasts", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def get_forecast(self, tenant_id: str, forecast_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get forecast details"""
|
||||
return await self.get(f"forecasts/{forecast_id}", tenant_id=tenant_id)
|
||||
|
||||
async def list_forecasts(
|
||||
self,
|
||||
tenant_id: str,
|
||||
status: Optional[str] = None,
|
||||
model_id: Optional[str] = None,
|
||||
limit: int = 50
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""List forecasts for a tenant"""
|
||||
params = {"limit": limit}
|
||||
if status:
|
||||
params["status"] = status
|
||||
if model_id:
|
||||
params["model_id"] = model_id
|
||||
|
||||
result = await self.get("forecasts", tenant_id=tenant_id, params=params)
|
||||
return result.get("forecasts", []) if result else None
|
||||
|
||||
async def delete_forecast(self, tenant_id: str, forecast_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Delete a forecast"""
|
||||
return await self.delete(f"forecasts/{forecast_id}", tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# PREDICTIONS
|
||||
# ================================================================
|
||||
|
||||
async def get_predictions(
|
||||
self,
|
||||
tenant_id: str,
|
||||
forecast_id: str,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
product_id: Optional[str] = None
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get predictions from a forecast"""
|
||||
params = {}
|
||||
if start_date:
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
if product_id:
|
||||
params["product_id"] = product_id
|
||||
|
||||
result = await self.get(f"forecasts/{forecast_id}/predictions", tenant_id=tenant_id, params=params)
|
||||
return result.get("predictions", []) if result else None
|
||||
|
||||
async def create_realtime_prediction(
|
||||
self,
|
||||
tenant_id: str,
|
||||
model_id: str,
|
||||
target_date: str,
|
||||
features: Dict[str, Any],
|
||||
inventory_product_id: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Create a real-time prediction"""
|
||||
data = {
|
||||
"model_id": model_id,
|
||||
"target_date": target_date,
|
||||
"features": features,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Add inventory_product_id if provided (required by forecasting service)
|
||||
if inventory_product_id:
|
||||
data["inventory_product_id"] = inventory_product_id
|
||||
|
||||
return await self.post("forecasts/single", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def create_single_forecast(
|
||||
self,
|
||||
tenant_id: str,
|
||||
inventory_product_id: str,
|
||||
forecast_date: date,
|
||||
location: str = "default",
|
||||
forecast_days: int = 1,
|
||||
confidence_level: float = 0.8,
|
||||
**kwargs
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Create a single product forecast using new API format"""
|
||||
from datetime import date as date_type
|
||||
|
||||
# Convert date to string if needed
|
||||
if isinstance(forecast_date, date_type):
|
||||
forecast_date_str = forecast_date.isoformat()
|
||||
else:
|
||||
forecast_date_str = str(forecast_date)
|
||||
|
||||
data = {
|
||||
"inventory_product_id": inventory_product_id,
|
||||
"forecast_date": forecast_date_str,
|
||||
"forecast_days": forecast_days,
|
||||
"location": location,
|
||||
"confidence_level": confidence_level,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
return await self.post("forecasts/single", data=data, tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# FORECAST VALIDATION & METRICS
|
||||
# ================================================================
|
||||
|
||||
async def get_forecast_accuracy(
|
||||
self,
|
||||
tenant_id: str,
|
||||
forecast_id: str,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get forecast accuracy metrics"""
|
||||
params = {}
|
||||
if start_date:
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
|
||||
return await self.get(f"forecasts/{forecast_id}/accuracy", tenant_id=tenant_id, params=params)
|
||||
|
||||
async def compare_forecasts(
|
||||
self,
|
||||
tenant_id: str,
|
||||
forecast_ids: List[str],
|
||||
metric: str = "mape"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Compare multiple forecasts"""
|
||||
data = {
|
||||
"forecast_ids": forecast_ids,
|
||||
"metric": metric
|
||||
}
|
||||
return await self.post("forecasts/compare", data=data, tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# FORECAST SCENARIOS
|
||||
# ================================================================
|
||||
|
||||
async def create_scenario_forecast(
|
||||
self,
|
||||
tenant_id: str,
|
||||
model_id: str,
|
||||
scenario_name: str,
|
||||
scenario_data: Dict[str, Any],
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
**kwargs
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Create a scenario-based forecast"""
|
||||
data = {
|
||||
"model_id": model_id,
|
||||
"scenario_name": scenario_name,
|
||||
"scenario_data": scenario_data,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
**kwargs
|
||||
}
|
||||
return await self.post("scenarios", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def list_scenarios(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""List forecast scenarios for a tenant"""
|
||||
result = await self.get("scenarios", tenant_id=tenant_id)
|
||||
return result.get("scenarios", []) if result else None
|
||||
return await self.generate_batch_forecast(
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_ids=product_ids,
|
||||
forecast_date=date.fromisoformat(start_date),
|
||||
forecast_days=1
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -31,13 +31,13 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
async def get_ingredient_by_id(self, ingredient_id: UUID, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get ingredient details by ID"""
|
||||
try:
|
||||
result = await self.get(f"ingredients/{ingredient_id}", tenant_id=tenant_id)
|
||||
result = await self.get(f"inventory/ingredients/{ingredient_id}", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved ingredient from inventory service",
|
||||
logger.info("Retrieved ingredient from inventory service",
|
||||
ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error fetching ingredient by ID",
|
||||
logger.error("Error fetching ingredient by ID",
|
||||
error=str(e), ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
@@ -64,10 +64,10 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if is_active is not None:
|
||||
params["is_active"] = is_active
|
||||
|
||||
result = await self.get("ingredients", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("inventory/ingredients", tenant_id=tenant_id, params=params)
|
||||
ingredients = result if isinstance(result, list) else []
|
||||
|
||||
logger.info("Searched ingredients in inventory service",
|
||||
|
||||
logger.info("Searched ingredients in inventory service",
|
||||
search_term=search, count=len(ingredients), tenant_id=tenant_id)
|
||||
return ingredients
|
||||
|
||||
@@ -83,7 +83,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if is_active is not None:
|
||||
params["is_active"] = is_active
|
||||
|
||||
ingredients = await self.get_paginated("ingredients", tenant_id=tenant_id, params=params)
|
||||
ingredients = await self.get_paginated("inventory/ingredients", tenant_id=tenant_id, params=params)
|
||||
|
||||
logger.info("Retrieved all ingredients from inventory service",
|
||||
count=len(ingredients), tenant_id=tenant_id)
|
||||
@@ -101,7 +101,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if is_active is not None:
|
||||
params["is_active"] = is_active
|
||||
|
||||
result = await self.get("ingredients/count", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("inventory/ingredients/count", tenant_id=tenant_id, params=params)
|
||||
count = result.get("ingredient_count", 0) if isinstance(result, dict) else 0
|
||||
|
||||
logger.info("Retrieved ingredient count from inventory service",
|
||||
@@ -116,7 +116,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
async def create_ingredient(self, ingredient_data: Dict[str, Any], tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new ingredient"""
|
||||
try:
|
||||
result = await self.post("ingredients", data=ingredient_data, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/ingredients", data=ingredient_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Created ingredient in inventory service",
|
||||
ingredient_name=ingredient_data.get('name'), tenant_id=tenant_id)
|
||||
@@ -134,7 +134,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Update an existing ingredient"""
|
||||
try:
|
||||
result = await self.put(f"ingredients/{ingredient_id}", data=ingredient_data, tenant_id=tenant_id)
|
||||
result = await self.put(f"inventory/ingredients/{ingredient_id}", data=ingredient_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Updated ingredient in inventory service",
|
||||
ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
@@ -147,7 +147,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
async def delete_ingredient(self, ingredient_id: UUID, tenant_id: str) -> bool:
|
||||
"""Delete (deactivate) an ingredient"""
|
||||
try:
|
||||
result = await self.delete(f"ingredients/{ingredient_id}", tenant_id=tenant_id)
|
||||
result = await self.delete(f"inventory/ingredients/{ingredient_id}", tenant_id=tenant_id)
|
||||
success = result is not None
|
||||
if success:
|
||||
logger.info("Deleted ingredient in inventory service",
|
||||
@@ -170,7 +170,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if include_unavailable:
|
||||
params["include_unavailable"] = include_unavailable
|
||||
|
||||
result = await self.get(f"ingredients/{ingredient_id}/stock", tenant_id=tenant_id, params=params)
|
||||
result = await self.get(f"inventory/ingredients/{ingredient_id}/stock", tenant_id=tenant_id, params=params)
|
||||
stock_entries = result if isinstance(result, list) else []
|
||||
|
||||
logger.info("Retrieved ingredient stock from inventory service",
|
||||
@@ -193,7 +193,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if ingredient_ids:
|
||||
params["ingredient_ids"] = [str(id) for id in ingredient_ids]
|
||||
|
||||
result = await self.get("stock", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("inventory/stock", tenant_id=tenant_id, params=params)
|
||||
stock_levels = result if isinstance(result, list) else []
|
||||
|
||||
logger.info("Retrieved stock levels from inventory service",
|
||||
@@ -208,7 +208,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
async def get_low_stock_alerts(self, tenant_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get low stock alerts"""
|
||||
try:
|
||||
result = await self.get("alerts", tenant_id=tenant_id, params={"type": "low_stock"})
|
||||
result = await self.get("inventory/alerts", tenant_id=tenant_id, params={"type": "low_stock"})
|
||||
alerts = result if isinstance(result, list) else []
|
||||
|
||||
logger.info("Retrieved low stock alerts from inventory service",
|
||||
@@ -227,7 +227,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Record stock consumption"""
|
||||
try:
|
||||
result = await self.post("stock/consume", data=consumption_data, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/operations/consume-stock", data=consumption_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Recorded stock consumption",
|
||||
tenant_id=tenant_id)
|
||||
@@ -244,7 +244,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Record stock receipt"""
|
||||
try:
|
||||
result = await self.post("stock/receive", data=receipt_data, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/operations/receive-stock", data=receipt_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Recorded stock receipt",
|
||||
tenant_id=tenant_id)
|
||||
@@ -271,7 +271,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
"sales_volume": sales_volume
|
||||
}
|
||||
|
||||
result = await self.post("inventory/classify-product", data=classification_data, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/operations/classify-product", data=classification_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Classified product",
|
||||
product=product_name,
|
||||
@@ -296,7 +296,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
"products": products
|
||||
}
|
||||
|
||||
result = await self.post("inventory/classify-products-batch", data=classification_data, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/operations/classify-products-batch", data=classification_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
suggestions = result.get('suggestions', [])
|
||||
business_model = result.get('business_model_analysis', {}).get('model', 'unknown')
|
||||
@@ -319,7 +319,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
async def get_inventory_dashboard(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get inventory dashboard data"""
|
||||
try:
|
||||
result = await self.get("dashboard", tenant_id=tenant_id)
|
||||
result = await self.get("inventory/dashboard/overview", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved inventory dashboard data", tenant_id=tenant_id)
|
||||
return result
|
||||
@@ -331,7 +331,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
async def get_inventory_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get inventory summary statistics"""
|
||||
try:
|
||||
result = await self.get("dashboard/summary", tenant_id=tenant_id)
|
||||
result = await self.get("inventory/dashboard/summary", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved inventory summary", tenant_id=tenant_id)
|
||||
return result
|
||||
@@ -351,7 +351,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Create a product transformation (e.g., par-baked to fully baked)"""
|
||||
try:
|
||||
result = await self.post("transformations", data=transformation_data, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/transformations", data=transformation_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Created product transformation",
|
||||
transformation_reference=result.get('transformation_reference'),
|
||||
@@ -388,7 +388,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if notes:
|
||||
params["notes"] = notes
|
||||
|
||||
result = await self.post("transformations/par-bake-to-fresh", params=params, tenant_id=tenant_id)
|
||||
result = await self.post("inventory/transformations/par-bake-to-fresh", params=params, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Created par-bake transformation",
|
||||
transformation_id=result.get('transformation_id'),
|
||||
@@ -426,7 +426,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
if days_back:
|
||||
params["days_back"] = days_back
|
||||
|
||||
result = await self.get("transformations", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("inventory/transformations", tenant_id=tenant_id, params=params)
|
||||
transformations = result if isinstance(result, list) else []
|
||||
|
||||
logger.info("Retrieved transformations from inventory service",
|
||||
@@ -445,7 +445,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Get specific transformation by ID"""
|
||||
try:
|
||||
result = await self.get(f"transformations/{transformation_id}", tenant_id=tenant_id)
|
||||
result = await self.get(f"inventory/transformations/{transformation_id}", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved transformation by ID",
|
||||
transformation_id=transformation_id, tenant_id=tenant_id)
|
||||
@@ -463,7 +463,7 @@ class InventoryServiceClient(BaseServiceClient):
|
||||
"""Get transformation summary for dashboard"""
|
||||
try:
|
||||
params = {"days_back": days_back}
|
||||
result = await self.get("transformations/summary", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("inventory/dashboard/transformations-summary", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved transformation summary",
|
||||
days_back=days_back, tenant_id=tenant_id)
|
||||
|
||||
@@ -15,156 +15,156 @@ logger = structlog.get_logger()
|
||||
|
||||
class OrdersServiceClient(BaseServiceClient):
|
||||
"""Client for communicating with the Orders Service"""
|
||||
|
||||
|
||||
def __init__(self, config: BaseServiceSettings):
|
||||
super().__init__("orders", config)
|
||||
|
||||
|
||||
def get_service_base_path(self) -> str:
|
||||
return "/api/v1"
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PROCUREMENT PLANNING
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_demand_requirements(self, tenant_id: str, date: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get demand requirements for production planning"""
|
||||
try:
|
||||
params = {"date": date}
|
||||
result = await self.get("demand-requirements", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("orders/demand-requirements", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved demand requirements from orders service",
|
||||
logger.info("Retrieved demand requirements from orders service",
|
||||
date=date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting demand requirements",
|
||||
logger.error("Error getting demand requirements",
|
||||
error=str(e), date=date, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_procurement_requirements(self, tenant_id: str, horizon: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get procurement requirements for purchasing planning"""
|
||||
try:
|
||||
params = {}
|
||||
if horizon:
|
||||
params["horizon"] = horizon
|
||||
|
||||
result = await self.get("procurement-requirements", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("orders/procurement-requirements", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved procurement requirements from orders service",
|
||||
logger.info("Retrieved procurement requirements from orders service",
|
||||
horizon=horizon, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting procurement requirements",
|
||||
logger.error("Error getting procurement requirements",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_weekly_ingredient_needs(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get weekly ingredient ordering needs for dashboard"""
|
||||
try:
|
||||
result = await self.get("weekly-ingredient-needs", tenant_id=tenant_id)
|
||||
result = await self.get("orders/dashboard/weekly-ingredient-needs", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved weekly ingredient needs from orders service",
|
||||
logger.info("Retrieved weekly ingredient needs from orders service",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting weekly ingredient needs",
|
||||
logger.error("Error getting weekly ingredient needs",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# CUSTOMER ORDERS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_customer_orders(self, tenant_id: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get customer orders with optional filtering"""
|
||||
try:
|
||||
result = await self.get("customer-orders", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("orders/list", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
orders_count = len(result.get('orders', [])) if isinstance(result, dict) else len(result) if isinstance(result, list) else 0
|
||||
logger.info("Retrieved customer orders from orders service",
|
||||
logger.info("Retrieved customer orders from orders service",
|
||||
orders_count=orders_count, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting customer orders",
|
||||
logger.error("Error getting customer orders",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def create_customer_order(self, tenant_id: str, order_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new customer order"""
|
||||
try:
|
||||
result = await self.post("customer-orders", data=order_data, tenant_id=tenant_id)
|
||||
result = await self.post("orders/list", data=order_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Created customer order",
|
||||
logger.info("Created customer order",
|
||||
order_id=result.get('id'), tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error creating customer order",
|
||||
logger.error("Error creating customer order",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def update_customer_order(self, tenant_id: str, order_id: str, order_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Update an existing customer order"""
|
||||
try:
|
||||
result = await self.put(f"customer-orders/{order_id}", data=order_data, tenant_id=tenant_id)
|
||||
result = await self.put(f"orders/list/{order_id}", data=order_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Updated customer order",
|
||||
logger.info("Updated customer order",
|
||||
order_id=order_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error updating customer order",
|
||||
logger.error("Error updating customer order",
|
||||
error=str(e), order_id=order_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# CENTRAL BAKERY ORDERS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_daily_finalized_orders(self, tenant_id: str, date: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get daily finalized orders for central bakery"""
|
||||
try:
|
||||
params = {}
|
||||
if date:
|
||||
params["date"] = date
|
||||
|
||||
result = await self.get("daily-finalized-orders", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("orders/daily-finalized", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved daily finalized orders from orders service",
|
||||
logger.info("Retrieved daily finalized orders from orders service",
|
||||
date=date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting daily finalized orders",
|
||||
logger.error("Error getting daily finalized orders",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_weekly_order_summaries(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get weekly order summaries for central bakery dashboard"""
|
||||
try:
|
||||
result = await self.get("weekly-order-summaries", tenant_id=tenant_id)
|
||||
result = await self.get("orders/dashboard/weekly-summaries", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved weekly order summaries from orders service",
|
||||
logger.info("Retrieved weekly order summaries from orders service",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting weekly order summaries",
|
||||
logger.error("Error getting weekly order summaries",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# DASHBOARD AND ANALYTICS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_dashboard_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get orders dashboard summary data"""
|
||||
try:
|
||||
result = await self.get("dashboard-summary", tenant_id=tenant_id)
|
||||
result = await self.get("orders/dashboard/summary", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved orders dashboard summary",
|
||||
logger.info("Retrieved orders dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting orders dashboard summary",
|
||||
logger.error("Error getting orders dashboard summary",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_order_trends(self, tenant_id: str, start_date: str, end_date: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get order trends analysis"""
|
||||
try:
|
||||
@@ -172,50 +172,50 @@ class OrdersServiceClient(BaseServiceClient):
|
||||
"start_date": start_date,
|
||||
"end_date": end_date
|
||||
}
|
||||
result = await self.get("order-trends", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("orders/analytics/trends", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved order trends from orders service",
|
||||
logger.info("Retrieved order trends from orders service",
|
||||
start_date=start_date, end_date=end_date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting order trends",
|
||||
logger.error("Error getting order trends",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# ALERTS AND NOTIFICATIONS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_central_bakery_alerts(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get central bakery specific alerts"""
|
||||
try:
|
||||
result = await self.get("central-bakery-alerts", tenant_id=tenant_id)
|
||||
result = await self.get("orders/alerts", tenant_id=tenant_id)
|
||||
alerts = result.get('alerts', []) if result else []
|
||||
logger.info("Retrieved central bakery alerts from orders service",
|
||||
logger.info("Retrieved central bakery alerts from orders service",
|
||||
alerts_count=len(alerts), tenant_id=tenant_id)
|
||||
return alerts
|
||||
except Exception as e:
|
||||
logger.error("Error getting central bakery alerts",
|
||||
logger.error("Error getting central bakery alerts",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
|
||||
async def acknowledge_alert(self, tenant_id: str, alert_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Acknowledge an order-related alert"""
|
||||
try:
|
||||
result = await self.post(f"alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
|
||||
result = await self.post(f"orders/alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Acknowledged order alert",
|
||||
logger.info("Acknowledged order alert",
|
||||
alert_id=alert_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error acknowledging order alert",
|
||||
logger.error("Error acknowledging order alert",
|
||||
error=str(e), alert_id=alert_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# UTILITY METHODS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def download_orders_pdf(self, tenant_id: str, order_ids: List[str], format_type: str = "supplier_communication") -> Optional[bytes]:
|
||||
"""Download orders as PDF for supplier communication"""
|
||||
try:
|
||||
@@ -225,16 +225,16 @@ class OrdersServiceClient(BaseServiceClient):
|
||||
"include_delivery_schedule": True
|
||||
}
|
||||
# Note: This would need special handling for binary data
|
||||
result = await self.post("download/pdf", data=data, tenant_id=tenant_id)
|
||||
result = await self.post("orders/operations/download-pdf", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Generated orders PDF",
|
||||
logger.info("Generated orders PDF",
|
||||
orders_count=len(order_ids), tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error generating orders PDF",
|
||||
logger.error("Error generating orders PDF",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if orders service is healthy"""
|
||||
try:
|
||||
@@ -248,4 +248,4 @@ class OrdersServiceClient(BaseServiceClient):
|
||||
# Factory function for dependency injection
|
||||
def create_orders_client(config: BaseServiceSettings) -> OrdersServiceClient:
|
||||
"""Create orders service client instance"""
|
||||
return OrdersServiceClient(config)
|
||||
return OrdersServiceClient(config)
|
||||
|
||||
@@ -15,51 +15,51 @@ logger = structlog.get_logger()
|
||||
|
||||
class ProductionServiceClient(BaseServiceClient):
|
||||
"""Client for communicating with the Production Service"""
|
||||
|
||||
|
||||
def __init__(self, config: BaseServiceSettings):
|
||||
super().__init__("production", config)
|
||||
|
||||
|
||||
def get_service_base_path(self) -> str:
|
||||
return "/api/v1"
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PRODUCTION PLANNING
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_production_requirements(self, tenant_id: str, date: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get production requirements for procurement planning"""
|
||||
try:
|
||||
params = {}
|
||||
if date:
|
||||
params["date"] = date
|
||||
|
||||
result = await self.get("requirements", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("production/requirements", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved production requirements from production service",
|
||||
logger.info("Retrieved production requirements from production service",
|
||||
date=date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production requirements",
|
||||
logger.error("Error getting production requirements",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_daily_requirements(self, tenant_id: str, date: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get daily production requirements"""
|
||||
try:
|
||||
params = {}
|
||||
if date:
|
||||
params["date"] = date
|
||||
|
||||
result = await self.get("daily-requirements", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("production/daily-requirements", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved daily production requirements from production service",
|
||||
logger.info("Retrieved daily production requirements from production service",
|
||||
date=date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting daily production requirements",
|
||||
logger.error("Error getting daily production requirements",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_production_schedule(self, tenant_id: str, start_date: Optional[str] = None, end_date: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get production schedule for a date range"""
|
||||
try:
|
||||
@@ -68,134 +68,134 @@ class ProductionServiceClient(BaseServiceClient):
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
|
||||
result = await self.get("schedule", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("production/schedules", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved production schedule from production service",
|
||||
logger.info("Retrieved production schedule from production service",
|
||||
start_date=start_date, end_date=end_date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production schedule",
|
||||
logger.error("Error getting production schedule",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# BATCH MANAGEMENT
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_active_batches(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get currently active production batches"""
|
||||
try:
|
||||
result = await self.get("batches/active", tenant_id=tenant_id)
|
||||
result = await self.get("production/batches/active", tenant_id=tenant_id)
|
||||
batches = result.get('batches', []) if result else []
|
||||
logger.info("Retrieved active production batches from production service",
|
||||
logger.info("Retrieved active production batches from production service",
|
||||
batches_count=len(batches), tenant_id=tenant_id)
|
||||
return batches
|
||||
except Exception as e:
|
||||
logger.error("Error getting active production batches",
|
||||
logger.error("Error getting active production batches",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
|
||||
async def create_production_batch(self, tenant_id: str, batch_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new production batch"""
|
||||
try:
|
||||
result = await self.post("batches", data=batch_data, tenant_id=tenant_id)
|
||||
result = await self.post("production/batches", data=batch_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Created production batch",
|
||||
batch_id=result.get('id'),
|
||||
logger.info("Created production batch",
|
||||
batch_id=result.get('id'),
|
||||
product_id=batch_data.get('product_id'),
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error creating production batch",
|
||||
logger.error("Error creating production batch",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def update_batch_status(self, tenant_id: str, batch_id: str, status: str, actual_quantity: Optional[float] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Update production batch status"""
|
||||
try:
|
||||
data = {"status": status}
|
||||
if actual_quantity is not None:
|
||||
data["actual_quantity"] = actual_quantity
|
||||
|
||||
result = await self.put(f"batches/{batch_id}/status", data=data, tenant_id=tenant_id)
|
||||
|
||||
result = await self.put(f"production/batches/{batch_id}/status", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Updated production batch status",
|
||||
logger.info("Updated production batch status",
|
||||
batch_id=batch_id, status=status, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error updating production batch status",
|
||||
logger.error("Error updating production batch status",
|
||||
error=str(e), batch_id=batch_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_batch_details(self, tenant_id: str, batch_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get detailed information about a production batch"""
|
||||
try:
|
||||
result = await self.get(f"batches/{batch_id}", tenant_id=tenant_id)
|
||||
result = await self.get(f"production/batches/{batch_id}", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved production batch details",
|
||||
logger.info("Retrieved production batch details",
|
||||
batch_id=batch_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production batch details",
|
||||
logger.error("Error getting production batch details",
|
||||
error=str(e), batch_id=batch_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# CAPACITY MANAGEMENT
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_capacity_status(self, tenant_id: str, date: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get production capacity status for a specific date"""
|
||||
try:
|
||||
params = {}
|
||||
if date:
|
||||
params["date"] = date
|
||||
|
||||
result = await self.get("capacity/status", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("production/capacity/status", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved production capacity status",
|
||||
logger.info("Retrieved production capacity status",
|
||||
date=date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production capacity status",
|
||||
logger.error("Error getting production capacity status",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def check_capacity_availability(self, tenant_id: str, requirements: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Check if production capacity is available for requirements"""
|
||||
try:
|
||||
result = await self.post("capacity/check-availability",
|
||||
{"requirements": requirements},
|
||||
result = await self.post("production/capacity/check-availability",
|
||||
{"requirements": requirements},
|
||||
tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Checked production capacity availability",
|
||||
logger.info("Checked production capacity availability",
|
||||
requirements_count=len(requirements), tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error checking production capacity availability",
|
||||
logger.error("Error checking production capacity availability",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# QUALITY CONTROL
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def record_quality_check(self, tenant_id: str, batch_id: str, quality_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Record quality control results for a batch"""
|
||||
try:
|
||||
result = await self.post(f"batches/{batch_id}/quality-check",
|
||||
data=quality_data,
|
||||
result = await self.post(f"production/batches/{batch_id}/quality-check",
|
||||
data=quality_data,
|
||||
tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Recorded quality check for production batch",
|
||||
logger.info("Recorded quality check for production batch",
|
||||
batch_id=batch_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error recording quality check",
|
||||
logger.error("Error recording quality check",
|
||||
error=str(e), batch_id=batch_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_yield_metrics(self, tenant_id: str, start_date: str, end_date: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get production yield metrics for analysis"""
|
||||
try:
|
||||
@@ -203,81 +203,81 @@ class ProductionServiceClient(BaseServiceClient):
|
||||
"start_date": start_date,
|
||||
"end_date": end_date
|
||||
}
|
||||
result = await self.get("metrics/yield", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("production/analytics/yield-metrics", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved production yield metrics",
|
||||
logger.info("Retrieved production yield metrics",
|
||||
start_date=start_date, end_date=end_date, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production yield metrics",
|
||||
logger.error("Error getting production yield metrics",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# DASHBOARD AND ANALYTICS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_dashboard_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get production dashboard summary data"""
|
||||
try:
|
||||
result = await self.get("dashboard-summary", tenant_id=tenant_id)
|
||||
result = await self.get("production/dashboard/summary", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved production dashboard summary",
|
||||
logger.info("Retrieved production dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production dashboard summary",
|
||||
logger.error("Error getting production dashboard summary",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
async def get_efficiency_metrics(self, tenant_id: str, period: str = "last_30_days") -> Optional[Dict[str, Any]]:
|
||||
"""Get production efficiency metrics"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
result = await self.get("metrics/efficiency", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("production/analytics/efficiency", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved production efficiency metrics",
|
||||
logger.info("Retrieved production efficiency metrics",
|
||||
period=period, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production efficiency metrics",
|
||||
logger.error("Error getting production efficiency metrics",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# ALERTS AND NOTIFICATIONS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def get_production_alerts(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get production-related alerts"""
|
||||
try:
|
||||
result = await self.get("alerts", tenant_id=tenant_id)
|
||||
result = await self.get("production/alerts", tenant_id=tenant_id)
|
||||
alerts = result.get('alerts', []) if result else []
|
||||
logger.info("Retrieved production alerts",
|
||||
logger.info("Retrieved production alerts",
|
||||
alerts_count=len(alerts), tenant_id=tenant_id)
|
||||
return alerts
|
||||
except Exception as e:
|
||||
logger.error("Error getting production alerts",
|
||||
logger.error("Error getting production alerts",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
|
||||
async def acknowledge_alert(self, tenant_id: str, alert_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Acknowledge a production-related alert"""
|
||||
try:
|
||||
result = await self.post(f"alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
|
||||
result = await self.post(f"production/alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Acknowledged production alert",
|
||||
logger.info("Acknowledged production alert",
|
||||
alert_id=alert_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error acknowledging production alert",
|
||||
logger.error("Error acknowledging production alert",
|
||||
error=str(e), alert_id=alert_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# UTILITY METHODS
|
||||
# ================================================================
|
||||
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if production service is healthy"""
|
||||
try:
|
||||
@@ -291,4 +291,4 @@ class ProductionServiceClient(BaseServiceClient):
|
||||
# Factory function for dependency injection
|
||||
def create_production_client(config: BaseServiceSettings) -> ProductionServiceClient:
|
||||
"""Create production service client instance"""
|
||||
return ProductionServiceClient(config)
|
||||
return ProductionServiceClient(config)
|
||||
|
||||
@@ -29,7 +29,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
async def get_recipe_by_id(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get recipe details by ID"""
|
||||
try:
|
||||
result = await self.get(f"recipes/{recipe_id}", tenant_id=tenant_id)
|
||||
result = await self.get(f"recipes/recipes/{recipe_id}", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved recipe details from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
@@ -43,7 +43,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
"""Get recipes for multiple products"""
|
||||
try:
|
||||
params = {"product_ids": ",".join(product_ids)}
|
||||
result = await self.get("recipes/by-products", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("recipes/recipes/by-products", tenant_id=tenant_id, params=params)
|
||||
recipes = result.get('recipes', []) if result else []
|
||||
logger.info("Retrieved recipes by product IDs from recipes service",
|
||||
product_ids_count=len(product_ids),
|
||||
@@ -82,7 +82,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
if recipe_ids:
|
||||
params["recipe_ids"] = ",".join(recipe_ids)
|
||||
|
||||
result = await self.get("requirements", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("recipes/requirements", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved recipe requirements from recipes service",
|
||||
recipe_ids_count=len(recipe_ids) if recipe_ids else 0,
|
||||
@@ -100,7 +100,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
if product_ids:
|
||||
params["product_ids"] = ",".join(product_ids)
|
||||
|
||||
result = await self.get("ingredient-requirements", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("recipes/ingredient-requirements", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved ingredient requirements from recipes service",
|
||||
product_ids_count=len(product_ids) if product_ids else 0,
|
||||
@@ -118,7 +118,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
"recipe_id": recipe_id,
|
||||
"quantity": quantity
|
||||
}
|
||||
result = await self.post("calculate-ingredients", data=data, tenant_id=tenant_id)
|
||||
result = await self.post("recipes/operations/calculate-ingredients", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Calculated ingredient quantities from recipes service",
|
||||
recipe_id=recipe_id, quantity=quantity, tenant_id=tenant_id)
|
||||
@@ -132,7 +132,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
"""Calculate total ingredient requirements for multiple production batches"""
|
||||
try:
|
||||
data = {"production_requests": production_requests}
|
||||
result = await self.post("calculate-batch-ingredients", data=data, tenant_id=tenant_id)
|
||||
result = await self.post("recipes/operations/calculate-batch-ingredients", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Calculated batch ingredient requirements from recipes service",
|
||||
batches_count=len(production_requests), tenant_id=tenant_id)
|
||||
@@ -149,7 +149,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
async def get_production_instructions(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get detailed production instructions for a recipe"""
|
||||
try:
|
||||
result = await self.get(f"recipes/{recipe_id}/production-instructions", tenant_id=tenant_id)
|
||||
result = await self.get(f"recipes/recipes/{recipe_id}/production-instructions", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved production instructions from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
@@ -162,7 +162,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
async def get_recipe_yield_info(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get yield information for a recipe"""
|
||||
try:
|
||||
result = await self.get(f"recipes/{recipe_id}/yield", tenant_id=tenant_id)
|
||||
result = await self.get(f"recipes/recipes/{recipe_id}/yield", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved recipe yield info from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
@@ -179,7 +179,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
"recipe_id": recipe_id,
|
||||
"quantity": quantity
|
||||
}
|
||||
result = await self.post("validate-feasibility", data=data, tenant_id=tenant_id)
|
||||
result = await self.post("recipes/operations/validate-feasibility", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Validated recipe feasibility from recipes service",
|
||||
recipe_id=recipe_id, quantity=quantity, tenant_id=tenant_id)
|
||||
@@ -196,7 +196,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
async def get_recipe_cost_analysis(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cost analysis for a recipe"""
|
||||
try:
|
||||
result = await self.get(f"recipes/{recipe_id}/cost-analysis", tenant_id=tenant_id)
|
||||
result = await self.get(f"recipes/recipes/{recipe_id}/cost-analysis", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved recipe cost analysis from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
@@ -210,7 +210,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
"""Optimize production batch to minimize waste and cost"""
|
||||
try:
|
||||
data = {"requirements": requirements}
|
||||
result = await self.post("optimize-batch", data=data, tenant_id=tenant_id)
|
||||
result = await self.post("recipes/operations/optimize-batch", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Optimized production batch from recipes service",
|
||||
requirements_count=len(requirements), tenant_id=tenant_id)
|
||||
@@ -227,7 +227,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
async def get_dashboard_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get recipes dashboard summary data"""
|
||||
try:
|
||||
result = await self.get("dashboard-summary", tenant_id=tenant_id)
|
||||
result = await self.get("recipes/dashboard/summary", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved recipes dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
@@ -241,7 +241,7 @@ class RecipesServiceClient(BaseServiceClient):
|
||||
"""Get most popular recipes based on production frequency"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
result = await self.get("popular-recipes", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("recipes/analytics/popular-recipes", tenant_id=tenant_id, params=params)
|
||||
recipes = result.get('recipes', []) if result else []
|
||||
logger.info("Retrieved popular recipes from recipes service",
|
||||
period=period, recipes_count=len(recipes), tenant_id=tenant_id)
|
||||
|
||||
@@ -44,7 +44,7 @@ class SalesServiceClient(BaseServiceClient):
|
||||
if product_id:
|
||||
params["product_id"] = product_id
|
||||
|
||||
result = await self.get("sales", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("sales/sales", tenant_id=tenant_id, params=params)
|
||||
return result.get("sales", []) if result else None
|
||||
|
||||
async def get_all_sales_data(
|
||||
@@ -72,7 +72,7 @@ class SalesServiceClient(BaseServiceClient):
|
||||
# Use the inherited paginated request method
|
||||
try:
|
||||
all_records = await self.get_paginated(
|
||||
"sales",
|
||||
"sales/sales",
|
||||
tenant_id=tenant_id,
|
||||
params=params,
|
||||
page_size=page_size,
|
||||
@@ -95,7 +95,7 @@ class SalesServiceClient(BaseServiceClient):
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Upload sales data"""
|
||||
data = {"sales": sales_data}
|
||||
return await self.post("sales", data=data, tenant_id=tenant_id)
|
||||
return await self.post("sales/sales", data=data, tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# PRODUCTS
|
||||
@@ -103,12 +103,12 @@ class SalesServiceClient(BaseServiceClient):
|
||||
|
||||
async def get_products(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get all products for a tenant"""
|
||||
result = await self.get("products", tenant_id=tenant_id)
|
||||
result = await self.get("sales/products", tenant_id=tenant_id)
|
||||
return result.get("products", []) if result else None
|
||||
|
||||
|
||||
async def get_product(self, tenant_id: str, product_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a specific product"""
|
||||
return await self.get(f"products/{product_id}", tenant_id=tenant_id)
|
||||
return await self.get(f"sales/products/{product_id}", tenant_id=tenant_id)
|
||||
|
||||
async def create_product(
|
||||
self,
|
||||
@@ -125,8 +125,8 @@ class SalesServiceClient(BaseServiceClient):
|
||||
"price": price,
|
||||
**kwargs
|
||||
}
|
||||
return await self.post("products", data=data, tenant_id=tenant_id)
|
||||
|
||||
return await self.post("sales/products", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def update_product(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -134,7 +134,7 @@ class SalesServiceClient(BaseServiceClient):
|
||||
**updates
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Update a product"""
|
||||
return await self.put(f"products/{product_id}", data=updates, tenant_id=tenant_id)
|
||||
return await self.put(f"sales/products/{product_id}", data=updates, tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# DATA IMPORT
|
||||
@@ -153,4 +153,4 @@ class SalesServiceClient(BaseServiceClient):
|
||||
"format": file_format,
|
||||
"filename": filename
|
||||
}
|
||||
return await self.post("import", data=data, tenant_id=tenant_id)
|
||||
return await self.post("sales/operations/import", data=data, tenant_id=tenant_id)
|
||||
@@ -28,7 +28,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def get_supplier_by_id(self, tenant_id: str, supplier_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get supplier details by ID"""
|
||||
try:
|
||||
result = await self.get(f"suppliers/{supplier_id}", tenant_id=tenant_id)
|
||||
result = await self.get(f"suppliers/list/{supplier_id}", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved supplier details from suppliers service",
|
||||
supplier_id=supplier_id, tenant_id=tenant_id)
|
||||
@@ -45,7 +45,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
if is_active is not None:
|
||||
params["is_active"] = is_active
|
||||
|
||||
result = await self.get_paginated("suppliers", tenant_id=tenant_id, params=params)
|
||||
result = await self.get_paginated("suppliers/list", tenant_id=tenant_id, params=params)
|
||||
logger.info("Retrieved all suppliers from suppliers service",
|
||||
suppliers_count=len(result), tenant_id=tenant_id)
|
||||
return result
|
||||
@@ -63,7 +63,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
if category:
|
||||
params["category"] = category
|
||||
|
||||
result = await self.get("suppliers/search", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("suppliers/list/search", tenant_id=tenant_id, params=params)
|
||||
suppliers = result.get('suppliers', []) if result else []
|
||||
logger.info("Searched suppliers from suppliers service",
|
||||
search_term=search, suppliers_count=len(suppliers), tenant_id=tenant_id)
|
||||
@@ -81,7 +81,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
"""Get supplier recommendations for procurement"""
|
||||
try:
|
||||
params = {"ingredient_id": ingredient_id}
|
||||
result = await self.get("recommendations", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("suppliers/recommendations", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved supplier recommendations from suppliers service",
|
||||
ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
@@ -98,7 +98,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
"ingredient_id": ingredient_id,
|
||||
"criteria": criteria or {}
|
||||
}
|
||||
result = await self.post("find-best-supplier", data=data, tenant_id=tenant_id)
|
||||
result = await self.post("suppliers/operations/find-best-supplier", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved best supplier from suppliers service",
|
||||
ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
@@ -115,7 +115,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def create_purchase_order(self, tenant_id: str, order_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Create a new purchase order"""
|
||||
try:
|
||||
result = await self.post("purchase-orders", data=order_data, tenant_id=tenant_id)
|
||||
result = await self.post("suppliers/purchase-orders", data=order_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Created purchase order",
|
||||
order_id=result.get('id'),
|
||||
@@ -136,7 +136,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
if supplier_id:
|
||||
params["supplier_id"] = supplier_id
|
||||
|
||||
result = await self.get("purchase-orders", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("suppliers/purchase-orders", tenant_id=tenant_id, params=params)
|
||||
orders = result.get('orders', []) if result else []
|
||||
logger.info("Retrieved purchase orders from suppliers service",
|
||||
orders_count=len(orders), tenant_id=tenant_id)
|
||||
@@ -150,7 +150,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
"""Update purchase order status"""
|
||||
try:
|
||||
data = {"status": status}
|
||||
result = await self.put(f"purchase-orders/{order_id}/status", data=data, tenant_id=tenant_id)
|
||||
result = await self.put(f"suppliers/purchase-orders/{order_id}/status", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Updated purchase order status",
|
||||
order_id=order_id, status=status, tenant_id=tenant_id)
|
||||
@@ -173,7 +173,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
if date:
|
||||
params["date"] = date
|
||||
|
||||
result = await self.get("deliveries", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("suppliers/deliveries", tenant_id=tenant_id, params=params)
|
||||
deliveries = result.get('deliveries', []) if result else []
|
||||
logger.info("Retrieved deliveries from suppliers service",
|
||||
deliveries_count=len(deliveries), tenant_id=tenant_id)
|
||||
@@ -190,7 +190,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
if notes:
|
||||
data["notes"] = notes
|
||||
|
||||
result = await self.put(f"deliveries/{delivery_id}/status", data=data, tenant_id=tenant_id)
|
||||
result = await self.put(f"suppliers/deliveries/{delivery_id}/status", data=data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Updated delivery status",
|
||||
delivery_id=delivery_id, status=status, tenant_id=tenant_id)
|
||||
@@ -203,7 +203,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def get_supplier_order_summaries(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get supplier order summaries for central bakery dashboard"""
|
||||
try:
|
||||
result = await self.get("supplier-order-summaries", tenant_id=tenant_id)
|
||||
result = await self.get("suppliers/dashboard/order-summaries", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved supplier order summaries from suppliers service",
|
||||
tenant_id=tenant_id)
|
||||
@@ -221,7 +221,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
"""Get supplier performance metrics"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
result = await self.get(f"suppliers/{supplier_id}/performance", tenant_id=tenant_id, params=params)
|
||||
result = await self.get(f"suppliers/analytics/performance/{supplier_id}", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved supplier performance from suppliers service",
|
||||
supplier_id=supplier_id, period=period, tenant_id=tenant_id)
|
||||
@@ -234,7 +234,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def get_performance_alerts(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get supplier performance alerts"""
|
||||
try:
|
||||
result = await self.get("performance-alerts", tenant_id=tenant_id)
|
||||
result = await self.get("suppliers/alerts/performance", tenant_id=tenant_id)
|
||||
alerts = result.get('alerts', []) if result else []
|
||||
logger.info("Retrieved supplier performance alerts",
|
||||
alerts_count=len(alerts), tenant_id=tenant_id)
|
||||
@@ -264,7 +264,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def get_dashboard_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get suppliers dashboard summary data"""
|
||||
try:
|
||||
result = await self.get("dashboard-summary", tenant_id=tenant_id)
|
||||
result = await self.get("suppliers/dashboard/summary", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved suppliers dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
@@ -281,7 +281,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
"start_date": start_date,
|
||||
"end_date": end_date
|
||||
}
|
||||
result = await self.get("cost-analysis", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("suppliers/analytics/cost-analysis", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
logger.info("Retrieved supplier cost analysis",
|
||||
start_date=start_date, end_date=end_date, tenant_id=tenant_id)
|
||||
@@ -294,7 +294,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def get_supplier_reliability_metrics(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get supplier reliability and quality metrics"""
|
||||
try:
|
||||
result = await self.get("reliability-metrics", tenant_id=tenant_id)
|
||||
result = await self.get("suppliers/analytics/reliability-metrics", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved supplier reliability metrics",
|
||||
tenant_id=tenant_id)
|
||||
@@ -311,7 +311,7 @@ class SuppliersServiceClient(BaseServiceClient):
|
||||
async def acknowledge_alert(self, tenant_id: str, alert_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Acknowledge a supplier-related alert"""
|
||||
try:
|
||||
result = await self.post(f"alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
|
||||
result = await self.post(f"suppliers/alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Acknowledged supplier alert",
|
||||
alert_id=alert_id, tenant_id=tenant_id)
|
||||
|
||||
@@ -37,12 +37,12 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
"min_data_points": min_data_points,
|
||||
**kwargs
|
||||
}
|
||||
return await self.post("jobs", data=data, tenant_id=tenant_id)
|
||||
return await self.post("training/jobs", data=data, tenant_id=tenant_id)
|
||||
|
||||
async def get_training_job(self, tenant_id: str, job_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get training job details"""
|
||||
return await self.get(f"jobs/{job_id}", tenant_id=tenant_id)
|
||||
|
||||
return await self.get(f"training/jobs/{job_id}/status", tenant_id=tenant_id)
|
||||
|
||||
async def list_training_jobs(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -53,13 +53,13 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
params = {"limit": limit}
|
||||
if status:
|
||||
params["status"] = status
|
||||
|
||||
result = await self.get("jobs", tenant_id=tenant_id, params=params)
|
||||
|
||||
result = await self.get("training/jobs", tenant_id=tenant_id, params=params)
|
||||
return result.get("jobs", []) if result else None
|
||||
|
||||
|
||||
async def cancel_training_job(self, tenant_id: str, job_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Cancel a training job"""
|
||||
return await self.delete(f"jobs/{job_id}", tenant_id=tenant_id)
|
||||
return await self.delete(f"training/jobs/{job_id}", tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# MODELS
|
||||
@@ -67,7 +67,7 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
|
||||
async def get_model(self, tenant_id: str, model_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get model details"""
|
||||
return await self.get(f"models/{model_id}", tenant_id=tenant_id)
|
||||
return await self.get(f"training/models/{model_id}", tenant_id=tenant_id)
|
||||
|
||||
async def list_models(
|
||||
self,
|
||||
@@ -83,7 +83,7 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
if model_type:
|
||||
params["model_type"] = model_type
|
||||
|
||||
result = await self.get("models", tenant_id=tenant_id, params=params)
|
||||
result = await self.get("training/models", tenant_id=tenant_id, params=params)
|
||||
return result.get("models", []) if result else None
|
||||
|
||||
async def get_active_model_for_product(
|
||||
@@ -95,16 +95,16 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
Get the active model for a specific product by inventory product ID
|
||||
This is the preferred method since models are stored per product.
|
||||
"""
|
||||
result = await self.get(f"models/{inventory_product_id}/active", tenant_id=tenant_id)
|
||||
result = await self.get(f"training/models/{inventory_product_id}/active", tenant_id=tenant_id)
|
||||
return result
|
||||
|
||||
async def deploy_model(self, tenant_id: str, model_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Deploy a trained model"""
|
||||
return await self.post(f"models/{model_id}/deploy", data={}, tenant_id=tenant_id)
|
||||
return await self.post(f"training/models/{model_id}/deploy", data={}, tenant_id=tenant_id)
|
||||
|
||||
async def delete_model(self, tenant_id: str, model_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Delete a model"""
|
||||
return await self.delete(f"models/{model_id}", tenant_id=tenant_id)
|
||||
return await self.delete(f"training/models/{model_id}", tenant_id=tenant_id)
|
||||
|
||||
# ================================================================
|
||||
# MODEL METRICS & PERFORMANCE
|
||||
@@ -112,7 +112,7 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
|
||||
async def get_model_metrics(self, tenant_id: str, model_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get model performance metrics"""
|
||||
return await self.get(f"models/{model_id}/metrics", tenant_id=tenant_id)
|
||||
return await self.get(f"training/models/{model_id}/metrics", tenant_id=tenant_id)
|
||||
|
||||
async def get_model_predictions(
|
||||
self,
|
||||
@@ -128,5 +128,5 @@ class TrainingServiceClient(BaseServiceClient):
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
|
||||
result = await self.get(f"models/{model_id}/predictions", tenant_id=tenant_id, params=params)
|
||||
result = await self.get(f"training/models/{model_id}/predictions", tenant_id=tenant_id, params=params)
|
||||
return result.get("predictions", []) if result else None
|
||||
15
shared/routing/__init__.py
Normal file
15
shared/routing/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Shared routing utilities for consistent URL structure across services
|
||||
"""
|
||||
|
||||
from shared.routing.route_builder import RouteBuilder, RouteCategory
|
||||
from shared.routing.route_helpers import build_base_route, build_dashboard_route, build_analytics_route, build_operations_route
|
||||
|
||||
__all__ = [
|
||||
'RouteBuilder',
|
||||
'RouteCategory',
|
||||
'build_base_route',
|
||||
'build_dashboard_route',
|
||||
'build_analytics_route',
|
||||
'build_operations_route',
|
||||
]
|
||||
295
shared/routing/route_builder.py
Normal file
295
shared/routing/route_builder.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Route Builder for standardized URL structure
|
||||
Ensures consistent API patterns across all microservices
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RouteCategory(Enum):
|
||||
"""Categories of API routes with different access patterns"""
|
||||
BASE = "base" # Atomic CRUD operations on resources
|
||||
DASHBOARD = "dashboard" # Dashboard data and summaries
|
||||
ANALYTICS = "analytics" # Analytics endpoints (tier-gated)
|
||||
OPERATIONS = "operations" # Service-specific operations
|
||||
|
||||
|
||||
class RouteBuilder:
|
||||
"""
|
||||
Builder for creating standardized API routes
|
||||
|
||||
URL Structure:
|
||||
- Base: /api/v1/tenants/{tenant_id}/{service}/{resource}
|
||||
- Dashboard: /api/v1/tenants/{tenant_id}/{service}/dashboard/{operation}
|
||||
- Analytics: /api/v1/tenants/{tenant_id}/{service}/analytics/{operation}
|
||||
- Operations: /api/v1/tenants/{tenant_id}/{service}/operations/{operation}
|
||||
"""
|
||||
|
||||
API_VERSION = "v1"
|
||||
BASE_PATH = f"/api/{API_VERSION}"
|
||||
|
||||
def __init__(self, service_name: str):
|
||||
"""
|
||||
Initialize route builder for a specific service
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (e.g., 'inventory', 'production')
|
||||
"""
|
||||
self.service_name = service_name
|
||||
|
||||
def build_base_route(self, resource: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build base CRUD route for a resource
|
||||
|
||||
Args:
|
||||
resource: Resource name (e.g., 'ingredients', 'batches')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_base_route('ingredients')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/{resource}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/{resource}"
|
||||
|
||||
def build_dashboard_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build dashboard route
|
||||
|
||||
Args:
|
||||
operation: Dashboard operation (e.g., 'summary', 'capacity-status')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('production')
|
||||
builder.build_dashboard_route('summary')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/dashboard/summary'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/dashboard/{operation}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/dashboard/{operation}"
|
||||
|
||||
def build_analytics_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build analytics route (tier-gated: professional/enterprise)
|
||||
|
||||
Args:
|
||||
operation: Analytics operation (e.g., 'equipment-efficiency', 'trends')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('production')
|
||||
builder.build_analytics_route('equipment-efficiency')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/analytics/equipment-efficiency'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/analytics/{operation}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/analytics/{operation}"
|
||||
|
||||
def build_operations_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build service operations route
|
||||
|
||||
Args:
|
||||
operation: Operation name (e.g., 'schedule-batch', 'stock-adjustment')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('production')
|
||||
builder.build_operations_route('schedule-batch')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/operations/schedule-batch'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/operations/{operation}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/operations/{operation}"
|
||||
|
||||
def build_resource_detail_route(self, resource: str, id_param: str = "id", include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build route for individual resource details
|
||||
|
||||
Args:
|
||||
resource: Resource name
|
||||
id_param: Name of the ID parameter
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_resource_detail_route('ingredients', 'ingredient_id')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}'
|
||||
"""
|
||||
base = self.build_base_route(resource, include_tenant_prefix)
|
||||
return f"{base}/{{{id_param}}}"
|
||||
|
||||
def build_nested_resource_route(
|
||||
self,
|
||||
parent_resource: str,
|
||||
parent_id_param: str,
|
||||
child_resource: str,
|
||||
include_tenant_prefix: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Build route for nested resources
|
||||
|
||||
Args:
|
||||
parent_resource: Parent resource name
|
||||
parent_id_param: Parent ID parameter name
|
||||
child_resource: Child resource name
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_nested_resource_route('ingredients', 'ingredient_id', 'stock')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}/stock'
|
||||
"""
|
||||
base = self.build_resource_detail_route(parent_resource, parent_id_param, include_tenant_prefix)
|
||||
return f"{base}/{child_resource}"
|
||||
|
||||
def build_resource_action_route(
|
||||
self,
|
||||
resource: str,
|
||||
id_param: str,
|
||||
action: str,
|
||||
include_tenant_prefix: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Build route for resource-specific actions
|
||||
|
||||
Args:
|
||||
resource: Resource name
|
||||
id_param: ID parameter name
|
||||
action: Action name
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('pos')
|
||||
builder.build_resource_action_route('configurations', 'config_id', 'test-connection')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/pos/configurations/{config_id}/test-connection'
|
||||
"""
|
||||
base = self.build_resource_detail_route(resource, id_param, include_tenant_prefix)
|
||||
return f"{base}/{action}"
|
||||
|
||||
def build_global_route(self, path: str) -> str:
|
||||
"""
|
||||
Build global route without tenant context
|
||||
|
||||
Args:
|
||||
path: Path after service name
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('pos')
|
||||
builder.build_global_route('supported-systems')
|
||||
# Returns: '/api/v1/pos/supported-systems'
|
||||
"""
|
||||
return f"{self.BASE_PATH}/{self.service_name}/{path}"
|
||||
|
||||
def build_webhook_route(self, path: str) -> str:
|
||||
"""
|
||||
Build webhook route (no tenant context)
|
||||
|
||||
Args:
|
||||
path: Webhook path
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('pos')
|
||||
builder.build_webhook_route('{pos_system}')
|
||||
# Returns: '/api/v1/webhooks/{pos_system}'
|
||||
"""
|
||||
return f"{self.BASE_PATH}/webhooks/{path}"
|
||||
|
||||
def build_custom_route(
|
||||
self,
|
||||
category: RouteCategory,
|
||||
path_segments: list,
|
||||
include_tenant_prefix: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Build custom route with specified category and path segments
|
||||
|
||||
Args:
|
||||
category: Route category
|
||||
path_segments: List of path segments after category
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_custom_route(RouteCategory.DASHBOARD, ['food-safety', 'compliance'])
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/dashboard/food-safety/compliance'
|
||||
"""
|
||||
base_prefix = f"{self.BASE_PATH}/tenants/{{tenant_id}}" if include_tenant_prefix else self.BASE_PATH
|
||||
|
||||
if category == RouteCategory.BASE:
|
||||
return f"{base_prefix}/{self.service_name}/{'/'.join(path_segments)}"
|
||||
elif category == RouteCategory.DASHBOARD:
|
||||
return f"{base_prefix}/{self.service_name}/dashboard/{'/'.join(path_segments)}"
|
||||
elif category == RouteCategory.ANALYTICS:
|
||||
return f"{base_prefix}/{self.service_name}/analytics/{'/'.join(path_segments)}"
|
||||
elif category == RouteCategory.OPERATIONS:
|
||||
return f"{base_prefix}/{self.service_name}/operations/{'/'.join(path_segments)}"
|
||||
|
||||
# Fallback to base
|
||||
return f"{base_prefix}/{self.service_name}/{'/'.join(path_segments)}"
|
||||
|
||||
@staticmethod
|
||||
def get_route_pattern(category: RouteCategory, service_name: Optional[str] = None) -> str:
|
||||
"""
|
||||
Get regex pattern for matching routes of a specific category
|
||||
|
||||
Args:
|
||||
category: Route category
|
||||
service_name: Optional service name to filter
|
||||
|
||||
Returns:
|
||||
Regex pattern for route matching
|
||||
|
||||
Example:
|
||||
RouteBuilder.get_route_pattern(RouteCategory.ANALYTICS)
|
||||
# Returns: r'^/api/v1/tenants/[^/]+/[^/]+/analytics/.*'
|
||||
|
||||
RouteBuilder.get_route_pattern(RouteCategory.ANALYTICS, 'production')
|
||||
# Returns: r'^/api/v1/tenants/[^/]+/production/analytics/.*'
|
||||
"""
|
||||
service_pattern = service_name if service_name else "[^/]+"
|
||||
|
||||
if category == RouteCategory.BASE:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/(?!dashboard|analytics|operations)[^/]+.*"
|
||||
elif category == RouteCategory.DASHBOARD:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/dashboard/.*"
|
||||
elif category == RouteCategory.ANALYTICS:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/analytics/.*"
|
||||
elif category == RouteCategory.OPERATIONS:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/operations/.*"
|
||||
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/.*"
|
||||
211
shared/routing/route_helpers.py
Normal file
211
shared/routing/route_helpers.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Helper functions for route building
|
||||
Provides convenience methods for common routing patterns
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def build_base_route(service: str, resource: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a base CRUD route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
resource: Resource name
|
||||
tenant_id: Optional tenant ID (if None, uses {tenant_id} placeholder)
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_base_route('inventory', 'ingredients')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients'
|
||||
|
||||
build_base_route('inventory', 'ingredients', 'uuid-123')
|
||||
# Returns: '/api/v1/tenants/uuid-123/inventory/ingredients'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/{resource}"
|
||||
|
||||
|
||||
def build_dashboard_route(service: str, operation: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a dashboard route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
operation: Dashboard operation
|
||||
tenant_id: Optional tenant ID
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_dashboard_route('production', 'summary')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/dashboard/summary'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/dashboard/{operation}"
|
||||
|
||||
|
||||
def build_analytics_route(service: str, operation: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build an analytics route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
operation: Analytics operation
|
||||
tenant_id: Optional tenant ID
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_analytics_route('production', 'equipment-efficiency')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/analytics/equipment-efficiency'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/analytics/{operation}"
|
||||
|
||||
|
||||
def build_operations_route(service: str, operation: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a service operations route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
operation: Operation name
|
||||
tenant_id: Optional tenant ID
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_operations_route('production', 'schedule-batch')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/operations/schedule-batch'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/operations/{operation}"
|
||||
|
||||
|
||||
def build_resource_detail_route(
|
||||
service: str,
|
||||
resource: str,
|
||||
resource_id: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
id_param_name: str = "id"
|
||||
) -> str:
|
||||
"""
|
||||
Build a route for individual resource details
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
resource: Resource name
|
||||
resource_id: Optional resource ID (if None, uses parameter name)
|
||||
tenant_id: Optional tenant ID
|
||||
id_param_name: Name of ID parameter when resource_id is None
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_resource_detail_route('inventory', 'ingredients', id_param_name='ingredient_id')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}'
|
||||
|
||||
build_resource_detail_route('inventory', 'ingredients', 'uuid-456', 'uuid-123')
|
||||
# Returns: '/api/v1/tenants/uuid-123/inventory/ingredients/uuid-456'
|
||||
"""
|
||||
base = build_base_route(service, resource, tenant_id)
|
||||
id_part = resource_id if resource_id else f"{{{id_param_name}}}"
|
||||
return f"{base}/{id_part}"
|
||||
|
||||
|
||||
def build_nested_route(
|
||||
service: str,
|
||||
parent_resource: str,
|
||||
child_resource: str,
|
||||
parent_id: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
parent_id_param: str = "parent_id"
|
||||
) -> str:
|
||||
"""
|
||||
Build a route for nested resources
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
parent_resource: Parent resource name
|
||||
child_resource: Child resource name
|
||||
parent_id: Optional parent resource ID
|
||||
tenant_id: Optional tenant ID
|
||||
parent_id_param: Parent ID parameter name when parent_id is None
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_nested_route('inventory', 'ingredients', 'stock', parent_id_param='ingredient_id')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}/stock'
|
||||
"""
|
||||
parent = build_resource_detail_route(service, parent_resource, parent_id, tenant_id, parent_id_param)
|
||||
return f"{parent}/{child_resource}"
|
||||
|
||||
|
||||
def extract_tenant_id_from_route(route_path: str) -> Optional[str]:
|
||||
"""
|
||||
Extract tenant ID from a route path
|
||||
|
||||
Args:
|
||||
route_path: Route path containing tenant ID
|
||||
|
||||
Returns:
|
||||
Tenant ID if found, None otherwise
|
||||
|
||||
Example:
|
||||
extract_tenant_id_from_route('/api/v1/tenants/uuid-123/inventory/ingredients')
|
||||
# Returns: 'uuid-123'
|
||||
"""
|
||||
import re
|
||||
match = re.search(r'/api/v1/tenants/([^/]+)/', route_path)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def extract_service_from_route(route_path: str) -> Optional[str]:
|
||||
"""
|
||||
Extract service name from a route path
|
||||
|
||||
Args:
|
||||
route_path: Route path containing service name
|
||||
|
||||
Returns:
|
||||
Service name if found, None otherwise
|
||||
|
||||
Example:
|
||||
extract_service_from_route('/api/v1/tenants/uuid-123/inventory/ingredients')
|
||||
# Returns: 'inventory'
|
||||
"""
|
||||
import re
|
||||
match = re.search(r'/api/v1/tenants/[^/]+/([^/]+)/', route_path)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def is_analytics_route(route_path: str) -> bool:
|
||||
"""Check if route is an analytics route"""
|
||||
return '/analytics/' in route_path
|
||||
|
||||
|
||||
def is_dashboard_route(route_path: str) -> bool:
|
||||
"""Check if route is a dashboard route"""
|
||||
return '/dashboard/' in route_path
|
||||
|
||||
|
||||
def is_operations_route(route_path: str) -> bool:
|
||||
"""Check if route is an operations route"""
|
||||
return '/operations/' in route_path
|
||||
|
||||
|
||||
def is_base_crud_route(route_path: str) -> bool:
|
||||
"""Check if route is a base CRUD route"""
|
||||
return not (is_analytics_route(route_path) or
|
||||
is_dashboard_route(route_path) or
|
||||
is_operations_route(route_path))
|
||||
Reference in New Issue
Block a user