New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -18,6 +18,7 @@ from .suppliers_client import SuppliersServiceClient
from .tenant_client import TenantServiceClient
from .ai_insights_client import AIInsightsClient
from .alerts_client import AlertsServiceClient
from .alert_processor_client import AlertProcessorClient, get_alert_processor_client
from .procurement_client import ProcurementServiceClient
from .distribution_client import DistributionServiceClient
@@ -158,6 +159,10 @@ def get_distribution_client(config: BaseServiceSettings = None, service_name: st
return _client_cache[cache_key]
# Note: get_alert_processor_client is already defined in alert_processor_client.py
# and imported above, so we don't need to redefine it here
class ServiceClients:
"""Convenient wrapper for all service clients"""
@@ -267,6 +272,7 @@ __all__ = [
'RecipesServiceClient',
'SuppliersServiceClient',
'AlertsServiceClient',
'AlertProcessorClient',
'TenantServiceClient',
'DistributionServiceClient',
'ServiceClients',
@@ -280,6 +286,7 @@ __all__ = [
'get_recipes_client',
'get_suppliers_client',
'get_alerts_client',
'get_alert_processor_client',
'get_tenant_client',
'get_procurement_client',
'get_distribution_client',

View File

@@ -0,0 +1,220 @@
# shared/clients/alert_processor_client.py
"""
Alert Processor Service Client - Inter-service communication
Handles communication with the alert processor service for alert lifecycle management
"""
import structlog
from typing import Dict, Any, List, Optional
from uuid import UUID
from shared.clients.base_service_client import BaseServiceClient
from shared.config.base import BaseServiceSettings
logger = structlog.get_logger()
class AlertProcessorClient(BaseServiceClient):
"""Client for communicating with the alert processor service via gateway"""
def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"):
super().__init__(calling_service_name, config)
def get_service_base_path(self) -> str:
"""Return the base path for alert processor service APIs"""
return "/api/v1"
# ================================================================
# ALERT LIFECYCLE MANAGEMENT
# ================================================================
async def acknowledge_alerts_by_metadata(
self,
tenant_id: UUID,
alert_type: str,
metadata_filter: Dict[str, Any],
acknowledged_by: Optional[str] = None
) -> Dict[str, Any]:
"""
Acknowledge all active alerts matching alert type and metadata.
Used when user actions trigger alert acknowledgment (e.g., approving a PO).
Args:
tenant_id: Tenant UUID
alert_type: Alert type to filter (e.g., 'po_approval_needed')
metadata_filter: Metadata fields to match (e.g., {'po_id': 'uuid'})
acknowledged_by: Optional user ID who acknowledged
Returns:
{
"success": true,
"acknowledged_count": 2,
"alert_ids": ["uuid1", "uuid2"]
}
"""
try:
payload = {
"alert_type": alert_type,
"metadata_filter": metadata_filter
}
if acknowledged_by:
payload["acknowledged_by"] = acknowledged_by
result = await self.post(
f"tenants/{tenant_id}/alerts/acknowledge-by-metadata",
tenant_id=str(tenant_id),
json=payload
)
if result and result.get("success"):
logger.info(
"Acknowledged alerts by metadata",
tenant_id=str(tenant_id),
alert_type=alert_type,
count=result.get("acknowledged_count", 0),
calling_service=self.calling_service_name
)
return result or {"success": False, "acknowledged_count": 0, "alert_ids": []}
except Exception as e:
logger.error(
"Error acknowledging alerts by metadata",
error=str(e),
tenant_id=str(tenant_id),
alert_type=alert_type,
metadata_filter=metadata_filter,
calling_service=self.calling_service_name
)
return {"success": False, "acknowledged_count": 0, "alert_ids": [], "error": str(e)}
async def resolve_alerts_by_metadata(
self,
tenant_id: UUID,
alert_type: str,
metadata_filter: Dict[str, Any],
resolved_by: Optional[str] = None
) -> Dict[str, Any]:
"""
Resolve all active alerts matching alert type and metadata.
Used when user actions complete an alert's underlying issue (e.g., marking delivery received).
Args:
tenant_id: Tenant UUID
alert_type: Alert type to filter (e.g., 'delivery_overdue')
metadata_filter: Metadata fields to match (e.g., {'po_id': 'uuid'})
resolved_by: Optional user ID who resolved
Returns:
{
"success": true,
"resolved_count": 1,
"alert_ids": ["uuid1"]
}
"""
try:
payload = {
"alert_type": alert_type,
"metadata_filter": metadata_filter
}
if resolved_by:
payload["resolved_by"] = resolved_by
result = await self.post(
f"tenants/{tenant_id}/alerts/resolve-by-metadata",
tenant_id=str(tenant_id),
json=payload
)
if result and result.get("success"):
logger.info(
"Resolved alerts by metadata",
tenant_id=str(tenant_id),
alert_type=alert_type,
count=result.get("resolved_count", 0),
calling_service=self.calling_service_name
)
return result or {"success": False, "resolved_count": 0, "alert_ids": []}
except Exception as e:
logger.error(
"Error resolving alerts by metadata",
error=str(e),
tenant_id=str(tenant_id),
alert_type=alert_type,
metadata_filter=metadata_filter,
calling_service=self.calling_service_name
)
return {"success": False, "resolved_count": 0, "alert_ids": [], "error": str(e)}
async def get_active_alerts(
self,
tenant_id: UUID,
priority_level: Optional[str] = None,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
Get active alerts for a tenant.
Args:
tenant_id: Tenant UUID
priority_level: Optional priority filter (critical, important, standard, info)
limit: Maximum number of alerts to return
Returns:
List of alert dictionaries
"""
try:
params = {
"status": "active",
"limit": limit
}
if priority_level:
params["priority_level"] = priority_level
result = await self.get(
f"tenants/{tenant_id}/alerts",
tenant_id=str(tenant_id),
params=params
)
alerts = result.get("alerts", []) if isinstance(result, dict) else []
logger.info(
"Retrieved active alerts",
tenant_id=str(tenant_id),
count=len(alerts),
calling_service=self.calling_service_name
)
return alerts
except Exception as e:
logger.error(
"Error fetching active alerts",
error=str(e),
tenant_id=str(tenant_id),
calling_service=self.calling_service_name
)
return []
# Factory function for easy import
def get_alert_processor_client(config: BaseServiceSettings, calling_service_name: str) -> AlertProcessorClient:
"""
Factory function to create an AlertProcessorClient instance.
Args:
config: Service configuration with gateway URL
calling_service_name: Name of the service making the call (for logging)
Returns:
AlertProcessorClient instance
"""
return AlertProcessorClient(config, calling_service_name)

View File

@@ -159,17 +159,38 @@ class DistributionServiceClient(BaseServiceClient):
if status:
params["status"] = status
response = await self.get(
# Use _make_request directly to construct correct URL
# Gateway route: /api/v1/tenants/{tenant_id}/distribution/{path}
response = await self._make_request(
"GET",
f"tenants/{tenant_id}/distribution/routes",
params=params,
tenant_id=tenant_id
params=params
)
if response:
logger.info("Retrieved delivery routes",
tenant_id=tenant_id,
count=len(response.get("routes", [])))
return response.get("routes", []) if response else []
# Handle different response formats
if isinstance(response, list):
# Direct list of routes
logger.info("Retrieved delivery routes",
tenant_id=tenant_id,
count=len(response))
return response
elif isinstance(response, dict):
# Response wrapped in routes key
if "routes" in response:
logger.info("Retrieved delivery routes",
tenant_id=tenant_id,
count=len(response.get("routes", [])))
return response.get("routes", [])
else:
# Return the whole dict if it's a single route
logger.info("Retrieved delivery routes",
tenant_id=tenant_id,
count=1)
return [response]
logger.info("No delivery routes found",
tenant_id=tenant_id)
return []
except Exception as e:
logger.error("Error getting delivery routes",
tenant_id=tenant_id,
@@ -193,14 +214,17 @@ class DistributionServiceClient(BaseServiceClient):
"""
try:
response = await self.get(
f"tenants/{tenant_id}/distribution/routes/{route_id}",
f"distribution/routes/{route_id}",
tenant_id=tenant_id
)
if response:
logger.info("Retrieved delivery route detail",
tenant_id=tenant_id,
route_id=route_id)
# Ensure we return the route data directly if it's wrapped in a route key
if isinstance(response, dict) and "route" in response:
return response["route"]
return response
except Exception as e:
logger.error("Error getting delivery route detail",
@@ -241,17 +265,38 @@ class DistributionServiceClient(BaseServiceClient):
if status:
params["status"] = status
response = await self.get(
# Use _make_request directly to construct correct URL
# Gateway route: /api/v1/tenants/{tenant_id}/distribution/{path}
response = await self._make_request(
"GET",
f"tenants/{tenant_id}/distribution/shipments",
params=params,
tenant_id=tenant_id
params=params
)
if response:
logger.info("Retrieved shipments",
tenant_id=tenant_id,
count=len(response.get("shipments", [])))
return response.get("shipments", []) if response else []
# Handle different response formats
if isinstance(response, list):
# Direct list of shipments
logger.info("Retrieved shipments",
tenant_id=tenant_id,
count=len(response))
return response
elif isinstance(response, dict):
# Response wrapped in shipments key
if "shipments" in response:
logger.info("Retrieved shipments",
tenant_id=tenant_id,
count=len(response.get("shipments", [])))
return response.get("shipments", [])
else:
# Return the whole dict if it's a single shipment
logger.info("Retrieved shipments",
tenant_id=tenant_id,
count=1)
return [response]
logger.info("No shipments found",
tenant_id=tenant_id)
return []
except Exception as e:
logger.error("Error getting shipments",
tenant_id=tenant_id,
@@ -275,14 +320,17 @@ class DistributionServiceClient(BaseServiceClient):
"""
try:
response = await self.get(
f"tenants/{tenant_id}/distribution/shipments/{shipment_id}",
f"distribution/shipments/{shipment_id}",
tenant_id=tenant_id
)
if response:
logger.info("Retrieved shipment detail",
tenant_id=tenant_id,
shipment_id=shipment_id)
# Ensure we return the shipment data directly if it's wrapped in a shipment key
if isinstance(response, dict) and "shipment" in response:
return response["shipment"]
return response
except Exception as e:
logger.error("Error getting shipment detail",
@@ -320,7 +368,7 @@ class DistributionServiceClient(BaseServiceClient):
}
response = await self.put(
f"tenants/{tenant_id}/distribution/shipments/{shipment_id}/status",
f"distribution/shipments/{shipment_id}/status",
data=payload,
tenant_id=tenant_id
)
@@ -343,57 +391,8 @@ class DistributionServiceClient(BaseServiceClient):
# INTERNAL DEMO ENDPOINTS
# ================================================================
async def setup_enterprise_distribution_demo(
self,
parent_tenant_id: str,
child_tenant_ids: List[str],
session_id: str
) -> Optional[Dict[str, Any]]:
"""
Internal endpoint to setup distribution for enterprise demo
Args:
parent_tenant_id: Parent tenant ID
child_tenant_ids: List of child tenant IDs
session_id: Demo session ID
Returns:
Distribution setup result
"""
try:
url = f"{self.service_base_url}/api/v1/internal/demo/setup"
async with self.get_http_client() as client:
response = await client.post(
url,
json={
"parent_tenant_id": parent_tenant_id,
"child_tenant_ids": child_tenant_ids,
"session_id": session_id
},
headers={
"X-Internal-API-Key": self.config.INTERNAL_API_KEY,
"Content-Type": "application/json"
}
)
if response.status_code == 200:
result = response.json()
logger.info("Setup enterprise distribution demo",
parent_tenant_id=parent_tenant_id,
child_count=len(child_tenant_ids))
return result
else:
logger.error("Failed to setup enterprise distribution demo",
status_code=response.status_code,
response_text=response.text)
return None
except Exception as e:
logger.error("Error setting up enterprise distribution demo",
parent_tenant_id=parent_tenant_id,
error=str(e))
return None
# Legacy setup_enterprise_distribution_demo method removed
# Distribution now uses standard /internal/demo/clone endpoint via DataCloner
async def get_shipments_for_date(
self,
@@ -411,21 +410,45 @@ class DistributionServiceClient(BaseServiceClient):
List of shipments for the date
"""
try:
response = await self.get(
# Use _make_request directly to construct correct URL
# Gateway route: /api/v1/tenants/{tenant_id}/distribution/{path}
response = await self._make_request(
"GET",
f"tenants/{tenant_id}/distribution/shipments",
params={
"date_from": target_date.isoformat(),
"date_to": target_date.isoformat()
},
tenant_id=tenant_id
}
)
if response:
logger.info("Retrieved shipments for date",
tenant_id=tenant_id,
target_date=target_date.isoformat(),
shipment_count=len(response.get("shipments", [])))
return response.get("shipments", []) if response else []
# Handle different response formats
if isinstance(response, list):
# Direct list of shipments
logger.info("Retrieved shipments for date",
tenant_id=tenant_id,
target_date=target_date.isoformat(),
shipment_count=len(response))
return response
elif isinstance(response, dict):
# Response wrapped in shipments key
if "shipments" in response:
logger.info("Retrieved shipments for date",
tenant_id=tenant_id,
target_date=target_date.isoformat(),
shipment_count=len(response.get("shipments", [])))
return response.get("shipments", [])
else:
# Return the whole dict if it's a single shipment
logger.info("Retrieved shipments for date",
tenant_id=tenant_id,
target_date=target_date.isoformat(),
shipment_count=1)
return [response]
logger.info("No shipments found for date",
tenant_id=tenant_id,
target_date=target_date.isoformat())
return []
except Exception as e:
logger.error("Error getting shipments for date",
tenant_id=tenant_id,
@@ -451,4 +474,4 @@ class DistributionServiceClient(BaseServiceClient):
# Factory function for dependency injection
def create_distribution_client(config: BaseServiceSettings, service_name: str = "unknown") -> DistributionServiceClient:
"""Create distribution service client instance"""
return DistributionServiceClient(config, service_name)
return DistributionServiceClient(config, service_name)

View File

@@ -420,9 +420,12 @@ class ForecastServiceClient(BaseServiceClient):
if product_id:
params["product_id"] = product_id
return await self.get(
"forecasting/enterprise/aggregated",
tenant_id=parent_tenant_id,
# Use _make_request directly because the base_service_client adds /tenants/{tenant_id}/ prefix
# Gateway route is: /api/v1/tenants/{tenant_id}/forecasting/enterprise/{path}
# So we need the full path without tenant_id parameter to avoid double prefixing
return await self._make_request(
"GET",
f"tenants/{parent_tenant_id}/forecasting/enterprise/aggregated",
params=params
)

View File

@@ -655,6 +655,53 @@ class InventoryServiceClient(BaseServiceClient):
# DASHBOARD METHODS
# ================================================================
async def get_inventory_summary_batch(
self,
tenant_ids: List[str]
) -> Dict[str, Any]:
"""
Get inventory summaries for multiple tenants in a single request.
Phase 2 optimization: Eliminates N+1 query patterns for enterprise dashboards.
Args:
tenant_ids: List of tenant IDs to fetch
Returns:
Dict mapping tenant_id -> inventory summary
"""
try:
if not tenant_ids:
return {}
if len(tenant_ids) > 100:
logger.warning("Batch request exceeds max tenant limit", requested=len(tenant_ids))
tenant_ids = tenant_ids[:100]
result = await self.post(
"inventory/batch/inventory-summary",
data={"tenant_ids": tenant_ids},
tenant_id=tenant_ids[0] # Use first tenant for auth context
)
summaries = result if isinstance(result, dict) else {}
logger.info(
"Batch retrieved inventory summaries",
requested=len(tenant_ids),
found=len(summaries)
)
return summaries
except Exception as e:
logger.error(
"Error batch fetching inventory summaries",
error=str(e),
tenant_count=len(tenant_ids)
)
return {}
async def get_stock_status(
self,
tenant_id: str
@@ -692,7 +739,7 @@ class InventoryServiceClient(BaseServiceClient):
"""
try:
return await self.get(
"/inventory/sustainability/widget",
"/sustainability/widget",
tenant_id=tenant_id
)
except Exception as e:

View File

@@ -138,7 +138,8 @@ class ProcurementServiceClient(BaseServiceClient):
async def get_pending_purchase_orders(
self,
tenant_id: str,
limit: int = 50
limit: int = 50,
enrich_supplier: bool = True
) -> Optional[List[Dict[str, Any]]]:
"""
Get pending purchase orders
@@ -146,6 +147,8 @@ class ProcurementServiceClient(BaseServiceClient):
Args:
tenant_id: Tenant ID
limit: Maximum number of results
enrich_supplier: Whether to include supplier details (default: True)
Set to False for faster queries when supplier data will be fetched separately
Returns:
List of pending purchase orders
@@ -153,14 +156,19 @@ class ProcurementServiceClient(BaseServiceClient):
try:
response = await self.get(
"procurement/purchase-orders",
params={"status": "pending_approval", "limit": limit},
params={
"status": "pending_approval",
"limit": limit,
"enrich_supplier": enrich_supplier
},
tenant_id=tenant_id
)
if response:
logger.info("Retrieved pending purchase orders",
tenant_id=tenant_id,
count=len(response))
count=len(response),
enriched=enrich_supplier)
return response if response else []
except Exception as e:
logger.error("Error getting pending purchase orders",
@@ -168,6 +176,60 @@ class ProcurementServiceClient(BaseServiceClient):
error=str(e))
return []
async def get_purchase_orders_by_supplier(
self,
tenant_id: str,
supplier_id: str,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
status: Optional[str] = None,
limit: int = 100
) -> Optional[List[Dict[str, Any]]]:
"""
Get purchase orders for a specific supplier
Args:
tenant_id: Tenant ID
supplier_id: Supplier ID to filter by
date_from: Start date for filtering
date_to: End date for filtering
status: Status filter (e.g., 'approved', 'delivered')
limit: Maximum number of results
Returns:
List of purchase orders with items
"""
try:
params = {
"supplier_id": supplier_id,
"limit": limit
}
if date_from:
params["date_from"] = date_from.isoformat()
if date_to:
params["date_to"] = date_to.isoformat()
if status:
params["status"] = status
response = await self.get(
"procurement/purchase-orders",
params=params,
tenant_id=tenant_id
)
if response:
logger.info("Retrieved purchase orders by supplier",
tenant_id=tenant_id,
supplier_id=supplier_id,
count=len(response))
return response if response else []
except Exception as e:
logger.error("Error getting purchase orders by supplier",
tenant_id=tenant_id,
supplier_id=supplier_id,
error=str(e))
return []
# ================================================================
# INTERNAL TRANSFER ENDPOINTS (NEW FOR ENTERPRISE TIER)
# ================================================================

View File

@@ -449,6 +449,53 @@ class ProductionServiceClient(BaseServiceClient):
# DASHBOARD METHODS
# ================================================================
async def get_production_summary_batch(
self,
tenant_ids: List[str]
) -> Dict[str, Any]:
"""
Get production summaries for multiple tenants in a single request.
Phase 2 optimization: Eliminates N+1 query patterns for enterprise dashboards.
Args:
tenant_ids: List of tenant IDs to fetch
Returns:
Dict mapping tenant_id -> production summary
"""
try:
if not tenant_ids:
return {}
if len(tenant_ids) > 100:
logger.warning("Batch request exceeds max tenant limit", requested=len(tenant_ids))
tenant_ids = tenant_ids[:100]
result = await self.post(
"production/batch/production-summary",
data={"tenant_ids": tenant_ids},
tenant_id=tenant_ids[0] # Use first tenant for auth context
)
summaries = result if isinstance(result, dict) else {}
logger.info(
"Batch retrieved production summaries",
requested=len(tenant_ids),
found=len(summaries)
)
return summaries
except Exception as e:
logger.error(
"Error batch fetching production summaries",
error=str(e),
tenant_count=len(tenant_ids)
)
return {}
async def get_todays_batches(
self,
tenant_id: str

View File

@@ -215,6 +215,65 @@ class SalesServiceClient(BaseServiceClient):
params=params
)
async def get_sales_summary_batch(
self,
tenant_ids: List[str],
start_date: date,
end_date: date
) -> Dict[str, Any]:
"""
Get sales summaries for multiple tenants in a single request.
Phase 2 optimization: Eliminates N+1 query patterns for enterprise dashboards.
Args:
tenant_ids: List of tenant IDs to fetch
start_date: Start date for summary range
end_date: End date for summary range
Returns:
Dict mapping tenant_id -> sales summary
"""
try:
if not tenant_ids:
return {}
if len(tenant_ids) > 100:
logger.warning("Batch request exceeds max tenant limit", requested=len(tenant_ids))
tenant_ids = tenant_ids[:100]
data = {
"tenant_ids": tenant_ids,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat()
}
result = await self.post(
"sales/batch/sales-summary",
data=data,
tenant_id=tenant_ids[0] # Use first tenant for auth context
)
summaries = result if isinstance(result, dict) else {}
logger.info(
"Batch retrieved sales summaries",
requested=len(tenant_ids),
found=len(summaries),
start_date=start_date.isoformat(),
end_date=end_date.isoformat()
)
return summaries
except Exception as e:
logger.error(
"Error batch fetching sales summaries",
error=str(e),
tenant_count=len(tenant_ids)
)
return {}
# ================================================================
# DATA IMPORT
# ================================================================

View File

@@ -62,17 +62,54 @@ class SuppliersServiceClient(BaseServiceClient):
params["search_term"] = search
if category:
params["supplier_type"] = category
result = await self.get("suppliers", tenant_id=tenant_id, params=params)
suppliers = result if result else []
logger.info("Searched suppliers from suppliers service",
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",
logger.error("Error searching suppliers",
error=str(e), tenant_id=tenant_id)
return []
async def get_suppliers_batch(self, tenant_id: str, supplier_ids: List[str]) -> Optional[List[Dict[str, Any]]]:
"""
Get multiple suppliers in a single request for performance optimization.
This method eliminates N+1 query patterns when fetching supplier data
for multiple purchase orders or other entities.
Args:
tenant_id: Tenant ID
supplier_ids: List of supplier IDs to fetch
Returns:
List of supplier dictionaries or empty list if error
"""
try:
if not supplier_ids:
return []
# Join IDs as comma-separated string
ids_param = ",".join(supplier_ids)
params = {"ids": ids_param}
result = await self.get("suppliers/batch", tenant_id=tenant_id, params=params)
suppliers = result if result else []
logger.info("Batch retrieved suppliers from suppliers service",
requested_count=len(supplier_ids),
found_count=len(suppliers),
tenant_id=tenant_id)
return suppliers
except Exception as e:
logger.error("Error batch retrieving suppliers",
error=str(e),
requested_count=len(supplier_ids),
tenant_id=tenant_id)
return []
# ================================================================
# SUPPLIER RECOMMENDATIONS
# ================================================================
@@ -107,186 +144,7 @@ class SuppliersServiceClient(BaseServiceClient):
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("suppliers/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("suppliers/purchase-orders", tenant_id=tenant_id, params=params)
orders = result.get('orders', []) if result else []
logger.info("Retrieved purchase orders from suppliers service",
orders_count=len(orders), tenant_id=tenant_id)
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"suppliers/purchase-orders/{order_id}/status", data=data, tenant_id=tenant_id)
if result:
logger.info("Updated purchase order status",
order_id=order_id, status=status, tenant_id=tenant_id)
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
async def approve_purchase_order(
self,
tenant_id: str,
po_id: str,
approval_data: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""
Auto-approve a purchase order
Args:
tenant_id: Tenant ID
po_id: Purchase Order ID
approval_data: Approval data including:
- approved_by: User ID or "system" for auto-approval
- approval_notes: Notes about the approval
- auto_approved: Boolean flag indicating auto-approval
- approval_reasons: List of reasons for auto-approval
Returns:
Updated purchase order data or None
"""
try:
# Format the approval request payload
payload = {
"action": "approve",
"notes": approval_data.get("approval_notes", "Auto-approved by system")
}
result = await self.post(
f"suppliers/purchase-orders/{po_id}/approve",
data=payload,
tenant_id=tenant_id
)
if result:
logger.info("Auto-approved purchase order",
po_id=po_id,
tenant_id=tenant_id,
auto_approved=approval_data.get("auto_approved", True))
return result
except Exception as e:
logger.error("Error auto-approving purchase order",
error=str(e),
po_id=po_id,
tenant_id=tenant_id)
return None
async def get_supplier(self, tenant_id: str, supplier_id: str) -> Optional[Dict[str, Any]]:
"""
Get supplier details with performance metrics
Args:
tenant_id: Tenant ID
supplier_id: Supplier ID
Returns:
Supplier data including performance metrics or None
"""
try:
# Use the existing get_supplier_by_id method which returns full supplier data
result = await self.get_supplier_by_id(tenant_id, supplier_id)
if result:
logger.info("Retrieved supplier data for auto-approval",
supplier_id=supplier_id,
tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier data",
error=str(e),
supplier_id=supplier_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("suppliers/deliveries", tenant_id=tenant_id, params=params)
deliveries = result.get('deliveries', []) if result else []
logger.info("Retrieved deliveries from suppliers service",
deliveries_count=len(deliveries), tenant_id=tenant_id)
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"suppliers/deliveries/{delivery_id}/status", data=data, tenant_id=tenant_id)
if result:
logger.info("Updated delivery status",
delivery_id=delivery_id, status=status, tenant_id=tenant_id)
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("suppliers/dashboard/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
# ================================================================

View File

@@ -310,7 +310,9 @@ class TenantServiceClient(BaseServiceClient):
List of child tenant dictionaries
"""
try:
result = await self.get("children", tenant_id=parent_tenant_id)
# Use _make_request directly to avoid double tenant_id in URL
# The gateway expects: /api/v1/tenants/{tenant_id}/children
result = await self._make_request("GET", f"tenants/{parent_tenant_id}/children")
if result:
logger.info("Retrieved child tenants",
parent_tenant_id=parent_tenant_id,