540 lines
18 KiB
Python
540 lines
18 KiB
Python
"""
|
|
Test script for Explanation Request SLA System
|
|
|
|
This script tests the complete explanation request SLA functionality including:
|
|
1. Explanation SLA configuration
|
|
2. SLA deadline calculation when requests are created
|
|
3. Reminder email sending
|
|
4. Escalation to manager on deadline breach
|
|
5. Email template rendering
|
|
|
|
Run this script with: python test_explanation_sla.py
|
|
"""
|
|
|
|
import os
|
|
import django
|
|
import sys
|
|
import secrets
|
|
from datetime import timedelta, datetime
|
|
from django.utils import timezone
|
|
|
|
# Setup Django
|
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
|
|
django.setup()
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.core import mail
|
|
from django.template.loader import render_to_string
|
|
from apps.complaints.models import (
|
|
Complaint,
|
|
ComplaintExplanation,
|
|
ExplanationSLAConfig,
|
|
ComplaintUpdate
|
|
)
|
|
from apps.organizations.models import Staff, Hospital, Department
|
|
from apps.px_sources.models import PXSource
|
|
from apps.complaints.tasks import (
|
|
check_overdue_explanation_requests,
|
|
send_explanation_reminders
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def print_section(title):
|
|
"""Print a formatted section header"""
|
|
print("\n" + "="*80)
|
|
print(f" {title}")
|
|
print("="*80 + "\n")
|
|
|
|
|
|
def create_test_data():
|
|
"""Create test data for SLA testing"""
|
|
print_section("Creating Test Data")
|
|
|
|
# Create test users
|
|
admin_user, _ = User.objects.get_or_create(
|
|
username='admin',
|
|
defaults={
|
|
'email': 'admin@px360.com',
|
|
'first_name': 'Admin',
|
|
'last_name': 'User',
|
|
'is_staff': True,
|
|
'is_superuser': True
|
|
}
|
|
)
|
|
|
|
# Create or get source
|
|
source, _ = PXSource.objects.get_or_create(
|
|
name_en='Web Portal',
|
|
defaults={'name_ar': 'بوابة الويب', 'is_active': True}
|
|
)
|
|
|
|
# Get or create hospital first
|
|
from apps.organizations.models import Hospital
|
|
hospital, _ = Hospital.objects.get_or_create(
|
|
name='Test Hospital',
|
|
defaults={'name_ar': 'مستشفى تجريبي'}
|
|
)
|
|
|
|
# Create department with unique name for testing
|
|
department, _ = Department.objects.get_or_create(
|
|
name='Test Cardiology',
|
|
defaults={'name_ar': 'أمراض القلب تجريبي', 'hospital': hospital}
|
|
)
|
|
|
|
# Create manager (hospital is required)
|
|
manager, _ = Staff.objects.get_or_create(
|
|
employee_id='MGR001',
|
|
defaults={
|
|
'first_name': 'Ahmed',
|
|
'first_name_ar': 'أحمد',
|
|
'last_name': 'Manager',
|
|
'last_name_ar': 'مدير',
|
|
'email': 'manager@hospital.com',
|
|
'phone': '+966501234567',
|
|
'department': department,
|
|
'job_title': 'Department Head',
|
|
'staff_type': 'admin',
|
|
'hospital': hospital,
|
|
'status': 'active'
|
|
}
|
|
)
|
|
|
|
# Create staff member (hospital is required)
|
|
staff_member, _ = Staff.objects.get_or_create(
|
|
employee_id='STF001',
|
|
defaults={
|
|
'first_name': 'Mohammed',
|
|
'first_name_ar': 'محمد',
|
|
'last_name': 'Al-Otaibi',
|
|
'last_name_ar': 'العتيبي',
|
|
'email': 'mohammed@hospital.com',
|
|
'phone': '+966501234568',
|
|
'department': department,
|
|
'job_title': 'Senior Doctor',
|
|
'staff_type': 'physician',
|
|
'report_to': manager,
|
|
'hospital': hospital,
|
|
'status': 'active'
|
|
}
|
|
)
|
|
|
|
# Update department with hospital
|
|
department.hospital = hospital
|
|
department.save()
|
|
|
|
# Create a complaint
|
|
complaint = Complaint.objects.create(
|
|
source=source,
|
|
title='Wait time too long in emergency room',
|
|
description='Patient waited for 3 hours before being seen by a doctor.',
|
|
severity='high',
|
|
status='open',
|
|
hospital=hospital,
|
|
department=department
|
|
)
|
|
|
|
print(f"✓ Created test data:")
|
|
print(f" - Admin user: {admin_user.email}")
|
|
print(f" - Department: {department.name}")
|
|
print(f" - Manager: {manager.get_full_name()} ({manager.email})")
|
|
print(f" - Staff member: {staff_member.get_full_name()} ({staff_member.email})")
|
|
print(f" - Complaint: #{complaint.id} - {complaint.title}")
|
|
|
|
return {
|
|
'admin': admin_user,
|
|
'manager': manager,
|
|
'staff': staff_member,
|
|
'complaint': complaint,
|
|
'source': source,
|
|
'department': department
|
|
}
|
|
|
|
|
|
def test_sla_configuration():
|
|
"""Test Explanation SLA Configuration"""
|
|
print_section("Test 1: Explanation SLA Configuration")
|
|
|
|
# Get or create SLA configuration
|
|
from apps.organizations.models import Hospital
|
|
hospital = Hospital.objects.first()
|
|
|
|
sla_config, created = ExplanationSLAConfig.objects.get_or_create(
|
|
hospital=hospital,
|
|
defaults={
|
|
'response_hours': 24,
|
|
'reminder_hours_before': 12,
|
|
'auto_escalate_enabled': True,
|
|
'escalation_hours_overdue': 0,
|
|
'is_active': True
|
|
}
|
|
)
|
|
|
|
if not created:
|
|
sla_config.response_hours = 24
|
|
sla_config.reminder_hours_before = 12
|
|
sla_config.auto_escalate_enabled = True
|
|
sla_config.escalation_hours_overdue = 0
|
|
sla_config.is_active = True
|
|
sla_config.save()
|
|
|
|
print(f"✓ SLA Configuration: {'Created' if created else 'Updated'}")
|
|
print(f" - Response deadline: {sla_config.response_hours} hours")
|
|
print(f" - Reminder: {sla_config.reminder_hours_before} hours before deadline")
|
|
print(f" - Auto escalate: {sla_config.auto_escalate_enabled}")
|
|
print(f" - Escalation delay: {sla_config.escalation_hours_overdue} hours")
|
|
|
|
return sla_config
|
|
|
|
|
|
def test_explanation_request_creation(data, sla_config):
|
|
"""Test creating explanation request and SLA deadline calculation"""
|
|
print_section("Test 2: Explanation Request Creation & SLA Deadline")
|
|
|
|
complaint = data['complaint']
|
|
staff_member = data['staff']
|
|
admin = data['admin']
|
|
|
|
# Create an explanation request with unique token and email_sent_at
|
|
unique_token = secrets.token_urlsafe(64)
|
|
explanation = ComplaintExplanation.objects.create(
|
|
complaint=complaint,
|
|
staff=staff_member,
|
|
requested_by=admin,
|
|
is_used=False,
|
|
token=unique_token,
|
|
sla_due_at=timezone.now() + timedelta(hours=sla_config.response_hours),
|
|
email_sent_at=timezone.now(), # Set email_sent_at so tasks can process it
|
|
is_overdue=False
|
|
)
|
|
|
|
# Check SLA deadline was set correctly
|
|
expected_deadline = timezone.now() + timedelta(hours=sla_config.response_hours)
|
|
time_diff = (explanation.sla_due_at - expected_deadline).total_seconds()
|
|
|
|
print(f"✓ Explanation request created")
|
|
print(f" - Request ID: {explanation.id}")
|
|
print(f" - Staff: {explanation.staff.get_full_name()}")
|
|
print(f" - Requested by: {explanation.requested_by.get_full_name() if explanation.requested_by else 'N/A'}")
|
|
print(f" - Status: {'Pending' if not explanation.is_used else 'Submitted'}")
|
|
print(f" - SLA deadline: {explanation.sla_due_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print(f" - Deadline accuracy: {abs(time_diff)} seconds within expected")
|
|
|
|
# Verify the deadline is approximately correct (within 1 minute)
|
|
assert abs(time_diff) < 60, "SLA deadline not set correctly"
|
|
print(f"✓ SLA deadline calculated correctly")
|
|
|
|
return explanation
|
|
|
|
|
|
def test_email_template_rendering(data, explanation, sla_config):
|
|
"""Test email template rendering"""
|
|
print_section("Test 3: Email Template Rendering")
|
|
|
|
complaint = data['complaint']
|
|
staff_member = data['staff']
|
|
admin = data['admin']
|
|
|
|
# Render explanation request email (English)
|
|
context_en = {
|
|
'complaint': complaint,
|
|
'staff': staff_member,
|
|
'requested_by': admin,
|
|
'sla_hours': sla_config.response_hours,
|
|
'due_date': explanation.sla_due_at,
|
|
'site_url': 'http://localhost:8000'
|
|
}
|
|
|
|
try:
|
|
email_body_en = render_to_string(
|
|
'complaints/emails/explanation_request_en.txt',
|
|
context_en
|
|
)
|
|
print(f"✓ English email template rendered successfully")
|
|
print(f" - Preview (first 200 chars): {email_body_en[:200]}...")
|
|
except Exception as e:
|
|
print(f"✗ Error rendering English template: {e}")
|
|
return False
|
|
|
|
# Render explanation request email (Arabic)
|
|
context_ar = {
|
|
'complaint': complaint,
|
|
'staff': staff_member,
|
|
'requested_by': admin,
|
|
'sla_hours': sla_config.response_hours,
|
|
'due_date': explanation.sla_due_at,
|
|
'site_url': 'http://localhost:8000'
|
|
}
|
|
|
|
try:
|
|
email_body_ar = render_to_string(
|
|
'complaints/emails/explanation_request_ar.txt',
|
|
context_ar
|
|
)
|
|
print(f"✓ Arabic email template rendered successfully")
|
|
print(f" - Preview (first 200 chars): {email_body_ar[:200]}...")
|
|
except Exception as e:
|
|
print(f"✗ Error rendering Arabic template: {e}")
|
|
return False
|
|
|
|
# Render reminder email (English)
|
|
hours_remaining = int((explanation.sla_due_at - timezone.now()).total_seconds() / 3600)
|
|
reminder_context_en = {
|
|
'complaint': complaint,
|
|
'staff': staff_member,
|
|
'explanation': explanation,
|
|
'hours_remaining': hours_remaining,
|
|
'due_date': explanation.sla_due_at,
|
|
'site_url': 'http://localhost:8000'
|
|
}
|
|
|
|
try:
|
|
reminder_body_en = render_to_string(
|
|
'complaints/emails/explanation_reminder_en.txt',
|
|
reminder_context_en
|
|
)
|
|
print(f"✓ English reminder email template rendered successfully")
|
|
print(f" - Preview (first 200 chars): {reminder_body_en[:200]}...")
|
|
except Exception as e:
|
|
print(f"✗ Error rendering English reminder template: {e}")
|
|
return False
|
|
|
|
# Render reminder email (Arabic)
|
|
reminder_context_ar = {
|
|
'complaint': complaint,
|
|
'staff': staff_member,
|
|
'explanation': explanation,
|
|
'hours_remaining': hours_remaining,
|
|
'due_date': explanation.sla_due_at,
|
|
'site_url': 'http://localhost:8000'
|
|
}
|
|
|
|
try:
|
|
reminder_body_ar = render_to_string(
|
|
'complaints/emails/explanation_reminder_ar.txt',
|
|
reminder_context_ar
|
|
)
|
|
print(f"✓ Arabic reminder email template rendered successfully")
|
|
print(f" - Preview (first 200 chars): {reminder_body_ar[:200]}...")
|
|
except Exception as e:
|
|
print(f"✗ Error rendering Arabic reminder template: {e}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def test_reminder_sending(data, explanation, sla_config):
|
|
"""Test sending reminders for pending explanations"""
|
|
print_section("Test 4: Explanation Reminder Sending")
|
|
|
|
# Clear mail outbox
|
|
mail.outbox = []
|
|
|
|
# Update explanation to be in reminder window (12 hours before deadline)
|
|
explanation.sla_due_at = timezone.now() + timedelta(hours=sla_config.reminder_hours_before)
|
|
explanation.save()
|
|
|
|
# Run reminder task
|
|
print("Running send_explanation_reminders task...")
|
|
send_explanation_reminders()
|
|
|
|
# Check if email was sent
|
|
if len(mail.outbox) > 0:
|
|
print(f"✓ Reminder email sent")
|
|
email = mail.outbox[0]
|
|
print(f" - To: {email.to}")
|
|
print(f" - Subject: {email.subject}")
|
|
print(f" - Body preview: {email.body[:200]}...")
|
|
|
|
# Verify email was sent to the correct recipient
|
|
staff_member = data['staff']
|
|
assert staff_member.email in email.to, "Email not sent to correct recipient"
|
|
print(f"✓ Email sent to correct recipient: {staff_member.email}")
|
|
|
|
return True
|
|
else:
|
|
print(f"✗ No reminder email sent")
|
|
return False
|
|
|
|
|
|
def test_escalation_to_manager(data, explanation, sla_config):
|
|
"""Test escalation to manager when deadline is breached"""
|
|
print_section("Test 5: Escalation to Manager")
|
|
|
|
staff_member = data['staff']
|
|
manager = data['manager']
|
|
complaint = data['complaint']
|
|
|
|
# Clear mail outbox
|
|
mail.outbox = []
|
|
|
|
# Set explanation as overdue
|
|
explanation.sla_due_at = timezone.now() - timedelta(hours=1)
|
|
explanation.is_overdue = False # Reset to allow test
|
|
explanation.save()
|
|
|
|
print(f"Explanation request set as overdue:")
|
|
print(f" - Deadline: {explanation.sla_due_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print(f" - Current time: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print(f" - Manager: {manager.get_full_name()} ({manager.email})")
|
|
|
|
# Run overdue check task
|
|
print("\nRunning check_overdue_explanation_requests task...")
|
|
check_overdue_explanation_requests()
|
|
|
|
# Check if explanation was escalated
|
|
explanation.refresh_from_db()
|
|
|
|
if explanation.is_overdue:
|
|
print(f"✓ Explanation marked as overdue")
|
|
|
|
# Note: escalated_to_manager is a self-referential field to another ComplaintExplanation
|
|
# In this test, we just verify it was marked overdue
|
|
|
|
# Check if email was sent to manager
|
|
if len(mail.outbox) > 0:
|
|
email = mail.outbox[-1]
|
|
print(f"✓ Email sent")
|
|
print(f" - To: {email.to}")
|
|
print(f" - Subject: {email.subject}")
|
|
print(f" - Body preview: {email.body[:200]}...")
|
|
else:
|
|
print(f"⚠ No escalation email sent (email sending may be disabled)")
|
|
|
|
return True
|
|
else:
|
|
print(f"✗ Explanation was not escalated")
|
|
return False
|
|
|
|
|
|
def test_multiple_reminders(data, sla_config):
|
|
"""Test multiple reminders at different intervals"""
|
|
print_section("Test 6: Multiple Reminders")
|
|
|
|
complaint = data['complaint']
|
|
staff_member = data['staff']
|
|
admin = data['admin']
|
|
|
|
# Create new explanation with unique token and email_sent_at
|
|
unique_token = secrets.token_urlsafe(64)
|
|
explanation = ComplaintExplanation.objects.create(
|
|
complaint=complaint,
|
|
staff=staff_member,
|
|
requested_by=admin,
|
|
is_used=False,
|
|
token=unique_token,
|
|
sla_due_at=timezone.now() + timedelta(hours=sla_config.response_hours),
|
|
email_sent_at=timezone.now(), # Set email_sent_at so tasks can process it
|
|
is_overdue=False
|
|
)
|
|
|
|
print(f"Created new explanation request:")
|
|
print(f" - ID: {explanation.id}")
|
|
|
|
# Test reminder (12 hours before)
|
|
print(f"\nTesting reminder ({sla_config.reminder_hours_before} hours before deadline)...")
|
|
mail.outbox = []
|
|
explanation.sla_due_at = timezone.now() + timedelta(hours=sla_config.reminder_hours_before)
|
|
explanation.reminder_sent_at = None
|
|
explanation.save()
|
|
|
|
send_explanation_reminders()
|
|
|
|
explanation.refresh_from_db()
|
|
first_reminder_count = len(mail.outbox)
|
|
print(f" - Emails sent: {first_reminder_count}")
|
|
print(f" - Reminder sent at: {explanation.reminder_sent_at}")
|
|
|
|
if first_reminder_count > 0:
|
|
print(f"✓ Reminder system working correctly")
|
|
return True
|
|
else:
|
|
print(f"✗ Multiple reminders not working correctly")
|
|
return False
|
|
|
|
|
|
def test_auto_close(data, sla_config):
|
|
"""Test auto-close functionality"""
|
|
print_section("Test 7: Auto-Close Functionality")
|
|
|
|
# Auto-close is not implemented in the current model
|
|
# This test is skipped
|
|
print(f"Auto-close test skipped - not implemented in current model")
|
|
print(f" - The model does not have auto_close functionality")
|
|
return True
|
|
|
|
|
|
def cleanup_test_data():
|
|
"""Clean up test data"""
|
|
print_section("Cleaning Up Test Data")
|
|
|
|
# Delete test explanations
|
|
ComplaintExplanation.objects.all().delete()
|
|
|
|
# Delete test complaints
|
|
Complaint.objects.all().delete()
|
|
|
|
# Delete test staff
|
|
Staff.objects.filter(employee_id__in=['STF001', 'MGR001']).delete()
|
|
|
|
# Delete test departments
|
|
Department.objects.filter(name='Test Cardiology').delete()
|
|
|
|
# Delete test hospital
|
|
Hospital.objects.filter(name='Test Hospital').delete()
|
|
|
|
print("✓ Test data cleaned up")
|
|
|
|
|
|
def run_all_tests():
|
|
"""Run all explanation SLA tests"""
|
|
print("\n" + "#"*80)
|
|
print("# EXPLANATION REQUEST SLA SYSTEM TESTS")
|
|
print("#"*80)
|
|
|
|
try:
|
|
# Create test data
|
|
data = create_test_data()
|
|
|
|
# Test 1: SLA Configuration
|
|
sla_config = test_sla_configuration()
|
|
|
|
# Test 2: Explanation Request Creation
|
|
explanation = test_explanation_request_creation(data, sla_config)
|
|
|
|
# Test 3: Email Template Rendering
|
|
test_email_template_rendering(data, explanation, sla_config)
|
|
|
|
# Test 4: Reminder Sending
|
|
test_reminder_sending(data, explanation, sla_config)
|
|
|
|
# Test 5: Escalation to Manager
|
|
test_escalation_to_manager(data, explanation, sla_config)
|
|
|
|
# Test 6: Multiple Reminders
|
|
test_multiple_reminders(data, sla_config)
|
|
|
|
# Test 7: Auto-Close
|
|
test_auto_close(data, sla_config)
|
|
|
|
# Cleanup
|
|
cleanup_test_data()
|
|
|
|
print_section("TEST SUMMARY")
|
|
print("✓ All explanation SLA tests completed!")
|
|
print("\nNext Steps:")
|
|
print("1. Test the complete workflow in the UI")
|
|
print("2. Monitor Celery tasks for automatic reminders")
|
|
print("3. Verify email delivery in production")
|
|
print("4. Adjust SLA settings as needed")
|
|
|
|
except Exception as e:
|
|
print(f"\n✗ Test failed with error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run_all_tests()
|