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

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

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

View 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