hospital-management/appointments/tests/test_smart_scheduler.py
Marwan Alwali 263292f6be update
2025-11-04 00:50:06 +03:00

494 lines
17 KiB
Python

"""
Unit tests for Smart Scheduling Engine (Phase 10)
Tests for SmartScheduler class and scoring algorithms
"""
import pytest
from datetime import datetime, timedelta, time
from django.test import TestCase
from django.utils import timezone
from django.contrib.auth import get_user_model
from appointments.scheduling.smart_scheduler import SmartScheduler
from appointments.models import (
AppointmentRequest, SchedulingPreference, AppointmentPriorityRule,
SchedulingMetrics, AppointmentTemplate
)
from patients.models import PatientProfile
from core.models import Tenant
User = get_user_model()
class SmartSchedulerTestCase(TestCase):
"""Test cases for SmartScheduler class"""
def setUp(self):
"""Set up test data"""
# Create tenant
self.tenant = Tenant.objects.create(
name="Test Hospital",
display_name="Test Hospital",
address_line1="123 Test St",
city="Test City",
state="Test State",
postal_code="12345",
phone_number="+966501234567",
email="test@hospital.com"
)
# Create provider
self.provider = User.objects.create_user(
email="provider@test.com",
first_name="Test",
last_name="Provider",
tenant=self.tenant,
role="PHYSICIAN"
)
# Create patient
self.patient = PatientProfile.objects.create(
tenant=self.tenant,
first_name="Test",
last_name="Patient",
date_of_birth=datetime(1990, 1, 1).date(),
gender="M",
mrn="TEST001"
)
# Create appointment template
self.appointment_type = AppointmentTemplate.objects.create(
tenant=self.tenant,
name="General Consultation",
appointment_type="CONSULTATION",
specialty="FAMILY_MEDICINE",
duration_minutes=30,
is_active=True
)
# Create scheduling preference for patient
self.preference = SchedulingPreference.objects.create(
tenant=self.tenant,
patient=self.patient,
preferred_days_of_week=[1, 2, 3], # Mon, Tue, Wed
preferred_time_of_day="MORNING",
no_show_rate=5.0,
completion_rate=95.0
)
# Initialize scheduler
self.scheduler = SmartScheduler(self.tenant)
def test_scheduler_initialization(self):
"""Test SmartScheduler initialization"""
self.assertIsNotNone(self.scheduler)
self.assertEqual(self.scheduler.tenant, self.tenant)
def test_calculate_provider_availability_score(self):
"""Test provider availability scoring"""
# Create a slot
slot_datetime = timezone.now() + timedelta(days=1, hours=10)
# Test with available slot
score = self.scheduler._calculate_provider_availability_score(
self.provider,
slot_datetime,
30
)
# Score should be between 0 and 1
self.assertGreaterEqual(score, 0.0)
self.assertLessEqual(score, 1.0)
def test_calculate_patient_priority_score(self):
"""Test patient priority scoring"""
# Create priority rule
priority_rule = AppointmentPriorityRule.objects.create(
tenant=self.tenant,
name="Urgent Care",
priority_level="URGENT",
icd10_codes=["R07.9"], # Chest pain
urgency_multiplier=2.0,
max_wait_days=7
)
# Test priority scoring
score = self.scheduler._calculate_patient_priority_score(
self.patient,
self.appointment_type,
icd10_codes=["R07.9"]
)
# Score should be elevated due to priority rule
self.assertGreater(score, 0.5)
def test_calculate_no_show_risk_score(self):
"""Test no-show risk calculation"""
# Test with patient who has low no-show rate
score = self.scheduler._calculate_no_show_risk_score(
self.patient,
timezone.now() + timedelta(days=1, hours=10)
)
# Score should be high (low risk)
self.assertGreater(score, 0.8)
# Update preference with high no-show rate
self.preference.no_show_rate = 50.0
self.preference.save()
# Recalculate
score = self.scheduler._calculate_no_show_risk_score(
self.patient,
timezone.now() + timedelta(days=1, hours=10)
)
# Score should be lower (higher risk)
self.assertLess(score, 0.6)
def test_calculate_geographic_score(self):
"""Test geographic proximity scoring"""
# Set patient location
self.preference.patient_latitude = 24.7136
self.preference.patient_longitude = 46.6753
self.preference.save()
# Test with nearby location
score = self.scheduler._calculate_geographic_score(
self.patient,
24.7200, # Nearby latitude
46.6800 # Nearby longitude
)
# Score should be high (close proximity)
self.assertGreater(score, 0.7)
def test_calculate_patient_preference_score(self):
"""Test patient preference scoring"""
# Test with preferred day and time
preferred_datetime = timezone.now().replace(
hour=9, minute=0, second=0, microsecond=0
)
# Make it a Monday
days_ahead = (0 - preferred_datetime.weekday()) % 7
if days_ahead == 0:
days_ahead = 7
preferred_datetime += timedelta(days=days_ahead)
score = self.scheduler._calculate_patient_preference_score(
self.patient,
preferred_datetime
)
# Score should be high (matches preferences)
self.assertGreater(score, 0.7)
# Test with non-preferred day (Sunday)
non_preferred_datetime = preferred_datetime + timedelta(days=6)
score = self.scheduler._calculate_patient_preference_score(
self.patient,
non_preferred_datetime
)
# Score should be lower
self.assertLess(score, 0.5)
def test_calculate_slot_score(self):
"""Test overall slot scoring"""
slot_datetime = timezone.now() + timedelta(days=1, hours=10)
score = self.scheduler._calculate_slot_score(
patient=self.patient,
provider=self.provider,
slot_datetime=slot_datetime,
appointment_type=self.appointment_type,
duration_minutes=30
)
# Score should be a dictionary with all components
self.assertIn('total_score', score)
self.assertIn('provider_availability', score)
self.assertIn('patient_priority', score)
self.assertIn('no_show_risk', score)
self.assertIn('geographic_proximity', score)
self.assertIn('patient_preference', score)
# Total score should be between 0 and 100
self.assertGreaterEqual(score['total_score'], 0)
self.assertLessEqual(score['total_score'], 100)
def test_find_optimal_slots_basic(self):
"""Test basic optimal slot finding"""
# Create date range
start_date = timezone.now() + timedelta(days=1)
date_range = [start_date + timedelta(days=i) for i in range(7)]
# Find slots
slots = self.scheduler.find_optimal_slots(
patient=self.patient,
provider=self.provider,
appointment_type=self.appointment_type,
preferred_dates=date_range,
duration_minutes=30,
max_results=5
)
# Should return a list
self.assertIsInstance(slots, list)
# Each slot should have required fields
if slots:
slot = slots[0]
self.assertIn('datetime', slot)
self.assertIn('score', slot)
self.assertIn('score_breakdown', slot)
def test_find_optimal_slots_with_constraints(self):
"""Test slot finding with time constraints"""
start_date = timezone.now() + timedelta(days=1)
date_range = [start_date + timedelta(days=i) for i in range(3)]
# Find slots with morning preference
slots = self.scheduler.find_optimal_slots(
patient=self.patient,
provider=self.provider,
appointment_type=self.appointment_type,
preferred_dates=date_range,
duration_minutes=30,
time_preferences=['MORNING'],
max_results=10
)
# All slots should be in morning hours (before 12:00)
for slot in slots:
slot_time = slot['datetime'].time()
self.assertLess(slot_time.hour, 12)
def test_scoring_weights(self):
"""Test that scoring weights sum to 1.0"""
weights = self.scheduler.scoring_weights
total_weight = sum(weights.values())
# Weights should sum to 1.0 (with small tolerance for floating point)
self.assertAlmostEqual(total_weight, 1.0, places=2)
def test_no_show_prediction_time_factors(self):
"""Test no-show prediction with time-based factors"""
# Early morning appointment (should have higher risk)
early_morning = timezone.now().replace(hour=7, minute=0)
score_early = self.scheduler._calculate_no_show_risk_score(
self.patient,
early_morning
)
# Mid-morning appointment (should have lower risk)
mid_morning = timezone.now().replace(hour=10, minute=0)
score_mid = self.scheduler._calculate_no_show_risk_score(
self.patient,
mid_morning
)
# Early morning should have higher risk (lower score)
self.assertLess(score_early, score_mid)
def test_priority_routing(self):
"""Test priority-based routing for urgent appointments"""
# Create urgent priority rule
urgent_rule = AppointmentPriorityRule.objects.create(
tenant=self.tenant,
name="Emergency",
priority_level="EMERGENCY",
icd10_codes=["I21.9"], # Acute MI
urgency_multiplier=5.0,
max_wait_days=1
)
# Calculate priority score
score = self.scheduler._calculate_patient_priority_score(
self.patient,
self.appointment_type,
icd10_codes=["I21.9"]
)
# Should have very high priority score
self.assertGreater(score, 0.9)
class ScoringAlgorithmTestCase(TestCase):
"""Test cases for individual scoring algorithms"""
def setUp(self):
"""Set up test data"""
self.tenant = Tenant.objects.create(
name="Test Hospital",
display_name="Test Hospital",
address_line1="123 Test St",
city="Test City",
state="Test State",
postal_code="12345",
phone_number="+966501234567",
email="test@hospital.com"
)
self.scheduler = SmartScheduler(self.tenant)
def test_time_of_day_scoring(self):
"""Test time of day preference scoring"""
# Morning preference
morning_time = time(9, 0)
afternoon_time = time(14, 0)
evening_time = time(18, 0)
# Test morning preference
score_morning = self.scheduler._score_time_of_day(morning_time, "MORNING")
score_afternoon = self.scheduler._score_time_of_day(afternoon_time, "MORNING")
# Morning time should score higher for morning preference
self.assertGreater(score_morning, score_afternoon)
def test_day_of_week_scoring(self):
"""Test day of week preference scoring"""
# Preferred days: Monday, Wednesday, Friday (0, 2, 4)
preferred_days = [0, 2, 4]
# Monday (preferred)
score_preferred = self.scheduler._score_day_of_week(0, preferred_days)
# Tuesday (not preferred)
score_not_preferred = self.scheduler._score_day_of_week(1, preferred_days)
# Preferred day should score higher
self.assertGreater(score_preferred, score_not_preferred)
def test_distance_scoring(self):
"""Test geographic distance scoring"""
# Test various distances
score_0km = self.scheduler._score_distance(0)
score_5km = self.scheduler._score_distance(5)
score_20km = self.scheduler._score_distance(20)
score_50km = self.scheduler._score_distance(50)
# Closer distances should score higher
self.assertGreater(score_0km, score_5km)
self.assertGreater(score_5km, score_20km)
self.assertGreater(score_20km, score_50km)
def test_no_show_rate_scoring(self):
"""Test no-show rate scoring"""
# Low no-show rate (good)
score_low = self.scheduler._score_no_show_rate(5.0)
# High no-show rate (bad)
score_high = self.scheduler._score_no_show_rate(50.0)
# Lower no-show rate should score higher
self.assertGreater(score_low, score_high)
@pytest.mark.django_db
class SmartSchedulerPerformanceTest(TestCase):
"""Performance tests for SmartScheduler"""
def setUp(self):
"""Set up test data"""
self.tenant = Tenant.objects.create(
name="Test Hospital",
display_name="Test Hospital",
address_line1="123 Test St",
city="Test City",
state="Test State",
postal_code="12345",
phone_number="+966501234567",
email="test@hospital.com"
)
self.scheduler = SmartScheduler(self.tenant)
# Create multiple providers
self.providers = []
for i in range(5):
provider = User.objects.create_user(
email=f"provider{i}@test.com",
first_name=f"Provider{i}",
last_name="Test",
tenant=self.tenant,
role="PHYSICIAN"
)
self.providers.append(provider)
# Create patient
self.patient = PatientProfile.objects.create(
tenant=self.tenant,
first_name="Test",
last_name="Patient",
date_of_birth=datetime(1990, 1, 1).date(),
gender="M",
mrn="TEST001"
)
# Create appointment template
self.appointment_type = AppointmentTemplate.objects.create(
tenant=self.tenant,
name="General Consultation",
appointment_type="CONSULTATION",
specialty="FAMILY_MEDICINE",
duration_minutes=30,
is_active=True
)
def test_slot_finding_performance(self):
"""Test performance of slot finding for 30-day range"""
import time
# Create 30-day date range
start_date = timezone.now() + timedelta(days=1)
date_range = [start_date + timedelta(days=i) for i in range(30)]
# Measure time
start_time = time.time()
slots = self.scheduler.find_optimal_slots(
patient=self.patient,
provider=self.providers[0],
appointment_type=self.appointment_type,
preferred_dates=date_range,
duration_minutes=30,
max_results=20
)
end_time = time.time()
execution_time = end_time - start_time
# Should complete in less than 2 seconds
self.assertLess(execution_time, 2.0)
print(f"\nSlot finding for 30 days completed in {execution_time:.3f} seconds")
def test_scoring_performance(self):
"""Test performance of scoring algorithm"""
import time
slot_datetime = timezone.now() + timedelta(days=1, hours=10)
# Measure time for 100 score calculations
start_time = time.time()
for _ in range(100):
self.scheduler._calculate_slot_score(
patient=self.patient,
provider=self.providers[0],
slot_datetime=slot_datetime,
appointment_type=self.appointment_type,
duration_minutes=30
)
end_time = time.time()
execution_time = end_time - start_time
# 100 calculations should complete in less than 1 second
self.assertLess(execution_time, 1.0)
print(f"\n100 score calculations completed in {execution_time:.3f} seconds")