Add more services
This commit is contained in:
@@ -9,6 +9,11 @@ from .training_client import TrainingServiceClient
|
||||
from .sales_client import SalesServiceClient
|
||||
from .external_client import ExternalServiceClient
|
||||
from .forecast_client import ForecastServiceClient
|
||||
from .inventory_client import InventoryServiceClient
|
||||
from .orders_client import OrdersServiceClient
|
||||
from .production_client import ProductionServiceClient
|
||||
from .recipes_client import RecipesServiceClient
|
||||
from .suppliers_client import SuppliersServiceClient
|
||||
|
||||
# Import config
|
||||
from shared.config.base import BaseServiceSettings
|
||||
@@ -56,6 +61,56 @@ def get_forecast_client(config: BaseServiceSettings = None, service_name: str =
|
||||
_client_cache[cache_key] = ForecastServiceClient(config, service_name)
|
||||
return _client_cache[cache_key]
|
||||
|
||||
def get_inventory_client(config: BaseServiceSettings = None, service_name: str = "unknown") -> InventoryServiceClient:
|
||||
"""Get or create an inventory service client"""
|
||||
if config is None:
|
||||
from app.core.config import settings as config
|
||||
|
||||
cache_key = f"inventory_{service_name}"
|
||||
if cache_key not in _client_cache:
|
||||
_client_cache[cache_key] = InventoryServiceClient(config)
|
||||
return _client_cache[cache_key]
|
||||
|
||||
def get_orders_client(config: BaseServiceSettings = None, service_name: str = "unknown") -> OrdersServiceClient:
|
||||
"""Get or create an orders service client"""
|
||||
if config is None:
|
||||
from app.core.config import settings as config
|
||||
|
||||
cache_key = f"orders_{service_name}"
|
||||
if cache_key not in _client_cache:
|
||||
_client_cache[cache_key] = OrdersServiceClient(config)
|
||||
return _client_cache[cache_key]
|
||||
|
||||
def get_production_client(config: BaseServiceSettings = None, service_name: str = "unknown") -> ProductionServiceClient:
|
||||
"""Get or create a production service client"""
|
||||
if config is None:
|
||||
from app.core.config import settings as config
|
||||
|
||||
cache_key = f"production_{service_name}"
|
||||
if cache_key not in _client_cache:
|
||||
_client_cache[cache_key] = ProductionServiceClient(config)
|
||||
return _client_cache[cache_key]
|
||||
|
||||
def get_recipes_client(config: BaseServiceSettings = None, service_name: str = "unknown") -> RecipesServiceClient:
|
||||
"""Get or create a recipes service client"""
|
||||
if config is None:
|
||||
from app.core.config import settings as config
|
||||
|
||||
cache_key = f"recipes_{service_name}"
|
||||
if cache_key not in _client_cache:
|
||||
_client_cache[cache_key] = RecipesServiceClient(config)
|
||||
return _client_cache[cache_key]
|
||||
|
||||
def get_suppliers_client(config: BaseServiceSettings = None, service_name: str = "unknown") -> SuppliersServiceClient:
|
||||
"""Get or create a suppliers service client"""
|
||||
if config is None:
|
||||
from app.core.config import settings as config
|
||||
|
||||
cache_key = f"suppliers_{service_name}"
|
||||
if cache_key not in _client_cache:
|
||||
_client_cache[cache_key] = SuppliersServiceClient(config)
|
||||
return _client_cache[cache_key]
|
||||
|
||||
|
||||
class ServiceClients:
|
||||
"""Convenient wrapper for all service clients"""
|
||||
@@ -69,6 +124,11 @@ class ServiceClients:
|
||||
self._sales_client = None
|
||||
self._external_client = None
|
||||
self._forecast_client = None
|
||||
self._inventory_client = None
|
||||
self._orders_client = None
|
||||
self._production_client = None
|
||||
self._recipes_client = None
|
||||
self._suppliers_client = None
|
||||
|
||||
def _get_default_config(self):
|
||||
"""Get default config from app settings"""
|
||||
@@ -105,6 +165,41 @@ class ServiceClients:
|
||||
if self._forecast_client is None:
|
||||
self._forecast_client = get_forecast_client(self.config, self.service_name)
|
||||
return self._forecast_client
|
||||
|
||||
@property
|
||||
def inventory(self) -> InventoryServiceClient:
|
||||
"""Get inventory service client"""
|
||||
if self._inventory_client is None:
|
||||
self._inventory_client = get_inventory_client(self.config, self.service_name)
|
||||
return self._inventory_client
|
||||
|
||||
@property
|
||||
def orders(self) -> OrdersServiceClient:
|
||||
"""Get orders service client"""
|
||||
if self._orders_client is None:
|
||||
self._orders_client = get_orders_client(self.config, self.service_name)
|
||||
return self._orders_client
|
||||
|
||||
@property
|
||||
def production(self) -> ProductionServiceClient:
|
||||
"""Get production service client"""
|
||||
if self._production_client is None:
|
||||
self._production_client = get_production_client(self.config, self.service_name)
|
||||
return self._production_client
|
||||
|
||||
@property
|
||||
def recipes(self) -> RecipesServiceClient:
|
||||
"""Get recipes service client"""
|
||||
if self._recipes_client is None:
|
||||
self._recipes_client = get_recipes_client(self.config, self.service_name)
|
||||
return self._recipes_client
|
||||
|
||||
@property
|
||||
def suppliers(self) -> SuppliersServiceClient:
|
||||
"""Get suppliers service client"""
|
||||
if self._suppliers_client is None:
|
||||
self._suppliers_client = get_suppliers_client(self.config, self.service_name)
|
||||
return self._suppliers_client
|
||||
|
||||
# Convenience function to get all clients
|
||||
def get_service_clients(config: BaseServiceSettings = None, service_name: str = "unknown") -> ServiceClients:
|
||||
@@ -119,10 +214,20 @@ __all__ = [
|
||||
'SalesServiceClient',
|
||||
'ExternalServiceClient',
|
||||
'ForecastServiceClient',
|
||||
'InventoryServiceClient',
|
||||
'OrdersServiceClient',
|
||||
'ProductionServiceClient',
|
||||
'RecipesServiceClient',
|
||||
'SuppliersServiceClient',
|
||||
'ServiceClients',
|
||||
'get_training_client',
|
||||
'get_sales_client',
|
||||
'get_external_client',
|
||||
'get_forecast_client',
|
||||
'get_inventory_client',
|
||||
'get_orders_client',
|
||||
'get_production_client',
|
||||
'get_recipes_client',
|
||||
'get_suppliers_client',
|
||||
'get_service_clients'
|
||||
]
|
||||
251
shared/clients/orders_client.py
Normal file
251
shared/clients/orders_client.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# shared/clients/orders_client.py
|
||||
"""
|
||||
Orders Service Client for Inter-Service Communication
|
||||
Provides access to orders and procurement planning from other services
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import Dict, Any, Optional, List
|
||||
from uuid import UUID
|
||||
from shared.clients.base_service_client import BaseServiceClient
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
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",
|
||||
orders_count=orders_count, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved orders dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
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:
|
||||
params = {
|
||||
"start_date": start_date,
|
||||
"end_date": end_date
|
||||
}
|
||||
result = await self.get("order-trends", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
alerts = result.get('alerts', []) if result else []
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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:
|
||||
data = {
|
||||
"order_ids": order_ids,
|
||||
"format": format_type,
|
||||
"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)
|
||||
if result:
|
||||
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",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if orders service is healthy"""
|
||||
try:
|
||||
result = await self.get("../health") # Health endpoint is not tenant-scoped
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error("Orders service health check failed", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Factory function for dependency injection
|
||||
def create_orders_client(config: BaseServiceSettings) -> OrdersServiceClient:
|
||||
"""Create orders service client instance"""
|
||||
return OrdersServiceClient(config)
|
||||
294
shared/clients/production_client.py
Normal file
294
shared/clients/production_client.py
Normal file
@@ -0,0 +1,294 @@
|
||||
# shared/clients/production_client.py
|
||||
"""
|
||||
Production Service Client for Inter-Service Communication
|
||||
Provides access to production planning and batch management from other services
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import Dict, Any, Optional, List
|
||||
from uuid import UUID
|
||||
from shared.clients.base_service_client import BaseServiceClient
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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:
|
||||
params = {}
|
||||
if start_date:
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
|
||||
result = await self.get("schedule", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
batches = result.get('batches', []) if result else []
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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},
|
||||
tenant_id=tenant_id)
|
||||
if result:
|
||||
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",
|
||||
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,
|
||||
tenant_id=tenant_id)
|
||||
if result:
|
||||
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",
|
||||
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:
|
||||
params = {
|
||||
"start_date": start_date,
|
||||
"end_date": end_date
|
||||
}
|
||||
result = await self.get("metrics/yield", tenant_id=tenant_id, params=params)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved production dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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)
|
||||
alerts = result.get('alerts', []) if result else []
|
||||
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",
|
||||
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)
|
||||
if result:
|
||||
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",
|
||||
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:
|
||||
result = await self.get("../health") # Health endpoint is not tenant-scoped
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error("Production service health check failed", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Factory function for dependency injection
|
||||
def create_production_client(config: BaseServiceSettings) -> ProductionServiceClient:
|
||||
"""Create production service client instance"""
|
||||
return ProductionServiceClient(config)
|
||||
271
shared/clients/recipes_client.py
Normal file
271
shared/clients/recipes_client.py
Normal file
@@ -0,0 +1,271 @@
|
||||
# shared/clients/recipes_client.py
|
||||
"""
|
||||
Recipes Service Client for Inter-Service Communication
|
||||
Provides access to recipe and ingredient requirements from other services
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import Dict, Any, Optional, List
|
||||
from uuid import UUID
|
||||
from shared.clients.base_service_client import BaseServiceClient
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class RecipesServiceClient(BaseServiceClient):
|
||||
"""Client for communicating with the Recipes Service"""
|
||||
|
||||
def __init__(self, config: BaseServiceSettings):
|
||||
super().__init__("recipes", config)
|
||||
|
||||
def get_service_base_path(self) -> str:
|
||||
return "/api/v1"
|
||||
|
||||
# ================================================================
|
||||
# RECIPE MANAGEMENT
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved recipe details from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipe details",
|
||||
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_recipes_by_product_ids(self, tenant_id: str, product_ids: List[str]) -> Optional[List[Dict[str, Any]]]:
|
||||
"""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)
|
||||
recipes = result.get('recipes', []) if result else []
|
||||
logger.info("Retrieved recipes by product IDs from recipes service",
|
||||
product_ids_count=len(product_ids),
|
||||
recipes_count=len(recipes),
|
||||
tenant_id=tenant_id)
|
||||
return recipes
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipes by product IDs",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
async def get_all_recipes(self, tenant_id: str, is_active: Optional[bool] = True) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get all recipes for a tenant"""
|
||||
try:
|
||||
params = {}
|
||||
if is_active is not None:
|
||||
params["is_active"] = is_active
|
||||
|
||||
result = await self.get_paginated("recipes", tenant_id=tenant_id, params=params)
|
||||
logger.info("Retrieved all recipes from recipes service",
|
||||
recipes_count=len(result), tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting all recipes",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
# ================================================================
|
||||
# INGREDIENT REQUIREMENTS
|
||||
# ================================================================
|
||||
|
||||
async def get_recipe_requirements(self, tenant_id: str, recipe_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get ingredient requirements for recipes"""
|
||||
try:
|
||||
params = {}
|
||||
if recipe_ids:
|
||||
params["recipe_ids"] = ",".join(recipe_ids)
|
||||
|
||||
result = await self.get("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,
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipe requirements",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_ingredient_requirements(self, tenant_id: str, product_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get ingredient requirements for production planning"""
|
||||
try:
|
||||
params = {}
|
||||
if product_ids:
|
||||
params["product_ids"] = ",".join(product_ids)
|
||||
|
||||
result = await self.get("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,
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting ingredient requirements",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def calculate_ingredients_for_quantity(self, tenant_id: str, recipe_id: str, quantity: float) -> Optional[Dict[str, Any]]:
|
||||
"""Calculate ingredient quantities needed for a specific production quantity"""
|
||||
try:
|
||||
data = {
|
||||
"recipe_id": recipe_id,
|
||||
"quantity": quantity
|
||||
}
|
||||
result = await self.post("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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error calculating ingredient quantities",
|
||||
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def calculate_batch_ingredients(self, tenant_id: str, production_requests: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""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)
|
||||
if result:
|
||||
logger.info("Calculated batch ingredient requirements from recipes service",
|
||||
batches_count=len(production_requests), tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error calculating batch ingredient requirements",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# PRODUCTION SUPPORT
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved production instructions from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting production instructions",
|
||||
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved recipe yield info from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipe yield info",
|
||||
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def validate_recipe_feasibility(self, tenant_id: str, recipe_id: str, quantity: float) -> Optional[Dict[str, Any]]:
|
||||
"""Validate if a recipe can be produced in the requested quantity"""
|
||||
try:
|
||||
data = {
|
||||
"recipe_id": recipe_id,
|
||||
"quantity": quantity
|
||||
}
|
||||
result = await self.post("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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error validating recipe feasibility",
|
||||
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# ANALYTICS AND OPTIMIZATION
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved recipe cost analysis from recipes service",
|
||||
recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipe cost analysis",
|
||||
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def optimize_production_batch(self, tenant_id: str, requirements: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Optimize production batch to minimize waste and cost"""
|
||||
try:
|
||||
data = {"requirements": requirements}
|
||||
result = await self.post("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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error optimizing production batch",
|
||||
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 recipes dashboard summary data"""
|
||||
try:
|
||||
result = await self.get("dashboard-summary", tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Retrieved recipes dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting recipes dashboard summary",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_popular_recipes(self, tenant_id: str, period: str = "last_30_days") -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get most popular recipes based on production frequency"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
result = await self.get("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)
|
||||
return recipes
|
||||
except Exception as e:
|
||||
logger.error("Error getting popular recipes",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
# ================================================================
|
||||
# UTILITY METHODS
|
||||
# ================================================================
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if recipes service is healthy"""
|
||||
try:
|
||||
result = await self.get("../health") # Health endpoint is not tenant-scoped
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error("Recipes service health check failed", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Factory function for dependency injection
|
||||
def create_recipes_client(config: BaseServiceSettings) -> RecipesServiceClient:
|
||||
"""Create recipes service client instance"""
|
||||
return RecipesServiceClient(config)
|
||||
341
shared/clients/suppliers_client.py
Normal file
341
shared/clients/suppliers_client.py
Normal file
@@ -0,0 +1,341 @@
|
||||
# shared/clients/suppliers_client.py
|
||||
"""
|
||||
Suppliers Service Client for Inter-Service Communication
|
||||
Provides access to supplier data and performance metrics from other services
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import Dict, Any, Optional, List
|
||||
from shared.clients.base_service_client import BaseServiceClient
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class SuppliersServiceClient(BaseServiceClient):
|
||||
"""Client for communicating with the Suppliers Service"""
|
||||
|
||||
def __init__(self, config: BaseServiceSettings):
|
||||
super().__init__("suppliers", config)
|
||||
|
||||
def get_service_base_path(self) -> str:
|
||||
return "/api/v1"
|
||||
|
||||
# ================================================================
|
||||
# SUPPLIER MANAGEMENT
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved supplier details from suppliers service",
|
||||
supplier_id=supplier_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier details",
|
||||
error=str(e), supplier_id=supplier_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_all_suppliers(self, tenant_id: str, is_active: Optional[bool] = True) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get all suppliers for a tenant"""
|
||||
try:
|
||||
params = {}
|
||||
if is_active is not None:
|
||||
params["is_active"] = is_active
|
||||
|
||||
result = await self.get_paginated("suppliers", tenant_id=tenant_id, params=params)
|
||||
logger.info("Retrieved all suppliers from suppliers service",
|
||||
suppliers_count=len(result), tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting all suppliers",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
async def search_suppliers(self, tenant_id: str, search: Optional[str] = None, category: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Search suppliers with filters"""
|
||||
try:
|
||||
params = {}
|
||||
if search:
|
||||
params["search"] = search
|
||||
if category:
|
||||
params["category"] = category
|
||||
|
||||
result = await self.get("suppliers/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)
|
||||
return suppliers
|
||||
except Exception as e:
|
||||
logger.error("Error searching suppliers",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
# ================================================================
|
||||
# SUPPLIER RECOMMENDATIONS
|
||||
# ================================================================
|
||||
|
||||
async def get_supplier_recommendations(self, tenant_id: str, ingredient_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get supplier recommendations for procurement"""
|
||||
try:
|
||||
params = {"ingredient_id": ingredient_id}
|
||||
result = await self.get("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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier recommendations",
|
||||
error=str(e), ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_best_supplier_for_ingredient(self, tenant_id: str, ingredient_id: str, criteria: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get best supplier for a specific ingredient based on criteria"""
|
||||
try:
|
||||
data = {
|
||||
"ingredient_id": ingredient_id,
|
||||
"criteria": criteria or {}
|
||||
}
|
||||
result = await self.post("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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting best supplier for ingredient",
|
||||
error=str(e), ingredient_id=ingredient_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# PURCHASE ORDER MANAGEMENT
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Created purchase order",
|
||||
order_id=result.get('id'),
|
||||
supplier_id=order_data.get('supplier_id'),
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error creating purchase order",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_purchase_orders(self, tenant_id: str, status: Optional[str] = None, supplier_id: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get purchase orders with optional filtering"""
|
||||
try:
|
||||
params = {}
|
||||
if status:
|
||||
params["status"] = status
|
||||
if supplier_id:
|
||||
params["supplier_id"] = supplier_id
|
||||
|
||||
result = await self.get("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)
|
||||
return orders
|
||||
except Exception as e:
|
||||
logger.error("Error getting purchase orders",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
async def update_purchase_order_status(self, tenant_id: str, order_id: str, status: str) -> Optional[Dict[str, Any]]:
|
||||
"""Update purchase order status"""
|
||||
try:
|
||||
data = {"status": status}
|
||||
result = await self.put(f"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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error updating purchase order status",
|
||||
error=str(e), order_id=order_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# DELIVERY MANAGEMENT
|
||||
# ================================================================
|
||||
|
||||
async def get_deliveries(self, tenant_id: str, status: Optional[str] = None, date: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get deliveries with optional filtering"""
|
||||
try:
|
||||
params = {}
|
||||
if status:
|
||||
params["status"] = status
|
||||
if date:
|
||||
params["date"] = date
|
||||
|
||||
result = await self.get("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)
|
||||
return deliveries
|
||||
except Exception as e:
|
||||
logger.error("Error getting deliveries",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
async def update_delivery_status(self, tenant_id: str, delivery_id: str, status: str, notes: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Update delivery status"""
|
||||
try:
|
||||
data = {"status": status}
|
||||
if notes:
|
||||
data["notes"] = notes
|
||||
|
||||
result = await self.put(f"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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error updating delivery status",
|
||||
error=str(e), delivery_id=delivery_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved supplier order summaries from suppliers service",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier order summaries",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# PERFORMANCE TRACKING
|
||||
# ================================================================
|
||||
|
||||
async def get_supplier_performance(self, tenant_id: str, supplier_id: str, period: str = "last_30_days") -> Optional[Dict[str, Any]]:
|
||||
"""Get supplier performance metrics"""
|
||||
try:
|
||||
params = {"period": period}
|
||||
result = await self.get(f"suppliers/{supplier_id}/performance", 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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier performance",
|
||||
error=str(e), supplier_id=supplier_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
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)
|
||||
alerts = result.get('alerts', []) if result else []
|
||||
logger.info("Retrieved supplier performance alerts",
|
||||
alerts_count=len(alerts), tenant_id=tenant_id)
|
||||
return alerts
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier performance alerts",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
async def record_supplier_rating(self, tenant_id: str, supplier_id: str, rating_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Record a rating/review for a supplier"""
|
||||
try:
|
||||
result = await self.post(f"suppliers/{supplier_id}/rating", data=rating_data, tenant_id=tenant_id)
|
||||
if result:
|
||||
logger.info("Recorded supplier rating",
|
||||
supplier_id=supplier_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error recording supplier rating",
|
||||
error=str(e), supplier_id=supplier_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# DASHBOARD AND ANALYTICS
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved suppliers dashboard summary",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting suppliers dashboard summary",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def get_cost_analysis(self, tenant_id: str, start_date: str, end_date: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cost analysis across suppliers"""
|
||||
try:
|
||||
params = {
|
||||
"start_date": start_date,
|
||||
"end_date": end_date
|
||||
}
|
||||
result = await self.get("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)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier cost analysis",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Retrieved supplier reliability metrics",
|
||||
tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error getting supplier reliability metrics",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# ALERTS AND NOTIFICATIONS
|
||||
# ================================================================
|
||||
|
||||
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)
|
||||
if result:
|
||||
logger.info("Acknowledged supplier alert",
|
||||
alert_id=alert_id, tenant_id=tenant_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error acknowledging supplier alert",
|
||||
error=str(e), alert_id=alert_id, tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# ================================================================
|
||||
# UTILITY METHODS
|
||||
# ================================================================
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""Check if suppliers service is healthy"""
|
||||
try:
|
||||
result = await self.get("../health") # Health endpoint is not tenant-scoped
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error("Suppliers service health check failed", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Factory function for dependency injection
|
||||
def create_suppliers_client(config: BaseServiceSettings) -> SuppliersServiceClient:
|
||||
"""Create suppliers service client instance"""
|
||||
return SuppliersServiceClient(config)
|
||||
22
shared/notifications/__init__.py
Normal file
22
shared/notifications/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ================================================================
|
||||
# shared/notifications/__init__.py
|
||||
# ================================================================
|
||||
"""
|
||||
Shared Notifications Module - Alert integration using existing notification service
|
||||
"""
|
||||
|
||||
from .alert_integration import (
|
||||
AlertIntegration,
|
||||
AlertSeverity,
|
||||
AlertType,
|
||||
AlertCategory,
|
||||
AlertSource
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'AlertIntegration',
|
||||
'AlertSeverity',
|
||||
'AlertType',
|
||||
'AlertCategory',
|
||||
'AlertSource'
|
||||
]
|
||||
285
shared/notifications/alert_integration.py
Normal file
285
shared/notifications/alert_integration.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# ================================================================
|
||||
# shared/notifications/alert_integration.py
|
||||
# ================================================================
|
||||
"""
|
||||
Simplified Alert Integration - Placeholder for unified alert system
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from uuid import UUID
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class AlertSeverity(enum.Enum):
|
||||
"""Alert severity levels"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class AlertType(enum.Enum):
|
||||
"""Alert types for different bakery operations"""
|
||||
# Production Alerts
|
||||
PRODUCTION_DELAY = "production_delay"
|
||||
BATCH_FAILURE = "batch_failure"
|
||||
EQUIPMENT_MALFUNCTION = "equipment_malfunction"
|
||||
TEMPERATURE_VIOLATION = "temperature_violation"
|
||||
QUALITY_ISSUE = "quality_issue"
|
||||
|
||||
# Inventory Alerts
|
||||
LOW_STOCK = "low_stock"
|
||||
OUT_OF_STOCK = "out_of_stock"
|
||||
EXPIRATION_WARNING = "expiration_warning"
|
||||
TEMPERATURE_BREACH = "temperature_breach"
|
||||
FOOD_SAFETY_VIOLATION = "food_safety_violation"
|
||||
|
||||
# Supplier Alerts
|
||||
SUPPLIER_PERFORMANCE = "supplier_performance"
|
||||
DELIVERY_DELAY = "delivery_delay"
|
||||
QUALITY_ISSUES = "quality_issues"
|
||||
CONTRACT_EXPIRY = "contract_expiry"
|
||||
|
||||
# Order Alerts
|
||||
ORDER_DELAY = "order_delay"
|
||||
CUSTOMER_COMPLAINT = "customer_complaint"
|
||||
PAYMENT_ISSUE = "payment_issue"
|
||||
|
||||
|
||||
class AlertSource(enum.Enum):
|
||||
"""Sources that can generate alerts"""
|
||||
PRODUCTION_SERVICE = "production_service"
|
||||
INVENTORY_SERVICE = "inventory_service"
|
||||
SUPPLIERS_SERVICE = "suppliers_service"
|
||||
ORDERS_SERVICE = "orders_service"
|
||||
EXTERNAL_SERVICE = "external_service"
|
||||
|
||||
|
||||
class AlertCategory(enum.Enum):
|
||||
"""Alert categories for organization"""
|
||||
OPERATIONAL = "operational"
|
||||
QUALITY = "quality"
|
||||
SAFETY = "safety"
|
||||
FINANCIAL = "financial"
|
||||
COMPLIANCE = "compliance"
|
||||
|
||||
|
||||
class AlertIntegration:
|
||||
"""
|
||||
Simplified alert integration that logs alerts.
|
||||
TODO: Implement proper service-to-service communication for notifications
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = structlog.get_logger("alert_integration")
|
||||
|
||||
async def create_alert(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
alert_type: AlertType,
|
||||
severity: AlertSeverity,
|
||||
title: str,
|
||||
message: str,
|
||||
source: AlertSource,
|
||||
category: AlertCategory = None,
|
||||
entity_id: Optional[UUID] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
recipients: Optional[List[UUID]] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Create a new alert (currently just logs it)
|
||||
|
||||
Returns:
|
||||
Alert ID if successful, None otherwise
|
||||
"""
|
||||
try:
|
||||
alert_data = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"alert_type": alert_type.value,
|
||||
"severity": severity.value,
|
||||
"title": title,
|
||||
"message": message,
|
||||
"source": source.value,
|
||||
"category": category.value if category else None,
|
||||
"entity_id": str(entity_id) if entity_id else None,
|
||||
"metadata": metadata or {},
|
||||
"recipients": [str(r) for r in recipients] if recipients else [],
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
# For now, just log the alert
|
||||
self.logger.info(
|
||||
"Alert created",
|
||||
**alert_data
|
||||
)
|
||||
|
||||
# Return a mock alert ID
|
||||
return f"alert_{datetime.utcnow().timestamp()}"
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
"Failed to create alert",
|
||||
tenant_id=str(tenant_id),
|
||||
alert_type=alert_type.value,
|
||||
error=str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
async def acknowledge_alert(self, alert_id: str, user_id: UUID) -> bool:
|
||||
"""Acknowledge an alert (currently just logs it)"""
|
||||
try:
|
||||
self.logger.info(
|
||||
"Alert acknowledged",
|
||||
alert_id=alert_id,
|
||||
user_id=str(user_id),
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
"Failed to acknowledge alert",
|
||||
alert_id=alert_id,
|
||||
error=str(e)
|
||||
)
|
||||
return False
|
||||
|
||||
async def resolve_alert(self, alert_id: str, user_id: UUID, resolution: str = None) -> bool:
|
||||
"""Resolve an alert (currently just logs it)"""
|
||||
try:
|
||||
self.logger.info(
|
||||
"Alert resolved",
|
||||
alert_id=alert_id,
|
||||
user_id=str(user_id),
|
||||
resolution=resolution,
|
||||
timestamp=datetime.utcnow().isoformat()
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
"Failed to resolve alert",
|
||||
alert_id=alert_id,
|
||||
error=str(e)
|
||||
)
|
||||
return False
|
||||
|
||||
# Convenience methods for specific alert types
|
||||
async def create_inventory_alert(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
alert_type: AlertType,
|
||||
severity: AlertSeverity,
|
||||
title: str,
|
||||
message: str,
|
||||
item_id: UUID = None,
|
||||
**kwargs
|
||||
) -> Optional[str]:
|
||||
"""Create an inventory-specific alert"""
|
||||
metadata = kwargs.pop('metadata', {})
|
||||
if item_id:
|
||||
metadata['item_id'] = str(item_id)
|
||||
|
||||
return await self.create_alert(
|
||||
tenant_id=tenant_id,
|
||||
alert_type=alert_type,
|
||||
severity=severity,
|
||||
title=title,
|
||||
message=message,
|
||||
source=AlertSource.INVENTORY_SERVICE,
|
||||
category=AlertCategory.OPERATIONAL,
|
||||
entity_id=item_id,
|
||||
metadata=metadata,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
async def create_production_alert(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
alert_type: AlertType,
|
||||
severity: AlertSeverity,
|
||||
title: str,
|
||||
message: str,
|
||||
batch_id: UUID = None,
|
||||
equipment_id: UUID = None,
|
||||
**kwargs
|
||||
) -> Optional[str]:
|
||||
"""Create a production-specific alert"""
|
||||
metadata = kwargs.pop('metadata', {})
|
||||
if batch_id:
|
||||
metadata['batch_id'] = str(batch_id)
|
||||
if equipment_id:
|
||||
metadata['equipment_id'] = str(equipment_id)
|
||||
|
||||
return await self.create_alert(
|
||||
tenant_id=tenant_id,
|
||||
alert_type=alert_type,
|
||||
severity=severity,
|
||||
title=title,
|
||||
message=message,
|
||||
source=AlertSource.PRODUCTION_SERVICE,
|
||||
category=AlertCategory.OPERATIONAL,
|
||||
metadata=metadata,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
async def create_supplier_alert(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
alert_type: AlertType,
|
||||
severity: AlertSeverity,
|
||||
title: str,
|
||||
message: str,
|
||||
supplier_id: UUID = None,
|
||||
**kwargs
|
||||
) -> Optional[str]:
|
||||
"""Create a supplier-specific alert"""
|
||||
metadata = kwargs.pop('metadata', {})
|
||||
if supplier_id:
|
||||
metadata['supplier_id'] = str(supplier_id)
|
||||
|
||||
return await self.create_alert(
|
||||
tenant_id=tenant_id,
|
||||
alert_type=alert_type,
|
||||
severity=severity,
|
||||
title=title,
|
||||
message=message,
|
||||
source=AlertSource.SUPPLIERS_SERVICE,
|
||||
category=AlertCategory.QUALITY,
|
||||
entity_id=supplier_id,
|
||||
metadata=metadata,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
async def create_order_alert(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
alert_type: AlertType,
|
||||
severity: AlertSeverity,
|
||||
title: str,
|
||||
message: str,
|
||||
order_id: UUID = None,
|
||||
customer_id: UUID = None,
|
||||
**kwargs
|
||||
) -> Optional[str]:
|
||||
"""Create an order-specific alert"""
|
||||
metadata = kwargs.pop('metadata', {})
|
||||
if order_id:
|
||||
metadata['order_id'] = str(order_id)
|
||||
if customer_id:
|
||||
metadata['customer_id'] = str(customer_id)
|
||||
|
||||
return await self.create_alert(
|
||||
tenant_id=tenant_id,
|
||||
alert_type=alert_type,
|
||||
severity=severity,
|
||||
title=title,
|
||||
message=message,
|
||||
source=AlertSource.ORDERS_SERVICE,
|
||||
category=AlertCategory.OPERATIONAL,
|
||||
entity_id=order_id,
|
||||
metadata=metadata,
|
||||
**kwargs
|
||||
)
|
||||
Reference in New Issue
Block a user