Add more services

This commit is contained in:
Urtzi Alfaro
2025-08-21 20:28:14 +02:00
parent d6fd53e461
commit c6dd6fd1de
85 changed files with 17842 additions and 1828 deletions

View File

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

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

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

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

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

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

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