Files
bakery-ia/services/procurement/tests/test_supplier_performance_predictor.py
2025-11-05 13:34:56 +01:00

482 lines
16 KiB
Python

"""
Tests for Supplier Performance Predictor
"""
import pytest
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from app.ml.supplier_performance_predictor import SupplierPerformancePredictor
@pytest.fixture
def sample_order_history_good_supplier():
"""Generate sample order history for a reliable supplier."""
dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='W')
orders = []
for i, date in enumerate(dates):
expected_delivery = date + timedelta(days=3)
# Good supplier: 95% on-time, occasional 1-day delay
if np.random.random() < 0.95:
actual_delivery = expected_delivery
else:
actual_delivery = expected_delivery + timedelta(days=1)
# Good quality: 98% no issues
quality_issues = np.random.random() > 0.98
quality_score = np.random.uniform(90, 100) if not quality_issues else np.random.uniform(70, 85)
# Good quantity accuracy: 99% accurate
quantity_accuracy = np.random.uniform(0.98, 1.02)
orders.append({
'order_id': f'order-{i}',
'order_date': date,
'expected_delivery_date': expected_delivery,
'actual_delivery_date': actual_delivery,
'order_quantity': 100,
'received_quantity': int(100 * quantity_accuracy),
'quality_issues': quality_issues,
'quality_score': quality_score,
'order_value': 500.0
})
return pd.DataFrame(orders)
@pytest.fixture
def sample_order_history_poor_supplier():
"""Generate sample order history for an unreliable supplier."""
dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='W')
orders = []
for i, date in enumerate(dates):
expected_delivery = date + timedelta(days=3)
# Poor supplier: 60% on-time, frequent delays of 2-5 days
if np.random.random() < 0.60:
actual_delivery = expected_delivery
else:
actual_delivery = expected_delivery + timedelta(days=np.random.randint(2, 6))
# Poor quality: 20% issues
quality_issues = np.random.random() > 0.80
quality_score = np.random.uniform(85, 100) if not quality_issues else np.random.uniform(50, 75)
# Poor quantity accuracy: frequent short deliveries
if np.random.random() < 0.25:
quantity_accuracy = np.random.uniform(0.75, 0.95) # Short delivery
else:
quantity_accuracy = np.random.uniform(0.95, 1.05)
orders.append({
'order_id': f'order-{i}',
'order_date': date,
'expected_delivery_date': expected_delivery,
'actual_delivery_date': actual_delivery,
'order_quantity': 100,
'received_quantity': int(100 * quantity_accuracy),
'quality_issues': quality_issues,
'quality_score': quality_score,
'order_value': 500.0
})
return pd.DataFrame(orders)
@pytest.mark.asyncio
async def test_analyze_good_supplier(sample_order_history_good_supplier):
"""Test analysis of a reliable supplier."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='good-supplier',
order_history=sample_order_history_good_supplier,
min_orders=10
)
# Check structure
assert 'tenant_id' in results
assert 'supplier_id' in results
assert 'reliability_score' in results
assert 'metrics' in results
assert 'predictions' in results
assert 'risk_assessment' in results
assert 'insights' in results
# Check metrics calculated
metrics = results['metrics']
assert metrics['total_orders'] == len(sample_order_history_good_supplier)
assert 'on_time_rate' in metrics
assert 'quality_issue_rate' in metrics
assert 'avg_quantity_accuracy' in metrics
# Good supplier should have high reliability score
reliability_score = results['reliability_score']
assert reliability_score >= 85, f"Expected high reliability, got {reliability_score}"
# Risk should be low
risk_assessment = results['risk_assessment']
assert risk_assessment['risk_level'] in ['low', 'medium']
@pytest.mark.asyncio
async def test_analyze_poor_supplier(sample_order_history_poor_supplier):
"""Test analysis of an unreliable supplier."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='poor-supplier',
order_history=sample_order_history_poor_supplier,
min_orders=10
)
# Poor supplier should have low reliability score
reliability_score = results['reliability_score']
assert reliability_score < 75, f"Expected low reliability, got {reliability_score}"
# Risk should be high or critical
risk_assessment = results['risk_assessment']
assert risk_assessment['risk_level'] in ['medium', 'high', 'critical']
# Should have risk factors
assert len(risk_assessment['risk_factors']) > 0
# Should generate insights
insights = results['insights']
assert len(insights) > 0
# Should have at least one alert or prediction
alert_insights = [i for i in insights if i['type'] in ['alert', 'prediction']]
assert len(alert_insights) > 0
@pytest.mark.asyncio
async def test_performance_metrics_calculation(sample_order_history_good_supplier):
"""Test detailed metrics calculation."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='test-supplier',
order_history=sample_order_history_good_supplier
)
metrics = results['metrics']
# Check all key metrics present
required_metrics = [
'total_orders',
'on_time_orders',
'delayed_orders',
'on_time_rate',
'avg_delivery_delay_days',
'avg_quantity_accuracy',
'short_deliveries',
'short_delivery_rate',
'quality_issues',
'quality_issue_rate',
'avg_quality_score',
'delivery_consistency',
'quantity_consistency'
]
for metric in required_metrics:
assert metric in metrics, f"Missing metric: {metric}"
# Check metrics are reasonable
assert 0 <= metrics['on_time_rate'] <= 100
assert 0 <= metrics['avg_quantity_accuracy'] <= 200 # Allow up to 200% over-delivery
assert 0 <= metrics['quality_issue_rate'] <= 100
assert 0 <= metrics['avg_quality_score'] <= 100
@pytest.mark.asyncio
async def test_reliability_score_calculation():
"""Test reliability score calculation with known inputs."""
predictor = SupplierPerformancePredictor()
# Perfect metrics
perfect_metrics = {
'on_time_rate': 100.0,
'avg_quantity_accuracy': 100.0,
'avg_quality_score': 100.0,
'delivery_consistency': 100.0,
'quantity_consistency': 100.0,
'quality_issue_rate': 0.0,
'short_delivery_rate': 0.0
}
perfect_score = predictor._calculate_reliability_score(perfect_metrics)
assert perfect_score >= 95, f"Expected perfect score ~100, got {perfect_score}"
# Poor metrics
poor_metrics = {
'on_time_rate': 50.0,
'avg_quantity_accuracy': 85.0,
'avg_quality_score': 70.0,
'delivery_consistency': 50.0,
'quantity_consistency': 60.0,
'quality_issue_rate': 20.0, # Should apply penalty
'short_delivery_rate': 25.0 # Should apply penalty
}
poor_score = predictor._calculate_reliability_score(poor_metrics)
assert poor_score < 70, f"Expected poor score <70, got {poor_score}"
@pytest.mark.asyncio
async def test_delay_probability_prediction(sample_order_history_poor_supplier):
"""Test delay probability prediction."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='test-supplier',
order_history=sample_order_history_poor_supplier
)
predictions = results['predictions']
# Should have delay probability
assert 'next_order_delay_probability' in predictions
assert 0 <= predictions['next_order_delay_probability'] <= 1.0
# Poor supplier should have higher delay probability
assert predictions['next_order_delay_probability'] > 0.3
# Should have confidence score
assert 'confidence' in predictions
assert 0 <= predictions['confidence'] <= 100
@pytest.mark.asyncio
async def test_risk_assessment(sample_order_history_poor_supplier):
"""Test procurement risk assessment."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='test-supplier',
order_history=sample_order_history_poor_supplier
)
risk_assessment = results['risk_assessment']
# Check structure
assert 'risk_level' in risk_assessment
assert 'risk_score' in risk_assessment
assert 'risk_factors' in risk_assessment
assert 'recommendation' in risk_assessment
# Risk level should be valid
assert risk_assessment['risk_level'] in ['low', 'medium', 'high', 'critical']
# Risk score should be 0-100
assert 0 <= risk_assessment['risk_score'] <= 100
# Should have risk factors for poor supplier
assert len(risk_assessment['risk_factors']) > 0
# Recommendation should be string
assert isinstance(risk_assessment['recommendation'], str)
assert len(risk_assessment['recommendation']) > 0
@pytest.mark.asyncio
async def test_insight_generation_low_reliability(sample_order_history_poor_supplier):
"""Test insight generation for low reliability supplier."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='poor-supplier',
order_history=sample_order_history_poor_supplier
)
insights = results['insights']
# Should generate insights
assert len(insights) > 0
# Check for low reliability alert
reliability_insights = [i for i in insights
if 'reliability' in i.get('title', '').lower()]
if reliability_insights:
insight = reliability_insights[0]
assert insight['type'] in ['alert', 'recommendation']
assert insight['priority'] in ['high', 'critical']
assert 'actionable' in insight
assert insight['actionable'] is True
assert 'recommendation_actions' in insight
assert len(insight['recommendation_actions']) > 0
@pytest.mark.asyncio
async def test_insight_generation_high_delay_risk(sample_order_history_poor_supplier):
"""Test insight generation for high delay probability."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='poor-supplier',
order_history=sample_order_history_poor_supplier
)
insights = results['insights']
# Check for delay risk prediction
delay_insights = [i for i in insights
if 'delay' in i.get('title', '').lower()]
if delay_insights:
insight = delay_insights[0]
assert 'confidence' in insight
assert 'metrics_json' in insight
assert 'recommendation_actions' in insight
@pytest.mark.asyncio
async def test_insight_generation_excellent_supplier(sample_order_history_good_supplier):
"""Test that excellent suppliers get positive insights."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='excellent-supplier',
order_history=sample_order_history_good_supplier
)
insights = results['insights']
# Should have positive insight for excellent performance
positive_insights = [i for i in insights
if 'excellent' in i.get('title', '').lower()]
if positive_insights:
insight = positive_insights[0]
assert insight['type'] == 'insight'
assert insight['impact_type'] == 'positive_performance'
def test_compare_suppliers():
"""Test supplier comparison functionality."""
predictor = SupplierPerformancePredictor()
# Mock analysis results
suppliers_analysis = [
{
'supplier_id': 'supplier-1',
'reliability_score': 95,
'risk_assessment': {'risk_level': 'low', 'risk_score': 10}
},
{
'supplier_id': 'supplier-2',
'reliability_score': 60,
'risk_assessment': {'risk_level': 'high', 'risk_score': 75}
},
{
'supplier_id': 'supplier-3',
'reliability_score': 80,
'risk_assessment': {'risk_level': 'medium', 'risk_score': 40}
}
]
comparison = predictor.compare_suppliers(suppliers_analysis)
# Check structure
assert 'suppliers_compared' in comparison
assert 'top_supplier' in comparison
assert 'top_supplier_score' in comparison
assert 'bottom_supplier' in comparison
assert 'bottom_supplier_score' in comparison
assert 'ranked_suppliers' in comparison
assert 'recommendations' in comparison
# Check ranking
assert comparison['suppliers_compared'] == 3
assert comparison['top_supplier'] == 'supplier-1'
assert comparison['top_supplier_score'] == 95
assert comparison['bottom_supplier'] == 'supplier-2'
assert comparison['bottom_supplier_score'] == 60
# Ranked suppliers should be in order
ranked = comparison['ranked_suppliers']
assert ranked[0]['supplier_id'] == 'supplier-1'
assert ranked[-1]['supplier_id'] == 'supplier-2'
# Should have recommendations
assert len(comparison['recommendations']) > 0
@pytest.mark.asyncio
async def test_insufficient_data_handling():
"""Test handling of insufficient order history."""
predictor = SupplierPerformancePredictor()
# Only 5 orders (less than min_orders=10)
small_history = pd.DataFrame([
{
'order_date': datetime(2024, 1, i),
'expected_delivery_date': datetime(2024, 1, i+3),
'actual_delivery_date': datetime(2024, 1, i+3),
'order_quantity': 100,
'received_quantity': 100,
'quality_issues': False,
'quality_score': 95.0,
'order_value': 500.0
}
for i in range(1, 6)
])
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='new-supplier',
order_history=small_history,
min_orders=10
)
# Should return insufficient data response
assert results['orders_analyzed'] == 0
assert results['reliability_score'] is None
assert results['risk_assessment']['risk_level'] == 'unknown'
assert 'Insufficient' in results['risk_assessment']['risk_factors'][0]
def test_get_supplier_reliability_score():
"""Test getting cached reliability scores."""
predictor = SupplierPerformancePredictor()
# Initially no score
assert predictor.get_supplier_reliability_score('supplier-1') is None
# Set a score
predictor.reliability_scores['supplier-1'] = 85
# Should retrieve it
assert predictor.get_supplier_reliability_score('supplier-1') == 85
@pytest.mark.asyncio
async def test_metrics_no_nan_values(sample_order_history_good_supplier):
"""Test that metrics never contain NaN values."""
predictor = SupplierPerformancePredictor()
results = await predictor.analyze_supplier_performance(
tenant_id='test-tenant',
supplier_id='test-supplier',
order_history=sample_order_history_good_supplier
)
metrics = results['metrics']
# Check no NaN values
for key, value in metrics.items():
if isinstance(value, float):
assert not np.isnan(value), f"Metric {key} is NaN"