459 lines
15 KiB
Python
459 lines
15 KiB
Python
"""
|
|
Test script for source-based SLA functionality.
|
|
|
|
Tests:
|
|
1. SLA config retrieval for different sources
|
|
2. Reminder timing calculation (hours after creation)
|
|
3. Escalation timing
|
|
4. SLA due date calculation
|
|
5. Fallback mechanism
|
|
"""
|
|
import os
|
|
import sys
|
|
import django
|
|
|
|
# Setup Django
|
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'PX360.settings')
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
django.setup()
|
|
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
from apps.organizations.models import Hospital
|
|
from apps.px_sources.models import PXSource
|
|
from apps.complaints.models import Complaint, ComplaintSLAConfig
|
|
from apps.accounts.models import User
|
|
|
|
|
|
def print_section(title):
|
|
"""Print a formatted section header"""
|
|
print("\n" + "="*70)
|
|
print(f" {title}")
|
|
print("="*70)
|
|
|
|
|
|
def print_test(test_name, result, details=""):
|
|
"""Print test result"""
|
|
status = "✓ PASS" if result else "✗ FAIL"
|
|
color = "\033[92m" if result else "\033[91m"
|
|
reset = "\033[0m"
|
|
print(f"{color}{status}{reset} - {test_name}")
|
|
if details:
|
|
print(f" {details}")
|
|
|
|
|
|
def test_source_configs():
|
|
"""Test that source-based configs exist"""
|
|
print_section("Test 1: Source-Based SLA Configs")
|
|
|
|
hospital = Hospital.objects.first()
|
|
if not hospital:
|
|
print("✗ FAIL: No hospitals found")
|
|
return False
|
|
|
|
print(f"Hospital: {hospital.name}")
|
|
|
|
sources_to_test = [
|
|
('Ministry of Health', 24, 12, 30, 24),
|
|
('Council of Cooperative Health Insurance', 48, 24, 60, 48),
|
|
('Patient', 72, 24, 72, 72),
|
|
('Family Member', 72, 24, 72, 72),
|
|
]
|
|
|
|
all_passed = True
|
|
for source_name, expected_sla, expected_first_rem, expected_second_rem, expected_escalation in sources_to_test:
|
|
source = PXSource.objects.filter(name_en=source_name).first()
|
|
if not source:
|
|
print_test(f"Source '{source_name}' exists", False, "Source not found")
|
|
all_passed = False
|
|
continue
|
|
|
|
config = ComplaintSLAConfig.objects.filter(
|
|
hospital=hospital,
|
|
source=source,
|
|
is_active=True
|
|
).first()
|
|
|
|
if not config:
|
|
print_test(f"SLA config for '{source_name}' exists", False, "Config not found")
|
|
all_passed = False
|
|
continue
|
|
|
|
sla_ok = config.sla_hours == expected_sla
|
|
first_rem_ok = config.first_reminder_hours_after == expected_first_rem
|
|
second_rem_ok = config.second_reminder_hours_after == expected_second_rem
|
|
escalation_ok = config.escalation_hours_after == expected_escalation
|
|
|
|
details = f"SLA: {config.sla_hours}h, 1st Rem: {config.first_reminder_hours_after}h, 2nd Rem: {config.second_reminder_hours_after}h, Escalation: {config.escalation_hours_after}h"
|
|
|
|
if sla_ok and first_rem_ok and second_rem_ok and escalation_ok:
|
|
print_test(f"SLA config for '{source_name}' has correct values", True, details)
|
|
else:
|
|
print_test(f"SLA config for '{source_name}' has correct values", False, details)
|
|
all_passed = False
|
|
|
|
return all_passed
|
|
|
|
|
|
def test_sla_config_methods():
|
|
"""Test helper methods on ComplaintSLAConfig"""
|
|
print_section("Test 2: SLA Config Helper Methods")
|
|
|
|
hospital = Hospital.objects.first()
|
|
moh_source = PXSource.objects.filter(name_en='Ministry of Health').first()
|
|
|
|
if not hospital or not moh_source:
|
|
print("✗ FAIL: Missing required data")
|
|
return False
|
|
|
|
config = ComplaintSLAConfig.objects.filter(
|
|
hospital=hospital,
|
|
source=moh_source
|
|
).first()
|
|
|
|
if not config:
|
|
print_test("Source-based config found", False, "MOH config not found")
|
|
return False
|
|
|
|
# Test get_first_reminder_hours_after()
|
|
expected_first = 12
|
|
actual_first = config.get_first_reminder_hours_after()
|
|
print_test(
|
|
f"get_first_reminder_hours_after() returns {expected_first}h",
|
|
actual_first == expected_first,
|
|
f"Expected: {expected_first}, Got: {actual_first}"
|
|
)
|
|
|
|
# Test get_second_reminder_hours_after()
|
|
expected_second = 30
|
|
actual_second = config.get_second_reminder_hours_after()
|
|
print_test(
|
|
f"get_second_reminder_hours_after() returns {expected_second}h",
|
|
actual_second == expected_second,
|
|
f"Expected: {expected_second}, Got: {actual_second}"
|
|
)
|
|
|
|
# Test get_escalation_hours_after()
|
|
expected_escalation = 24
|
|
actual_escalation = config.get_escalation_hours_after()
|
|
print_test(
|
|
f"get_escalation_hours_after() returns {expected_escalation}h",
|
|
actual_escalation == expected_escalation,
|
|
f"Expected: {expected_escalation}, Got: {actual_escalation}"
|
|
)
|
|
|
|
# Test legacy config (no source)
|
|
legacy_config = ComplaintSLAConfig.objects.filter(
|
|
hospital=hospital,
|
|
source__isnull=True
|
|
).first()
|
|
|
|
if legacy_config and legacy_config.reminder_hours_before:
|
|
legacy_first = legacy_config.get_first_reminder_hours_after()
|
|
print_test(
|
|
"Legacy config (no source) returns None for source-based timing",
|
|
legacy_first is None,
|
|
f"Expected: None, Got: {legacy_first}"
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def test_complaint_get_sla_config():
|
|
"""Test Complaint.get_sla_config() method"""
|
|
print_section("Test 3: Complaint get_sla_config() Method")
|
|
|
|
hospital = Hospital.objects.first()
|
|
moh_source = PXSource.objects.filter(name_en='Ministry of Health').first()
|
|
patient_source = PXSource.objects.filter(name_en='Patient').first()
|
|
|
|
if not hospital or not moh_source or not patient_source:
|
|
print("✗ FAIL: Missing required data")
|
|
return False
|
|
|
|
# Create test complaints (without saving to database)
|
|
moh_complaint = Complaint(
|
|
hospital=hospital,
|
|
source=moh_source,
|
|
severity='high',
|
|
priority='high',
|
|
status='open'
|
|
)
|
|
|
|
patient_complaint = Complaint(
|
|
hospital=hospital,
|
|
source=patient_source,
|
|
severity='medium',
|
|
priority='medium',
|
|
status='open'
|
|
)
|
|
|
|
# Test MOH complaint
|
|
moh_config = moh_complaint.get_sla_config()
|
|
print_test(
|
|
"MOH complaint returns source-based config",
|
|
moh_config is not None and moh_config.source == moh_source,
|
|
f"Config source: {moh_config.source if moh_config else 'None'}"
|
|
)
|
|
|
|
if moh_config:
|
|
print_test(
|
|
"MOH config has correct SLA hours (24h)",
|
|
moh_config.sla_hours == 24,
|
|
f"SLA hours: {moh_config.sla_hours}"
|
|
)
|
|
|
|
# Test Patient complaint
|
|
patient_config = patient_complaint.get_sla_config()
|
|
print_test(
|
|
"Patient complaint returns source-based config",
|
|
patient_config is not None and patient_config.source == patient_source,
|
|
f"Config source: {patient_config.source if patient_config else 'None'}"
|
|
)
|
|
|
|
if patient_config:
|
|
print_test(
|
|
"Patient config has correct SLA hours (72h)",
|
|
patient_config.sla_hours == 72,
|
|
f"SLA hours: {patient_config.sla_hours}"
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def test_sla_due_date_calculation():
|
|
"""Test SLA due date calculation for different sources"""
|
|
print_section("Test 4: SLA Due Date Calculation")
|
|
|
|
hospital = Hospital.objects.first()
|
|
moh_source = PXSource.objects.filter(name_en='Ministry of Health').first()
|
|
cchi_source = PXSource.objects.filter(name_en='Council of Cooperative Health Insurance').first()
|
|
patient_source = PXSource.objects.filter(name_en='Patient').first()
|
|
|
|
if not hospital:
|
|
print("✗ FAIL: No hospital found")
|
|
return False
|
|
|
|
base_time = timezone.now()
|
|
|
|
# MOH: 24 hours
|
|
if moh_source:
|
|
moh_complaint = Complaint(
|
|
hospital=hospital,
|
|
source=moh_source,
|
|
created_at=base_time
|
|
)
|
|
moh_complaint.calculate_sla_due_date()
|
|
moh_due_at = moh_complaint.due_at
|
|
|
|
if moh_due_at:
|
|
moh_sla_hours = (moh_due_at - base_time).total_seconds() / 3600
|
|
print_test(
|
|
"MOH complaint SLA is 24 hours",
|
|
23 < moh_sla_hours <= 24,
|
|
f"Calculated SLA: {moh_sla_hours:.1f} hours"
|
|
)
|
|
|
|
# CCHI: 48 hours
|
|
if cchi_source:
|
|
cchi_complaint = Complaint(
|
|
hospital=hospital,
|
|
source=cchi_source,
|
|
created_at=base_time
|
|
)
|
|
cchi_complaint.calculate_sla_due_date()
|
|
cchi_due_at = cchi_complaint.due_at
|
|
|
|
if cchi_due_at:
|
|
cchi_sla_hours = (cchi_due_at - base_time).total_seconds() / 3600
|
|
print_test(
|
|
"CCHI complaint SLA is 48 hours",
|
|
47 < cchi_sla_hours <= 48,
|
|
f"Calculated SLA: {cchi_sla_hours:.1f} hours"
|
|
)
|
|
|
|
# Patient: 72 hours
|
|
if patient_source:
|
|
patient_complaint = Complaint(
|
|
hospital=hospital,
|
|
source=patient_source,
|
|
created_at=base_time
|
|
)
|
|
patient_complaint.calculate_sla_due_date()
|
|
patient_due_at = patient_complaint.due_at
|
|
|
|
if patient_due_at:
|
|
patient_sla_hours = (patient_due_at - base_time).total_seconds() / 3600
|
|
print_test(
|
|
"Patient complaint SLA is 72 hours",
|
|
71 < patient_sla_hours <= 72,
|
|
f"Calculated SLA: {patient_sla_hours:.1f} hours"
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def test_reminder_timing():
|
|
"""Test reminder timing (hours after creation)"""
|
|
print_section("Test 5: Reminder Timing (Hours After Creation)")
|
|
|
|
hospital = Hospital.objects.first()
|
|
moh_source = PXSource.objects.filter(name_en='Ministry of Health').first()
|
|
|
|
if not hospital or not moh_source:
|
|
print("✗ FAIL: Missing required data")
|
|
return False
|
|
|
|
config = ComplaintSLAConfig.objects.filter(
|
|
hospital=hospital,
|
|
source=moh_source
|
|
).first()
|
|
|
|
if not config:
|
|
print_test("MOH config exists", False, "Config not found")
|
|
return False
|
|
|
|
# MOH: 1st reminder at 12h, 2nd at 30h (12 + 18)
|
|
base_time = timezone.now()
|
|
|
|
# Test 1st reminder timing
|
|
first_reminder_hours = config.get_first_reminder_hours_after()
|
|
first_reminder_at = base_time + timedelta(hours=first_reminder_hours) if first_reminder_hours else None
|
|
|
|
print_test(
|
|
"MOH 1st reminder is scheduled at 12 hours after creation",
|
|
first_reminder_hours == 12,
|
|
f"Hours after creation: {first_reminder_hours}"
|
|
)
|
|
|
|
if first_reminder_at:
|
|
print_test(
|
|
"1st reminder datetime is correctly calculated",
|
|
True,
|
|
f"Created: {base_time.strftime('%H:%M')}, Reminder: {first_reminder_at.strftime('%H:%M')}"
|
|
)
|
|
|
|
# Test 2nd reminder timing
|
|
second_reminder_hours = config.get_second_reminder_hours_after()
|
|
second_reminder_at = base_time + timedelta(hours=second_reminder_hours) if second_reminder_hours else None
|
|
|
|
print_test(
|
|
"MOH 2nd reminder is scheduled at 30 hours after creation",
|
|
second_reminder_hours == 30,
|
|
f"Hours after creation: {second_reminder_hours}"
|
|
)
|
|
|
|
if second_reminder_at:
|
|
print_test(
|
|
"2nd reminder datetime is correctly calculated",
|
|
True,
|
|
f"Created: {base_time.strftime('%H:%M')}, Reminder: {second_reminder_at.strftime('%H:%M')}"
|
|
)
|
|
|
|
# Test escalation timing
|
|
escalation_hours = config.get_escalation_hours_after()
|
|
escalation_at = base_time + timedelta(hours=escalation_hours) if escalation_hours else None
|
|
|
|
print_test(
|
|
"MOH escalation is scheduled at 24 hours after creation",
|
|
escalation_hours == 24,
|
|
f"Hours after creation: {escalation_hours}"
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def test_fallback_mechanism():
|
|
"""Test fallback mechanism (source → severity/priority → settings)"""
|
|
print_section("Test 6: Fallback Mechanism")
|
|
|
|
hospital = Hospital.objects.first()
|
|
|
|
if not hospital:
|
|
print("✗ FAIL: No hospital found")
|
|
return False
|
|
|
|
# Create a complaint with a source that has no config
|
|
call_center_source = PXSource.objects.filter(name_en='Call Center').first()
|
|
|
|
if call_center_source:
|
|
complaint = Complaint(
|
|
hospital=hospital,
|
|
source=call_center_source,
|
|
severity='high',
|
|
priority='high',
|
|
status='open'
|
|
)
|
|
|
|
config = complaint.get_sla_config()
|
|
|
|
# Should fall back to severity/priority-based config
|
|
if config:
|
|
has_config = True
|
|
is_source_based = config.source == call_center_source
|
|
is_severity_based = config.severity == 'high' and config.priority == 'high'
|
|
|
|
print_test(
|
|
"Fallback: Returns a config when source config doesn't exist",
|
|
has_config,
|
|
f"Config found: {has_config}"
|
|
)
|
|
|
|
print_test(
|
|
"Fallback: Returns severity/priority-based config",
|
|
not is_source_based,
|
|
f"Source-based: {is_source_based}, Severity-based: {is_severity_based}"
|
|
)
|
|
else:
|
|
print_test(
|
|
"Fallback: Returns config even without source config",
|
|
False,
|
|
"No config found"
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Run all tests"""
|
|
print("\n" + "="*70)
|
|
print(" SOURCE-BASED SLA FUNCTIONALITY TESTS")
|
|
print("="*70)
|
|
|
|
results = []
|
|
|
|
# Run all tests
|
|
results.append(("Source-Based SLA Configs", test_source_configs()))
|
|
results.append(("SLA Config Helper Methods", test_sla_config_methods()))
|
|
results.append(("Complaint get_sla_config()", test_complaint_get_sla_config()))
|
|
results.append(("SLA Due Date Calculation", test_sla_due_date_calculation()))
|
|
results.append(("Reminder Timing", test_reminder_timing()))
|
|
results.append(("Fallback Mechanism", test_fallback_mechanism()))
|
|
|
|
# Print summary
|
|
print_section("TEST SUMMARY")
|
|
|
|
total_tests = len(results)
|
|
passed_tests = sum(1 for _, result in results if result)
|
|
failed_tests = total_tests - passed_tests
|
|
|
|
for test_name, result in results:
|
|
status = "✓ PASS" if result else "✗ FAIL"
|
|
print(f"{status} - {test_name}")
|
|
|
|
print("\n" + "="*70)
|
|
print(f" Total: {total_tests} | Passed: {passed_tests} | Failed: {failed_tests}")
|
|
print("="*70)
|
|
|
|
if failed_tests == 0:
|
|
print("\n✓ All tests passed!")
|
|
else:
|
|
print(f"\n✗ {failed_tests} test(s) failed")
|
|
|
|
return failed_tests == 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
success = main()
|
|
sys.exit(0 if success else 1) |