REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View 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'])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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',
]

View 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}/.*"

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