Files
bakery-ia/shared/utils/tenant_settings_client.py

361 lines
12 KiB
Python
Raw Permalink Normal View History

# shared/utils/tenant_settings_client.py
"""
Tenant Settings Client
Shared utility for services to fetch tenant-specific settings from Tenant Service
Includes Redis caching for performance
"""
import httpx
import json
from typing import Dict, Any, Optional
from uuid import UUID
import redis.asyncio as aioredis
from datetime import timedelta
import logging
logger = logging.getLogger(__name__)
class TenantSettingsClient:
"""
Client for fetching tenant settings from Tenant Service
Features:
- HTTP client to fetch settings from Tenant Service API
- Redis caching with configurable TTL (default 5 minutes)
- Automatic cache invalidation support
- Fallback to defaults if Tenant Service is unavailable
"""
def __init__(
self,
tenant_service_url: str,
redis_client: Optional[aioredis.Redis] = None,
cache_ttl: int = 300, # 5 minutes default
http_timeout: int = 10
):
"""
Initialize TenantSettingsClient
Args:
tenant_service_url: Base URL of Tenant Service (e.g., "http://tenant-service:8000")
redis_client: Optional Redis client for caching
cache_ttl: Cache TTL in seconds (default 300 = 5 minutes)
http_timeout: HTTP request timeout in seconds
"""
self.tenant_service_url = tenant_service_url.rstrip('/')
self.redis = redis_client
self.cache_ttl = cache_ttl
self.http_timeout = http_timeout
# HTTP client with connection pooling
self.http_client = httpx.AsyncClient(
timeout=http_timeout,
limits=httpx.Limits(max_keepalive_connections=20, max_connections=100)
)
async def get_procurement_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get procurement settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with procurement settings
"""
return await self._get_category_settings(tenant_id, "procurement")
async def get_inventory_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get inventory settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with inventory settings
"""
return await self._get_category_settings(tenant_id, "inventory")
async def get_production_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get production settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with production settings
"""
return await self._get_category_settings(tenant_id, "production")
async def get_supplier_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get supplier settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with supplier settings
"""
return await self._get_category_settings(tenant_id, "supplier")
async def get_pos_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get POS settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with POS settings
"""
return await self._get_category_settings(tenant_id, "pos")
async def get_order_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get order settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with order settings
"""
return await self._get_category_settings(tenant_id, "order")
async def get_all_settings(self, tenant_id: UUID) -> Dict[str, Any]:
"""
Get all settings for a tenant
Args:
tenant_id: UUID of the tenant
Returns:
Dictionary with all setting categories
"""
cache_key = f"tenant_settings:{tenant_id}:all"
# Try cache first
if self.redis:
cached = await self._get_from_cache(cache_key)
if cached:
return cached
# Fetch from Tenant Service
try:
url = f"{self.tenant_service_url}/api/v1/tenants/{tenant_id}/settings"
response = await self.http_client.get(url)
response.raise_for_status()
settings = response.json()
# Cache the result
if self.redis:
await self._set_in_cache(cache_key, settings)
return settings
except Exception as e:
logger.error(f"Failed to fetch all settings for tenant {tenant_id}: {e}")
return self._get_default_settings()
async def invalidate_cache(self, tenant_id: UUID, category: Optional[str] = None):
"""
Invalidate cache for a tenant's settings
Args:
tenant_id: UUID of the tenant
category: Optional category to invalidate. If None, invalidates all categories.
"""
if not self.redis:
return
if category:
cache_key = f"tenant_settings:{tenant_id}:{category}"
await self.redis.delete(cache_key)
logger.info(f"Invalidated cache for tenant {tenant_id}, category {category}")
else:
# Invalidate all categories
pattern = f"tenant_settings:{tenant_id}:*"
keys = await self.redis.keys(pattern)
if keys:
await self.redis.delete(*keys)
logger.info(f"Invalidated all cached settings for tenant {tenant_id}")
async def _get_category_settings(self, tenant_id: UUID, category: str) -> Dict[str, Any]:
"""
Internal method to fetch settings for a specific category
Args:
tenant_id: UUID of the tenant
category: Category name
Returns:
Dictionary with category settings
"""
cache_key = f"tenant_settings:{tenant_id}:{category}"
# Try cache first
if self.redis:
cached = await self._get_from_cache(cache_key)
if cached:
return cached
# Fetch from Tenant Service
try:
url = f"{self.tenant_service_url}/api/v1/tenants/{tenant_id}/settings/{category}"
response = await self.http_client.get(url)
response.raise_for_status()
data = response.json()
settings = data.get("settings", {})
# Cache the result
if self.redis:
await self._set_in_cache(cache_key, settings)
return settings
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
logger.warning(f"Settings not found for tenant {tenant_id}, using defaults")
else:
logger.error(f"HTTP error fetching {category} settings for tenant {tenant_id}: {e}")
return self._get_default_category_settings(category)
except Exception as e:
logger.error(f"Failed to fetch {category} settings for tenant {tenant_id}: {e}")
return self._get_default_category_settings(category)
async def _get_from_cache(self, key: str) -> Optional[Dict[str, Any]]:
"""Get value from Redis cache"""
try:
cached = await self.redis.get(key)
if cached:
return json.loads(cached)
except Exception as e:
logger.warning(f"Redis get error for key {key}: {e}")
return None
async def _set_in_cache(self, key: str, value: Dict[str, Any]):
"""Set value in Redis cache with TTL"""
try:
await self.redis.setex(
key,
timedelta(seconds=self.cache_ttl),
json.dumps(value)
)
except Exception as e:
logger.warning(f"Redis set error for key {key}: {e}")
def _get_default_category_settings(self, category: str) -> Dict[str, Any]:
"""Get default settings for a category as fallback"""
defaults = self._get_default_settings()
return defaults.get(f"{category}_settings", {})
def _get_default_settings(self) -> Dict[str, Any]:
"""Get default settings for all categories as fallback"""
return {
"procurement_settings": {
"auto_approve_enabled": True,
"auto_approve_threshold_eur": 500.0,
"auto_approve_min_supplier_score": 0.80,
"require_approval_new_suppliers": True,
"require_approval_critical_items": True,
"procurement_lead_time_days": 3,
"demand_forecast_days": 14,
"safety_stock_percentage": 20.0,
"po_approval_reminder_hours": 24,
"po_critical_escalation_hours": 12
},
"inventory_settings": {
"low_stock_threshold": 10,
"reorder_point": 20,
"reorder_quantity": 50,
"expiring_soon_days": 7,
"expiration_warning_days": 3,
"quality_score_threshold": 8.0,
"temperature_monitoring_enabled": True,
"refrigeration_temp_min": 1.0,
"refrigeration_temp_max": 4.0,
"freezer_temp_min": -20.0,
"freezer_temp_max": -15.0,
"room_temp_min": 18.0,
"room_temp_max": 25.0,
"temp_deviation_alert_minutes": 15,
"critical_temp_deviation_minutes": 5
},
"production_settings": {
"planning_horizon_days": 7,
"minimum_batch_size": 1.0,
"maximum_batch_size": 100.0,
"production_buffer_percentage": 10.0,
"working_hours_per_day": 12,
"max_overtime_hours": 4,
"capacity_utilization_target": 0.85,
"capacity_warning_threshold": 0.95,
"quality_check_enabled": True,
"minimum_yield_percentage": 85.0,
"quality_score_threshold": 8.0,
"schedule_optimization_enabled": True,
"prep_time_buffer_minutes": 30,
"cleanup_time_buffer_minutes": 15,
"labor_cost_per_hour_eur": 15.0,
"overhead_cost_percentage": 20.0
},
"supplier_settings": {
"default_payment_terms_days": 30,
"default_delivery_days": 3,
"excellent_delivery_rate": 95.0,
"good_delivery_rate": 90.0,
"excellent_quality_rate": 98.0,
"good_quality_rate": 95.0,
"critical_delivery_delay_hours": 24,
"critical_quality_rejection_rate": 10.0,
"high_cost_variance_percentage": 15.0
},
"pos_settings": {
"sync_interval_minutes": 5,
"auto_sync_products": True,
"auto_sync_transactions": True
},
"order_settings": {
"max_discount_percentage": 50.0,
"default_delivery_window_hours": 48,
"dynamic_pricing_enabled": False,
"discount_enabled": True,
"delivery_tracking_enabled": True
}
}
async def close(self):
"""Close HTTP client connections"""
await self.http_client.aclose()
# Factory function for easy instantiation
def create_tenant_settings_client(
tenant_service_url: str,
redis_client: Optional[aioredis.Redis] = None,
cache_ttl: int = 300
) -> TenantSettingsClient:
"""
Factory function to create a TenantSettingsClient
Args:
tenant_service_url: Base URL of Tenant Service
redis_client: Optional Redis client for caching
cache_ttl: Cache TTL in seconds
Returns:
TenantSettingsClient instance
"""
return TenantSettingsClient(
tenant_service_url=tenant_service_url,
redis_client=redis_client,
cache_ttl=cache_ttl
)