Add more services
This commit is contained in:
@@ -1 +1,19 @@
|
||||
# services/suppliers/app/services/__init__.py
|
||||
# services/suppliers/app/services/__init__.py
|
||||
"""
|
||||
Services package for the Supplier service
|
||||
"""
|
||||
|
||||
from .supplier_service import SupplierService
|
||||
from .purchase_order_service import PurchaseOrderService
|
||||
from .delivery_service import DeliveryService
|
||||
from .performance_service import PerformanceTrackingService, AlertService
|
||||
from .dashboard_service import DashboardService
|
||||
|
||||
__all__ = [
|
||||
'SupplierService',
|
||||
'PurchaseOrderService',
|
||||
'DeliveryService',
|
||||
'PerformanceTrackingService',
|
||||
'AlertService',
|
||||
'DashboardService'
|
||||
]
|
||||
624
services/suppliers/app/services/dashboard_service.py
Normal file
624
services/suppliers/app/services/dashboard_service.py
Normal file
@@ -0,0 +1,624 @@
|
||||
# ================================================================
|
||||
# services/suppliers/app/services/dashboard_service.py
|
||||
# ================================================================
|
||||
"""
|
||||
Supplier Dashboard and Analytics Service
|
||||
Comprehensive supplier performance dashboards and business intelligence
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_, or_, desc, asc, text
|
||||
from decimal import Decimal
|
||||
import structlog
|
||||
|
||||
from app.models.suppliers import (
|
||||
Supplier, PurchaseOrder, Delivery, SupplierQualityReview,
|
||||
SupplierStatus, SupplierType, PurchaseOrderStatus, DeliveryStatus
|
||||
)
|
||||
from app.models.performance import (
|
||||
SupplierPerformanceMetric, SupplierScorecard, SupplierAlert,
|
||||
PerformanceMetricType, PerformancePeriod, AlertSeverity, AlertStatus
|
||||
)
|
||||
from app.schemas.performance import (
|
||||
PerformanceDashboardSummary, SupplierPerformanceInsights,
|
||||
PerformanceAnalytics, BusinessModelInsights, AlertSummary
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DashboardService:
|
||||
"""Service for supplier performance dashboards and analytics"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger.bind(service="dashboard_service")
|
||||
|
||||
async def get_performance_dashboard_summary(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None
|
||||
) -> PerformanceDashboardSummary:
|
||||
"""Get comprehensive performance dashboard summary"""
|
||||
try:
|
||||
# Default date range - last 30 days
|
||||
if not date_to:
|
||||
date_to = datetime.now(timezone.utc)
|
||||
if not date_from:
|
||||
date_from = date_to - timedelta(days=30)
|
||||
|
||||
self.logger.info("Generating dashboard summary",
|
||||
tenant_id=str(tenant_id),
|
||||
date_from=date_from.isoformat(),
|
||||
date_to=date_to.isoformat())
|
||||
|
||||
# Get supplier statistics
|
||||
supplier_stats = await self._get_supplier_statistics(db, tenant_id)
|
||||
|
||||
# Get performance statistics
|
||||
performance_stats = await self._get_performance_statistics(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get alert statistics
|
||||
alert_stats = await self._get_alert_statistics(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get financial statistics
|
||||
financial_stats = await self._get_financial_statistics(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get business model insights
|
||||
business_model = await self._detect_business_model(db, tenant_id)
|
||||
|
||||
# Calculate trends
|
||||
trends = await self._calculate_performance_trends(db, tenant_id, date_from, date_to)
|
||||
|
||||
return PerformanceDashboardSummary(
|
||||
total_suppliers=supplier_stats['total_suppliers'],
|
||||
active_suppliers=supplier_stats['active_suppliers'],
|
||||
suppliers_above_threshold=performance_stats['above_threshold'],
|
||||
suppliers_below_threshold=performance_stats['below_threshold'],
|
||||
average_overall_score=performance_stats['avg_overall_score'],
|
||||
average_delivery_rate=performance_stats['avg_delivery_rate'],
|
||||
average_quality_rate=performance_stats['avg_quality_rate'],
|
||||
total_active_alerts=alert_stats['total_active'],
|
||||
critical_alerts=alert_stats['critical_alerts'],
|
||||
high_priority_alerts=alert_stats['high_priority'],
|
||||
recent_scorecards_generated=performance_stats['recent_scorecards'],
|
||||
cost_savings_this_month=financial_stats['cost_savings'],
|
||||
performance_trend=trends['performance_trend'],
|
||||
delivery_trend=trends['delivery_trend'],
|
||||
quality_trend=trends['quality_trend'],
|
||||
detected_business_model=business_model['model'],
|
||||
model_confidence=business_model['confidence'],
|
||||
business_model_metrics=business_model['metrics']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error generating dashboard summary", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_supplier_performance_insights(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
supplier_id: UUID,
|
||||
days_back: int = 30
|
||||
) -> SupplierPerformanceInsights:
|
||||
"""Get detailed performance insights for a specific supplier"""
|
||||
try:
|
||||
date_to = datetime.now(timezone.utc)
|
||||
date_from = date_to - timedelta(days=days_back)
|
||||
|
||||
# Get supplier info
|
||||
supplier = await self._get_supplier_info(db, supplier_id, tenant_id)
|
||||
|
||||
# Get current performance metrics
|
||||
current_metrics = await self._get_current_performance_metrics(db, supplier_id, tenant_id)
|
||||
|
||||
# Get previous period metrics for comparison
|
||||
previous_metrics = await self._get_previous_performance_metrics(db, supplier_id, tenant_id, days_back)
|
||||
|
||||
# Get recent activity statistics
|
||||
activity_stats = await self._get_supplier_activity_stats(db, supplier_id, tenant_id, date_from, date_to)
|
||||
|
||||
# Get alert summary
|
||||
alert_summary = await self._get_supplier_alert_summary(db, supplier_id, tenant_id, date_from, date_to)
|
||||
|
||||
# Calculate performance categorization
|
||||
performance_category = self._categorize_performance(current_metrics.get('overall_score', 0))
|
||||
risk_level = self._assess_risk_level(current_metrics, alert_summary)
|
||||
|
||||
# Generate recommendations
|
||||
recommendations = await self._generate_supplier_recommendations(
|
||||
db, supplier_id, tenant_id, current_metrics, activity_stats, alert_summary
|
||||
)
|
||||
|
||||
return SupplierPerformanceInsights(
|
||||
supplier_id=supplier_id,
|
||||
supplier_name=supplier['name'],
|
||||
current_overall_score=current_metrics.get('overall_score', 0),
|
||||
previous_score=previous_metrics.get('overall_score'),
|
||||
score_change_percentage=self._calculate_change_percentage(
|
||||
current_metrics.get('overall_score', 0),
|
||||
previous_metrics.get('overall_score')
|
||||
),
|
||||
performance_rank=current_metrics.get('rank'),
|
||||
delivery_performance=current_metrics.get('delivery_performance', 0),
|
||||
quality_performance=current_metrics.get('quality_performance', 0),
|
||||
cost_performance=current_metrics.get('cost_performance', 0),
|
||||
service_performance=current_metrics.get('service_performance', 0),
|
||||
orders_last_30_days=activity_stats['orders_count'],
|
||||
average_delivery_time=activity_stats['avg_delivery_time'],
|
||||
quality_issues_count=activity_stats['quality_issues'],
|
||||
cost_variance=activity_stats['cost_variance'],
|
||||
active_alerts=alert_summary['active_count'],
|
||||
resolved_alerts_last_30_days=alert_summary['resolved_count'],
|
||||
alert_trend=alert_summary['trend'],
|
||||
performance_category=performance_category,
|
||||
risk_level=risk_level,
|
||||
top_strengths=recommendations['strengths'],
|
||||
improvement_priorities=recommendations['improvements'],
|
||||
recommended_actions=recommendations['actions']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error generating supplier insights",
|
||||
supplier_id=str(supplier_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_performance_analytics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
period_days: int = 90
|
||||
) -> PerformanceAnalytics:
|
||||
"""Get advanced performance analytics"""
|
||||
try:
|
||||
date_to = datetime.now(timezone.utc)
|
||||
date_from = date_to - timedelta(days=period_days)
|
||||
|
||||
# Get performance distribution
|
||||
performance_distribution = await self._get_performance_distribution(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get trend analysis
|
||||
trends = await self._get_detailed_trends(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get comparative analysis
|
||||
comparative_analysis = await self._get_comparative_analysis(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get risk analysis
|
||||
risk_analysis = await self._get_risk_analysis(db, tenant_id, date_from, date_to)
|
||||
|
||||
# Get financial impact
|
||||
financial_impact = await self._get_financial_impact(db, tenant_id, date_from, date_to)
|
||||
|
||||
return PerformanceAnalytics(
|
||||
period_start=date_from,
|
||||
period_end=date_to,
|
||||
total_suppliers_analyzed=performance_distribution['total_suppliers'],
|
||||
performance_distribution=performance_distribution['distribution'],
|
||||
score_ranges=performance_distribution['score_ranges'],
|
||||
overall_trend=trends['overall'],
|
||||
delivery_trends=trends['delivery'],
|
||||
quality_trends=trends['quality'],
|
||||
cost_trends=trends['cost'],
|
||||
top_performers=comparative_analysis['top_performers'],
|
||||
underperformers=comparative_analysis['underperformers'],
|
||||
most_improved=comparative_analysis['most_improved'],
|
||||
biggest_declines=comparative_analysis['biggest_declines'],
|
||||
high_risk_suppliers=risk_analysis['high_risk'],
|
||||
contract_renewals_due=risk_analysis['contract_renewals'],
|
||||
certification_expiries=risk_analysis['certification_expiries'],
|
||||
total_procurement_value=financial_impact['total_value'],
|
||||
cost_savings_achieved=financial_impact['cost_savings'],
|
||||
cost_avoidance=financial_impact['cost_avoidance'],
|
||||
financial_risk_exposure=financial_impact['risk_exposure']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error generating performance analytics", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_business_model_insights(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID
|
||||
) -> BusinessModelInsights:
|
||||
"""Get business model detection and insights"""
|
||||
try:
|
||||
# Analyze supplier patterns
|
||||
supplier_patterns = await self._analyze_supplier_patterns(db, tenant_id)
|
||||
|
||||
# Detect business model
|
||||
business_model = await self._detect_business_model_detailed(db, tenant_id)
|
||||
|
||||
# Generate optimization recommendations
|
||||
optimization = await self._generate_optimization_recommendations(db, tenant_id, business_model)
|
||||
|
||||
# Get benchmarking data
|
||||
benchmarking = await self._get_benchmarking_data(db, tenant_id, business_model['model'])
|
||||
|
||||
return BusinessModelInsights(
|
||||
detected_model=business_model['model'],
|
||||
confidence_score=business_model['confidence'],
|
||||
model_characteristics=business_model['characteristics'],
|
||||
supplier_diversity_score=supplier_patterns['diversity_score'],
|
||||
procurement_volume_patterns=supplier_patterns['volume_patterns'],
|
||||
delivery_frequency_patterns=supplier_patterns['delivery_patterns'],
|
||||
order_size_patterns=supplier_patterns['order_size_patterns'],
|
||||
optimization_opportunities=optimization['opportunities'],
|
||||
recommended_supplier_mix=optimization['supplier_mix'],
|
||||
cost_optimization_potential=optimization['cost_potential'],
|
||||
risk_mitigation_suggestions=optimization['risk_mitigation'],
|
||||
industry_comparison=benchmarking['industry'],
|
||||
peer_comparison=benchmarking.get('peer')
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error generating business model insights", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_alert_summary(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None
|
||||
) -> List[AlertSummary]:
|
||||
"""Get alert summary by type and severity"""
|
||||
try:
|
||||
if not date_to:
|
||||
date_to = datetime.now(timezone.utc)
|
||||
if not date_from:
|
||||
date_from = date_to - timedelta(days=30)
|
||||
|
||||
query = select(
|
||||
SupplierAlert.alert_type,
|
||||
SupplierAlert.severity,
|
||||
func.count(SupplierAlert.id).label('count'),
|
||||
func.avg(
|
||||
func.extract('epoch', SupplierAlert.resolved_at - SupplierAlert.triggered_at) / 3600
|
||||
).label('avg_resolution_hours'),
|
||||
func.max(
|
||||
func.extract('epoch', func.current_timestamp() - SupplierAlert.triggered_at) / 3600
|
||||
).label('oldest_age_hours')
|
||||
).where(
|
||||
and_(
|
||||
SupplierAlert.tenant_id == tenant_id,
|
||||
SupplierAlert.triggered_at >= date_from,
|
||||
SupplierAlert.triggered_at <= date_to
|
||||
)
|
||||
).group_by(SupplierAlert.alert_type, SupplierAlert.severity)
|
||||
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
alert_summaries = []
|
||||
for row in rows:
|
||||
alert_summaries.append(AlertSummary(
|
||||
alert_type=row.alert_type,
|
||||
severity=row.severity,
|
||||
count=row.count,
|
||||
avg_resolution_time_hours=row.avg_resolution_hours,
|
||||
oldest_alert_age_hours=row.oldest_age_hours
|
||||
))
|
||||
|
||||
return alert_summaries
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error getting alert summary", error=str(e))
|
||||
raise
|
||||
|
||||
# === Private Helper Methods ===
|
||||
|
||||
async def _get_supplier_statistics(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, int]:
|
||||
"""Get basic supplier statistics"""
|
||||
query = select(
|
||||
func.count(Supplier.id).label('total_suppliers'),
|
||||
func.count(Supplier.id.filter(Supplier.status == SupplierStatus.ACTIVE)).label('active_suppliers')
|
||||
).where(Supplier.tenant_id == tenant_id)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
return {
|
||||
'total_suppliers': row.total_suppliers or 0,
|
||||
'active_suppliers': row.active_suppliers or 0
|
||||
}
|
||||
|
||||
async def _get_performance_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
date_from: datetime,
|
||||
date_to: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Get performance statistics"""
|
||||
# Get recent performance metrics
|
||||
query = select(
|
||||
func.avg(SupplierPerformanceMetric.metric_value).label('avg_score'),
|
||||
func.count(
|
||||
SupplierPerformanceMetric.id.filter(
|
||||
SupplierPerformanceMetric.metric_value >= settings.GOOD_DELIVERY_RATE
|
||||
)
|
||||
).label('above_threshold'),
|
||||
func.count(
|
||||
SupplierPerformanceMetric.id.filter(
|
||||
SupplierPerformanceMetric.metric_value < settings.GOOD_DELIVERY_RATE
|
||||
)
|
||||
).label('below_threshold')
|
||||
).where(
|
||||
and_(
|
||||
SupplierPerformanceMetric.tenant_id == tenant_id,
|
||||
SupplierPerformanceMetric.calculated_at >= date_from,
|
||||
SupplierPerformanceMetric.calculated_at <= date_to,
|
||||
SupplierPerformanceMetric.metric_type == PerformanceMetricType.DELIVERY_PERFORMANCE
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
# Get quality statistics
|
||||
quality_query = select(
|
||||
func.avg(SupplierPerformanceMetric.metric_value).label('avg_quality')
|
||||
).where(
|
||||
and_(
|
||||
SupplierPerformanceMetric.tenant_id == tenant_id,
|
||||
SupplierPerformanceMetric.calculated_at >= date_from,
|
||||
SupplierPerformanceMetric.calculated_at <= date_to,
|
||||
SupplierPerformanceMetric.metric_type == PerformanceMetricType.QUALITY_SCORE
|
||||
)
|
||||
)
|
||||
|
||||
quality_result = await db.execute(quality_query)
|
||||
quality_row = quality_result.first()
|
||||
|
||||
# Get scorecard count
|
||||
scorecard_query = select(func.count(SupplierScorecard.id)).where(
|
||||
and_(
|
||||
SupplierScorecard.tenant_id == tenant_id,
|
||||
SupplierScorecard.generated_at >= date_from,
|
||||
SupplierScorecard.generated_at <= date_to
|
||||
)
|
||||
)
|
||||
|
||||
scorecard_result = await db.execute(scorecard_query)
|
||||
scorecard_count = scorecard_result.scalar() or 0
|
||||
|
||||
return {
|
||||
'avg_overall_score': row.avg_score or 0,
|
||||
'above_threshold': row.above_threshold or 0,
|
||||
'below_threshold': row.below_threshold or 0,
|
||||
'avg_delivery_rate': row.avg_score or 0,
|
||||
'avg_quality_rate': quality_row.avg_quality or 0,
|
||||
'recent_scorecards': scorecard_count
|
||||
}
|
||||
|
||||
async def _get_alert_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
date_from: datetime,
|
||||
date_to: datetime
|
||||
) -> Dict[str, int]:
|
||||
"""Get alert statistics"""
|
||||
query = select(
|
||||
func.count(SupplierAlert.id.filter(SupplierAlert.status == AlertStatus.ACTIVE)).label('total_active'),
|
||||
func.count(SupplierAlert.id.filter(SupplierAlert.severity == AlertSeverity.CRITICAL)).label('critical'),
|
||||
func.count(SupplierAlert.id.filter(SupplierAlert.priority_score >= 70)).label('high_priority')
|
||||
).where(
|
||||
and_(
|
||||
SupplierAlert.tenant_id == tenant_id,
|
||||
SupplierAlert.triggered_at >= date_from,
|
||||
SupplierAlert.triggered_at <= date_to
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
return {
|
||||
'total_active': row.total_active or 0,
|
||||
'critical_alerts': row.critical or 0,
|
||||
'high_priority': row.high_priority or 0
|
||||
}
|
||||
|
||||
async def _get_financial_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
date_from: datetime,
|
||||
date_to: datetime
|
||||
) -> Dict[str, Decimal]:
|
||||
"""Get financial statistics"""
|
||||
# For now, return placeholder values
|
||||
# TODO: Implement cost savings calculation when pricing data is available
|
||||
return {
|
||||
'cost_savings': Decimal('0')
|
||||
}
|
||||
|
||||
async def _detect_business_model(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Detect business model based on supplier patterns"""
|
||||
# Get supplier count by category
|
||||
query = select(
|
||||
func.count(Supplier.id).label('total_suppliers'),
|
||||
func.count(Supplier.id.filter(Supplier.supplier_type == SupplierType.INGREDIENTS)).label('ingredient_suppliers')
|
||||
).where(
|
||||
and_(
|
||||
Supplier.tenant_id == tenant_id,
|
||||
Supplier.status == SupplierStatus.ACTIVE
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
total_suppliers = row.total_suppliers or 0
|
||||
ingredient_suppliers = row.ingredient_suppliers or 0
|
||||
|
||||
# Simple business model detection logic
|
||||
if total_suppliers >= settings.CENTRAL_BAKERY_THRESHOLD_SUPPLIERS:
|
||||
model = "central_bakery"
|
||||
confidence = 0.85
|
||||
elif total_suppliers >= settings.INDIVIDUAL_BAKERY_THRESHOLD_SUPPLIERS:
|
||||
model = "individual_bakery"
|
||||
confidence = 0.75
|
||||
else:
|
||||
model = "small_bakery"
|
||||
confidence = 0.60
|
||||
|
||||
return {
|
||||
'model': model,
|
||||
'confidence': confidence,
|
||||
'metrics': {
|
||||
'total_suppliers': total_suppliers,
|
||||
'ingredient_suppliers': ingredient_suppliers,
|
||||
'supplier_diversity': ingredient_suppliers / max(total_suppliers, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async def _calculate_performance_trends(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
date_from: datetime,
|
||||
date_to: datetime
|
||||
) -> Dict[str, str]:
|
||||
"""Calculate performance trends"""
|
||||
# For now, return stable trends
|
||||
# TODO: Implement trend calculation based on historical data
|
||||
return {
|
||||
'performance_trend': 'stable',
|
||||
'delivery_trend': 'stable',
|
||||
'quality_trend': 'stable'
|
||||
}
|
||||
|
||||
def _categorize_performance(self, score: float) -> str:
|
||||
"""Categorize performance based on score"""
|
||||
if score >= settings.EXCELLENT_DELIVERY_RATE:
|
||||
return "excellent"
|
||||
elif score >= settings.GOOD_DELIVERY_RATE:
|
||||
return "good"
|
||||
elif score >= settings.ACCEPTABLE_DELIVERY_RATE:
|
||||
return "acceptable"
|
||||
elif score >= settings.POOR_DELIVERY_RATE:
|
||||
return "needs_improvement"
|
||||
else:
|
||||
return "poor"
|
||||
|
||||
def _assess_risk_level(self, metrics: Dict[str, Any], alerts: Dict[str, Any]) -> str:
|
||||
"""Assess risk level based on metrics and alerts"""
|
||||
if alerts.get('active_count', 0) > 3 or metrics.get('overall_score', 0) < 50:
|
||||
return "critical"
|
||||
elif alerts.get('active_count', 0) > 1 or metrics.get('overall_score', 0) < 70:
|
||||
return "high"
|
||||
elif alerts.get('active_count', 0) > 0 or metrics.get('overall_score', 0) < 85:
|
||||
return "medium"
|
||||
else:
|
||||
return "low"
|
||||
|
||||
def _calculate_change_percentage(self, current: float, previous: Optional[float]) -> Optional[float]:
|
||||
"""Calculate percentage change between current and previous values"""
|
||||
if previous is None or previous == 0:
|
||||
return None
|
||||
return ((current - previous) / previous) * 100
|
||||
|
||||
# === Placeholder methods for complex analytics ===
|
||||
# These methods return placeholder data and should be implemented with actual business logic
|
||||
|
||||
async def _get_supplier_info(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID) -> Dict[str, Any]:
|
||||
stmt = select(Supplier).where(and_(Supplier.id == supplier_id, Supplier.tenant_id == tenant_id))
|
||||
result = await db.execute(stmt)
|
||||
supplier = result.scalar_one_or_none()
|
||||
return {'name': supplier.name if supplier else 'Unknown Supplier'}
|
||||
|
||||
async def _get_current_performance_metrics(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID) -> Dict[str, Any]:
|
||||
return {'overall_score': 75.0, 'delivery_performance': 80.0, 'quality_performance': 85.0, 'cost_performance': 70.0, 'service_performance': 75.0}
|
||||
|
||||
async def _get_previous_performance_metrics(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, days_back: int) -> Dict[str, Any]:
|
||||
return {'overall_score': 70.0}
|
||||
|
||||
async def _get_supplier_activity_stats(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {'orders_count': 15, 'avg_delivery_time': 3.2, 'quality_issues': 2, 'cost_variance': 5.5}
|
||||
|
||||
async def _get_supplier_alert_summary(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {'active_count': 1, 'resolved_count': 3, 'trend': 'improving'}
|
||||
|
||||
async def _generate_supplier_recommendations(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, metrics: Dict[str, Any], activity: Dict[str, Any], alerts: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
'strengths': ['Consistent quality', 'Reliable delivery'],
|
||||
'improvements': ['Cost optimization', 'Communication'],
|
||||
'actions': [{'action': 'Negotiate better pricing', 'priority': 'high'}]
|
||||
}
|
||||
|
||||
async def _get_performance_distribution(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {
|
||||
'total_suppliers': 25,
|
||||
'distribution': {'excellent': 5, 'good': 12, 'acceptable': 6, 'poor': 2},
|
||||
'score_ranges': {'excellent': [95, 100, 97.5], 'good': [80, 94, 87.0]}
|
||||
}
|
||||
|
||||
async def _get_detailed_trends(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {
|
||||
'overall': {'month_over_month': 2.5},
|
||||
'delivery': {'month_over_month': 1.8},
|
||||
'quality': {'month_over_month': 3.2},
|
||||
'cost': {'month_over_month': -1.5}
|
||||
}
|
||||
|
||||
async def _get_comparative_analysis(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {
|
||||
'top_performers': [],
|
||||
'underperformers': [],
|
||||
'most_improved': [],
|
||||
'biggest_declines': []
|
||||
}
|
||||
|
||||
async def _get_risk_analysis(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {
|
||||
'high_risk': [],
|
||||
'contract_renewals': [],
|
||||
'certification_expiries': []
|
||||
}
|
||||
|
||||
async def _get_financial_impact(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
||||
return {
|
||||
'total_value': Decimal('150000'),
|
||||
'cost_savings': Decimal('5000'),
|
||||
'cost_avoidance': Decimal('2000'),
|
||||
'risk_exposure': Decimal('10000')
|
||||
}
|
||||
|
||||
async def _analyze_supplier_patterns(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, Any]:
|
||||
return {
|
||||
'diversity_score': 75.0,
|
||||
'volume_patterns': {'peak_months': ['March', 'December']},
|
||||
'delivery_patterns': {'frequency': 'weekly'},
|
||||
'order_size_patterns': {'average_size': 'medium'}
|
||||
}
|
||||
|
||||
async def _detect_business_model_detailed(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, Any]:
|
||||
return {
|
||||
'model': 'individual_bakery',
|
||||
'confidence': 0.85,
|
||||
'characteristics': {'supplier_count': 15, 'order_frequency': 'weekly'}
|
||||
}
|
||||
|
||||
async def _generate_optimization_recommendations(self, db: AsyncSession, tenant_id: UUID, business_model: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
'opportunities': [{'type': 'consolidation', 'potential_savings': '10%'}],
|
||||
'supplier_mix': {'ingredients': '60%', 'packaging': '25%', 'services': '15%'},
|
||||
'cost_potential': Decimal('5000'),
|
||||
'risk_mitigation': ['Diversify supplier base', 'Implement backup suppliers']
|
||||
}
|
||||
|
||||
async def _get_benchmarking_data(self, db: AsyncSession, tenant_id: UUID, business_model: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'industry': {'delivery_rate': 88.5, 'quality_score': 91.2},
|
||||
'peer': {'delivery_rate': 86.8, 'quality_score': 89.5}
|
||||
}
|
||||
662
services/suppliers/app/services/performance_service.py
Normal file
662
services/suppliers/app/services/performance_service.py
Normal file
@@ -0,0 +1,662 @@
|
||||
# ================================================================
|
||||
# services/suppliers/app/services/performance_service.py
|
||||
# ================================================================
|
||||
"""
|
||||
Supplier Performance Tracking Service
|
||||
Comprehensive supplier performance calculation, tracking, and analytics
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from uuid import UUID
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_, or_, desc, asc
|
||||
from sqlalchemy.orm import selectinload
|
||||
import structlog
|
||||
from decimal import Decimal
|
||||
|
||||
from app.models.suppliers import (
|
||||
Supplier, PurchaseOrder, Delivery, SupplierQualityReview,
|
||||
PurchaseOrderStatus, DeliveryStatus, QualityRating, DeliveryRating
|
||||
)
|
||||
from app.models.performance import (
|
||||
SupplierPerformanceMetric, SupplierScorecard, SupplierAlert,
|
||||
PerformanceMetricType, PerformancePeriod, AlertType, AlertSeverity,
|
||||
AlertStatus
|
||||
)
|
||||
from app.schemas.performance import (
|
||||
PerformanceMetricCreate, ScorecardCreate, AlertCreate,
|
||||
PerformanceDashboardSummary, SupplierPerformanceInsights,
|
||||
PerformanceAnalytics, BusinessModelInsights
|
||||
)
|
||||
from app.core.config import settings
|
||||
from shared.database.transactions import transactional
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class PerformanceTrackingService:
|
||||
"""Service for tracking and calculating supplier performance metrics"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger.bind(service="performance_tracking")
|
||||
|
||||
@transactional
|
||||
async def calculate_supplier_performance(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
tenant_id: UUID,
|
||||
period: PerformancePeriod,
|
||||
period_start: datetime,
|
||||
period_end: datetime
|
||||
) -> SupplierPerformanceMetric:
|
||||
"""Calculate comprehensive performance metrics for a supplier"""
|
||||
try:
|
||||
self.logger.info("Calculating supplier performance",
|
||||
supplier_id=str(supplier_id),
|
||||
period=period.value,
|
||||
period_start=period_start.isoformat(),
|
||||
period_end=period_end.isoformat())
|
||||
|
||||
# Get base data for calculations
|
||||
orders_data = await self._get_orders_data(db, supplier_id, tenant_id, period_start, period_end)
|
||||
deliveries_data = await self._get_deliveries_data(db, supplier_id, tenant_id, period_start, period_end)
|
||||
quality_data = await self._get_quality_data(db, supplier_id, tenant_id, period_start, period_end)
|
||||
|
||||
# Calculate delivery performance
|
||||
delivery_performance = await self._calculate_delivery_performance(
|
||||
orders_data, deliveries_data
|
||||
)
|
||||
|
||||
# Calculate quality performance
|
||||
quality_performance = await self._calculate_quality_performance(
|
||||
deliveries_data, quality_data
|
||||
)
|
||||
|
||||
# Calculate cost performance
|
||||
cost_performance = await self._calculate_cost_performance(
|
||||
orders_data, deliveries_data
|
||||
)
|
||||
|
||||
# Calculate service performance
|
||||
service_performance = await self._calculate_service_performance(
|
||||
orders_data, quality_data
|
||||
)
|
||||
|
||||
# Calculate overall performance (weighted average)
|
||||
overall_performance = (
|
||||
delivery_performance * 0.30 +
|
||||
quality_performance * 0.30 +
|
||||
cost_performance * 0.20 +
|
||||
service_performance * 0.20
|
||||
)
|
||||
|
||||
# Create performance metrics for each category
|
||||
performance_metrics = []
|
||||
|
||||
metrics_to_create = [
|
||||
(PerformanceMetricType.DELIVERY_PERFORMANCE, delivery_performance),
|
||||
(PerformanceMetricType.QUALITY_SCORE, quality_performance),
|
||||
(PerformanceMetricType.PRICE_COMPETITIVENESS, cost_performance),
|
||||
(PerformanceMetricType.COMMUNICATION_RATING, service_performance)
|
||||
]
|
||||
|
||||
for metric_type, value in metrics_to_create:
|
||||
# Get previous period value for trend calculation
|
||||
previous_value = await self._get_previous_period_value(
|
||||
db, supplier_id, tenant_id, metric_type, period, period_start
|
||||
)
|
||||
|
||||
# Calculate trend
|
||||
trend_direction, trend_percentage = self._calculate_trend(value, previous_value)
|
||||
|
||||
# Prepare detailed metrics data
|
||||
metrics_data = await self._prepare_detailed_metrics(
|
||||
metric_type, orders_data, deliveries_data, quality_data
|
||||
)
|
||||
|
||||
# Create performance metric
|
||||
metric_create = PerformanceMetricCreate(
|
||||
supplier_id=supplier_id,
|
||||
metric_type=metric_type,
|
||||
period=period,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
metric_value=value,
|
||||
target_value=self._get_target_value(metric_type),
|
||||
total_orders=orders_data.get('total_orders', 0),
|
||||
total_deliveries=deliveries_data.get('total_deliveries', 0),
|
||||
on_time_deliveries=deliveries_data.get('on_time_deliveries', 0),
|
||||
late_deliveries=deliveries_data.get('late_deliveries', 0),
|
||||
quality_issues=quality_data.get('quality_issues', 0),
|
||||
total_amount=orders_data.get('total_amount', Decimal('0')),
|
||||
metrics_data=metrics_data
|
||||
)
|
||||
|
||||
performance_metric = SupplierPerformanceMetric(
|
||||
tenant_id=tenant_id,
|
||||
supplier_id=supplier_id,
|
||||
metric_type=metric_create.metric_type,
|
||||
period=metric_create.period,
|
||||
period_start=metric_create.period_start,
|
||||
period_end=metric_create.period_end,
|
||||
metric_value=metric_create.metric_value,
|
||||
target_value=metric_create.target_value,
|
||||
previous_value=previous_value,
|
||||
total_orders=metric_create.total_orders,
|
||||
total_deliveries=metric_create.total_deliveries,
|
||||
on_time_deliveries=metric_create.on_time_deliveries,
|
||||
late_deliveries=metric_create.late_deliveries,
|
||||
quality_issues=metric_create.quality_issues,
|
||||
total_amount=metric_create.total_amount,
|
||||
metrics_data=metric_create.metrics_data,
|
||||
trend_direction=trend_direction,
|
||||
trend_percentage=trend_percentage,
|
||||
calculated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.add(performance_metric)
|
||||
performance_metrics.append(performance_metric)
|
||||
|
||||
await db.flush()
|
||||
|
||||
# Update supplier's overall performance ratings
|
||||
await self._update_supplier_ratings(db, supplier_id, overall_performance, quality_performance)
|
||||
|
||||
self.logger.info("Supplier performance calculated successfully",
|
||||
supplier_id=str(supplier_id),
|
||||
overall_performance=overall_performance)
|
||||
|
||||
# Return the overall performance metric
|
||||
return performance_metrics[0] if performance_metrics else None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error calculating supplier performance",
|
||||
supplier_id=str(supplier_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def _get_orders_data(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
tenant_id: UUID,
|
||||
period_start: datetime,
|
||||
period_end: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Get orders data for performance calculation"""
|
||||
query = select(
|
||||
func.count(PurchaseOrder.id).label('total_orders'),
|
||||
func.sum(PurchaseOrder.total_amount).label('total_amount'),
|
||||
func.avg(PurchaseOrder.total_amount).label('avg_order_value'),
|
||||
func.count(
|
||||
PurchaseOrder.id.filter(
|
||||
PurchaseOrder.status == PurchaseOrderStatus.COMPLETED
|
||||
)
|
||||
).label('completed_orders')
|
||||
).where(
|
||||
and_(
|
||||
PurchaseOrder.supplier_id == supplier_id,
|
||||
PurchaseOrder.tenant_id == tenant_id,
|
||||
PurchaseOrder.order_date >= period_start,
|
||||
PurchaseOrder.order_date <= period_end
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
return {
|
||||
'total_orders': row.total_orders or 0,
|
||||
'total_amount': row.total_amount or Decimal('0'),
|
||||
'avg_order_value': row.avg_order_value or Decimal('0'),
|
||||
'completed_orders': row.completed_orders or 0
|
||||
}
|
||||
|
||||
async def _get_deliveries_data(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
tenant_id: UUID,
|
||||
period_start: datetime,
|
||||
period_end: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Get deliveries data for performance calculation"""
|
||||
# Get delivery statistics
|
||||
query = select(
|
||||
func.count(Delivery.id).label('total_deliveries'),
|
||||
func.count(
|
||||
Delivery.id.filter(
|
||||
and_(
|
||||
Delivery.actual_arrival <= Delivery.scheduled_date,
|
||||
Delivery.status == DeliveryStatus.DELIVERED
|
||||
)
|
||||
)
|
||||
).label('on_time_deliveries'),
|
||||
func.count(
|
||||
Delivery.id.filter(
|
||||
and_(
|
||||
Delivery.actual_arrival > Delivery.scheduled_date,
|
||||
Delivery.status == DeliveryStatus.DELIVERED
|
||||
)
|
||||
)
|
||||
).label('late_deliveries'),
|
||||
func.avg(
|
||||
func.extract('epoch', Delivery.actual_arrival - Delivery.scheduled_date) / 3600
|
||||
).label('avg_delay_hours')
|
||||
).where(
|
||||
and_(
|
||||
Delivery.supplier_id == supplier_id,
|
||||
Delivery.tenant_id == tenant_id,
|
||||
Delivery.scheduled_date >= period_start,
|
||||
Delivery.scheduled_date <= period_end,
|
||||
Delivery.status.in_([DeliveryStatus.DELIVERED, DeliveryStatus.PARTIALLY_DELIVERED])
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
return {
|
||||
'total_deliveries': row.total_deliveries or 0,
|
||||
'on_time_deliveries': row.on_time_deliveries or 0,
|
||||
'late_deliveries': row.late_deliveries or 0,
|
||||
'avg_delay_hours': row.avg_delay_hours or 0
|
||||
}
|
||||
|
||||
async def _get_quality_data(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
tenant_id: UUID,
|
||||
period_start: datetime,
|
||||
period_end: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Get quality data for performance calculation"""
|
||||
query = select(
|
||||
func.count(SupplierQualityReview.id).label('total_reviews'),
|
||||
func.avg(
|
||||
func.cast(SupplierQualityReview.quality_rating, func.Float)
|
||||
).label('avg_quality_rating'),
|
||||
func.avg(
|
||||
func.cast(SupplierQualityReview.delivery_rating, func.Float)
|
||||
).label('avg_delivery_rating'),
|
||||
func.avg(SupplierQualityReview.communication_rating).label('avg_communication_rating'),
|
||||
func.count(
|
||||
SupplierQualityReview.id.filter(
|
||||
SupplierQualityReview.quality_issues.isnot(None)
|
||||
)
|
||||
).label('quality_issues')
|
||||
).where(
|
||||
and_(
|
||||
SupplierQualityReview.supplier_id == supplier_id,
|
||||
SupplierQualityReview.tenant_id == tenant_id,
|
||||
SupplierQualityReview.review_date >= period_start,
|
||||
SupplierQualityReview.review_date <= period_end
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
|
||||
return {
|
||||
'total_reviews': row.total_reviews or 0,
|
||||
'avg_quality_rating': row.avg_quality_rating or 0,
|
||||
'avg_delivery_rating': row.avg_delivery_rating or 0,
|
||||
'avg_communication_rating': row.avg_communication_rating or 0,
|
||||
'quality_issues': row.quality_issues or 0
|
||||
}
|
||||
|
||||
async def _calculate_delivery_performance(
|
||||
self,
|
||||
orders_data: Dict[str, Any],
|
||||
deliveries_data: Dict[str, Any]
|
||||
) -> float:
|
||||
"""Calculate delivery performance score (0-100)"""
|
||||
total_deliveries = deliveries_data.get('total_deliveries', 0)
|
||||
if total_deliveries == 0:
|
||||
return 0.0
|
||||
|
||||
on_time_deliveries = deliveries_data.get('on_time_deliveries', 0)
|
||||
on_time_rate = (on_time_deliveries / total_deliveries) * 100
|
||||
|
||||
# Apply penalty for average delay
|
||||
avg_delay_hours = deliveries_data.get('avg_delay_hours', 0)
|
||||
delay_penalty = min(avg_delay_hours * 2, 20) # Max 20 point penalty
|
||||
|
||||
performance_score = max(on_time_rate - delay_penalty, 0)
|
||||
return min(performance_score, 100.0)
|
||||
|
||||
async def _calculate_quality_performance(
|
||||
self,
|
||||
deliveries_data: Dict[str, Any],
|
||||
quality_data: Dict[str, Any]
|
||||
) -> float:
|
||||
"""Calculate quality performance score (0-100)"""
|
||||
total_reviews = quality_data.get('total_reviews', 0)
|
||||
if total_reviews == 0:
|
||||
return 50.0 # Default score when no reviews
|
||||
|
||||
# Base quality score from ratings
|
||||
avg_quality_rating = quality_data.get('avg_quality_rating', 0)
|
||||
base_score = (avg_quality_rating / 5.0) * 100
|
||||
|
||||
# Apply penalty for quality issues
|
||||
quality_issues = quality_data.get('quality_issues', 0)
|
||||
issue_penalty = min(quality_issues * 5, 30) # Max 30 point penalty
|
||||
|
||||
performance_score = max(base_score - issue_penalty, 0)
|
||||
return min(performance_score, 100.0)
|
||||
|
||||
async def _calculate_cost_performance(
|
||||
self,
|
||||
orders_data: Dict[str, Any],
|
||||
deliveries_data: Dict[str, Any]
|
||||
) -> float:
|
||||
"""Calculate cost performance score (0-100)"""
|
||||
# For now, return a baseline score
|
||||
# In future, implement price comparison with market rates
|
||||
return 75.0
|
||||
|
||||
async def _calculate_service_performance(
|
||||
self,
|
||||
orders_data: Dict[str, Any],
|
||||
quality_data: Dict[str, Any]
|
||||
) -> float:
|
||||
"""Calculate service performance score (0-100)"""
|
||||
total_reviews = quality_data.get('total_reviews', 0)
|
||||
if total_reviews == 0:
|
||||
return 50.0 # Default score when no reviews
|
||||
|
||||
avg_communication_rating = quality_data.get('avg_communication_rating', 0)
|
||||
return (avg_communication_rating / 5.0) * 100
|
||||
|
||||
def _calculate_trend(self, current_value: float, previous_value: Optional[float]) -> Tuple[Optional[str], Optional[float]]:
|
||||
"""Calculate performance trend"""
|
||||
if previous_value is None or previous_value == 0:
|
||||
return None, None
|
||||
|
||||
change_percentage = ((current_value - previous_value) / previous_value) * 100
|
||||
|
||||
if abs(change_percentage) < 2: # Less than 2% change considered stable
|
||||
trend_direction = "stable"
|
||||
elif change_percentage > 0:
|
||||
trend_direction = "improving"
|
||||
else:
|
||||
trend_direction = "declining"
|
||||
|
||||
return trend_direction, change_percentage
|
||||
|
||||
async def _get_previous_period_value(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
tenant_id: UUID,
|
||||
metric_type: PerformanceMetricType,
|
||||
period: PerformancePeriod,
|
||||
current_period_start: datetime
|
||||
) -> Optional[float]:
|
||||
"""Get the previous period's value for trend calculation"""
|
||||
# Calculate previous period dates
|
||||
if period == PerformancePeriod.DAILY:
|
||||
previous_start = current_period_start - timedelta(days=1)
|
||||
previous_end = current_period_start
|
||||
elif period == PerformancePeriod.WEEKLY:
|
||||
previous_start = current_period_start - timedelta(weeks=1)
|
||||
previous_end = current_period_start
|
||||
elif period == PerformancePeriod.MONTHLY:
|
||||
previous_start = current_period_start - timedelta(days=30)
|
||||
previous_end = current_period_start
|
||||
elif period == PerformancePeriod.QUARTERLY:
|
||||
previous_start = current_period_start - timedelta(days=90)
|
||||
previous_end = current_period_start
|
||||
else: # YEARLY
|
||||
previous_start = current_period_start - timedelta(days=365)
|
||||
previous_end = current_period_start
|
||||
|
||||
query = select(SupplierPerformanceMetric.metric_value).where(
|
||||
and_(
|
||||
SupplierPerformanceMetric.supplier_id == supplier_id,
|
||||
SupplierPerformanceMetric.tenant_id == tenant_id,
|
||||
SupplierPerformanceMetric.metric_type == metric_type,
|
||||
SupplierPerformanceMetric.period == period,
|
||||
SupplierPerformanceMetric.period_start >= previous_start,
|
||||
SupplierPerformanceMetric.period_start < previous_end
|
||||
)
|
||||
).order_by(desc(SupplierPerformanceMetric.period_start)).limit(1)
|
||||
|
||||
result = await db.execute(query)
|
||||
row = result.first()
|
||||
return row[0] if row else None
|
||||
|
||||
def _get_target_value(self, metric_type: PerformanceMetricType) -> float:
|
||||
"""Get target value for metric type"""
|
||||
targets = {
|
||||
PerformanceMetricType.DELIVERY_PERFORMANCE: settings.GOOD_DELIVERY_RATE,
|
||||
PerformanceMetricType.QUALITY_SCORE: settings.GOOD_QUALITY_RATE,
|
||||
PerformanceMetricType.PRICE_COMPETITIVENESS: 80.0,
|
||||
PerformanceMetricType.COMMUNICATION_RATING: 80.0,
|
||||
PerformanceMetricType.ORDER_ACCURACY: 95.0,
|
||||
PerformanceMetricType.RESPONSE_TIME: 90.0,
|
||||
PerformanceMetricType.COMPLIANCE_SCORE: 95.0,
|
||||
PerformanceMetricType.FINANCIAL_STABILITY: 85.0
|
||||
}
|
||||
return targets.get(metric_type, 80.0)
|
||||
|
||||
async def _prepare_detailed_metrics(
|
||||
self,
|
||||
metric_type: PerformanceMetricType,
|
||||
orders_data: Dict[str, Any],
|
||||
deliveries_data: Dict[str, Any],
|
||||
quality_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare detailed metrics breakdown"""
|
||||
if metric_type == PerformanceMetricType.DELIVERY_PERFORMANCE:
|
||||
return {
|
||||
"on_time_rate": (deliveries_data.get('on_time_deliveries', 0) /
|
||||
max(deliveries_data.get('total_deliveries', 1), 1)) * 100,
|
||||
"avg_delay_hours": deliveries_data.get('avg_delay_hours', 0),
|
||||
"late_delivery_count": deliveries_data.get('late_deliveries', 0)
|
||||
}
|
||||
elif metric_type == PerformanceMetricType.QUALITY_SCORE:
|
||||
return {
|
||||
"avg_quality_rating": quality_data.get('avg_quality_rating', 0),
|
||||
"quality_issues_count": quality_data.get('quality_issues', 0),
|
||||
"total_reviews": quality_data.get('total_reviews', 0)
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
async def _update_supplier_ratings(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
overall_performance: float,
|
||||
quality_performance: float
|
||||
) -> None:
|
||||
"""Update supplier's overall ratings"""
|
||||
stmt = select(Supplier).where(Supplier.id == supplier_id)
|
||||
result = await db.execute(stmt)
|
||||
supplier = result.scalar_one_or_none()
|
||||
|
||||
if supplier:
|
||||
supplier.quality_rating = quality_performance / 20 # Convert to 1-5 scale
|
||||
supplier.delivery_rating = overall_performance / 20 # Convert to 1-5 scale
|
||||
db.add(supplier)
|
||||
|
||||
|
||||
class AlertService:
|
||||
"""Service for managing supplier alerts"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger.bind(service="alert_service")
|
||||
|
||||
@transactional
|
||||
async def evaluate_performance_alerts(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
tenant_id: UUID,
|
||||
supplier_id: Optional[UUID] = None
|
||||
) -> List[SupplierAlert]:
|
||||
"""Evaluate and create performance-based alerts"""
|
||||
try:
|
||||
alerts_created = []
|
||||
|
||||
# Get suppliers to evaluate
|
||||
if supplier_id:
|
||||
supplier_filter = and_(Supplier.id == supplier_id, Supplier.tenant_id == tenant_id)
|
||||
else:
|
||||
supplier_filter = and_(Supplier.tenant_id == tenant_id, Supplier.status == "active")
|
||||
|
||||
stmt = select(Supplier).where(supplier_filter)
|
||||
result = await db.execute(stmt)
|
||||
suppliers = result.scalars().all()
|
||||
|
||||
for supplier in suppliers:
|
||||
# Get recent performance metrics
|
||||
recent_metrics = await self._get_recent_performance_metrics(db, supplier.id, tenant_id)
|
||||
|
||||
# Evaluate delivery performance alerts
|
||||
delivery_alerts = await self._evaluate_delivery_alerts(db, supplier, recent_metrics)
|
||||
alerts_created.extend(delivery_alerts)
|
||||
|
||||
# Evaluate quality alerts
|
||||
quality_alerts = await self._evaluate_quality_alerts(db, supplier, recent_metrics)
|
||||
alerts_created.extend(quality_alerts)
|
||||
|
||||
# Evaluate cost variance alerts
|
||||
cost_alerts = await self._evaluate_cost_alerts(db, supplier, recent_metrics)
|
||||
alerts_created.extend(cost_alerts)
|
||||
|
||||
return alerts_created
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error evaluating performance alerts", error=str(e))
|
||||
raise
|
||||
|
||||
async def _get_recent_performance_metrics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier_id: UUID,
|
||||
tenant_id: UUID
|
||||
) -> Dict[PerformanceMetricType, SupplierPerformanceMetric]:
|
||||
"""Get recent performance metrics for a supplier"""
|
||||
query = select(SupplierPerformanceMetric).where(
|
||||
and_(
|
||||
SupplierPerformanceMetric.supplier_id == supplier_id,
|
||||
SupplierPerformanceMetric.tenant_id == tenant_id,
|
||||
SupplierPerformanceMetric.calculated_at >= datetime.now(timezone.utc) - timedelta(days=7)
|
||||
)
|
||||
).order_by(desc(SupplierPerformanceMetric.calculated_at))
|
||||
|
||||
result = await db.execute(query)
|
||||
metrics = result.scalars().all()
|
||||
|
||||
# Return the most recent metric for each type
|
||||
metrics_dict = {}
|
||||
for metric in metrics:
|
||||
if metric.metric_type not in metrics_dict:
|
||||
metrics_dict[metric.metric_type] = metric
|
||||
|
||||
return metrics_dict
|
||||
|
||||
async def _evaluate_delivery_alerts(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier: Supplier,
|
||||
metrics: Dict[PerformanceMetricType, SupplierPerformanceMetric]
|
||||
) -> List[SupplierAlert]:
|
||||
"""Evaluate delivery performance alerts"""
|
||||
alerts = []
|
||||
|
||||
delivery_metric = metrics.get(PerformanceMetricType.DELIVERY_PERFORMANCE)
|
||||
if not delivery_metric:
|
||||
return alerts
|
||||
|
||||
# Poor delivery performance alert
|
||||
if delivery_metric.metric_value < settings.POOR_DELIVERY_RATE:
|
||||
severity = AlertSeverity.CRITICAL if delivery_metric.metric_value < 70 else AlertSeverity.HIGH
|
||||
|
||||
alert = SupplierAlert(
|
||||
tenant_id=supplier.tenant_id,
|
||||
supplier_id=supplier.id,
|
||||
alert_type=AlertType.POOR_QUALITY,
|
||||
severity=severity,
|
||||
title=f"Poor Delivery Performance - {supplier.name}",
|
||||
message=f"Delivery performance has dropped to {delivery_metric.metric_value:.1f}%",
|
||||
description=f"Supplier {supplier.name} delivery performance is below acceptable threshold",
|
||||
trigger_value=delivery_metric.metric_value,
|
||||
threshold_value=settings.POOR_DELIVERY_RATE,
|
||||
metric_type=PerformanceMetricType.DELIVERY_PERFORMANCE,
|
||||
performance_metric_id=delivery_metric.id,
|
||||
priority_score=90 if severity == AlertSeverity.CRITICAL else 70,
|
||||
business_impact="high" if severity == AlertSeverity.CRITICAL else "medium",
|
||||
recommended_actions=[
|
||||
{"action": "Review delivery processes with supplier"},
|
||||
{"action": "Request delivery improvement plan"},
|
||||
{"action": "Consider alternative suppliers"}
|
||||
]
|
||||
)
|
||||
|
||||
db.add(alert)
|
||||
alerts.append(alert)
|
||||
|
||||
return alerts
|
||||
|
||||
async def _evaluate_quality_alerts(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier: Supplier,
|
||||
metrics: Dict[PerformanceMetricType, SupplierPerformanceMetric]
|
||||
) -> List[SupplierAlert]:
|
||||
"""Evaluate quality performance alerts"""
|
||||
alerts = []
|
||||
|
||||
quality_metric = metrics.get(PerformanceMetricType.QUALITY_SCORE)
|
||||
if not quality_metric:
|
||||
return alerts
|
||||
|
||||
# Poor quality performance alert
|
||||
if quality_metric.metric_value < settings.POOR_QUALITY_RATE:
|
||||
severity = AlertSeverity.CRITICAL if quality_metric.metric_value < 70 else AlertSeverity.HIGH
|
||||
|
||||
alert = SupplierAlert(
|
||||
tenant_id=supplier.tenant_id,
|
||||
supplier_id=supplier.id,
|
||||
alert_type=AlertType.POOR_QUALITY,
|
||||
severity=severity,
|
||||
title=f"Poor Quality Performance - {supplier.name}",
|
||||
message=f"Quality performance has dropped to {quality_metric.metric_value:.1f}%",
|
||||
description=f"Supplier {supplier.name} quality performance is below acceptable threshold",
|
||||
trigger_value=quality_metric.metric_value,
|
||||
threshold_value=settings.POOR_QUALITY_RATE,
|
||||
metric_type=PerformanceMetricType.QUALITY_SCORE,
|
||||
performance_metric_id=quality_metric.id,
|
||||
priority_score=95 if severity == AlertSeverity.CRITICAL else 75,
|
||||
business_impact="high" if severity == AlertSeverity.CRITICAL else "medium",
|
||||
recommended_actions=[
|
||||
{"action": "Conduct quality audit with supplier"},
|
||||
{"action": "Request quality improvement plan"},
|
||||
{"action": "Increase incoming inspection frequency"}
|
||||
]
|
||||
)
|
||||
|
||||
db.add(alert)
|
||||
alerts.append(alert)
|
||||
|
||||
return alerts
|
||||
|
||||
async def _evaluate_cost_alerts(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
supplier: Supplier,
|
||||
metrics: Dict[PerformanceMetricType, SupplierPerformanceMetric]
|
||||
) -> List[SupplierAlert]:
|
||||
"""Evaluate cost variance alerts"""
|
||||
alerts = []
|
||||
|
||||
# For now, return empty list - cost analysis requires market data
|
||||
# TODO: Implement cost variance analysis when price benchmarks are available
|
||||
|
||||
return alerts
|
||||
Reference in New Issue
Block a user