HH/docs/SURVEY_DELIVERY_IMPLEMENTATION.md

22 KiB

Survey Delivery Implementation

Date: January 28, 2026
Status: Complete
Version: 1.0


Overview

The Survey Delivery System automatically sends patient experience surveys via SMS or Email when patients complete their journey and are discharged. This document describes the implementation, architecture, and usage of the survey delivery system.


Architecture

Components

HIS Simulator
    ↓
[Patient Discharged]
    ↓
HISAdapter.trigger_post_discharge_survey()
    ↓
SurveyDeliveryService.deliver_survey()
    ↓
┌──────────────────┬──────────────────┐
│   SMS Delivery   │  Email Delivery  │
│  (Logged/Planned)│  (Implemented)   │
└──────────────────┴──────────────────┘

Files

File Purpose
apps/surveys/services.py SurveyDeliveryService - handles all survey delivery
apps/integrations/services/his_adapter.py Trigger surveys when patients are discharged
apps/surveys/models.py SurveyInstance model - tracks delivery status

SurveyDeliveryService

Location

apps/surveys/services.py

Class Methods

1. generate_survey_url(survey_instance) -> str

Generates secure survey URL with access token.

Parameters:

  • survey_instance: SurveyInstance object

Returns:

  • Full survey URL (e.g., http://localhost:8000/surveys/s/TOKEN/)

Example:

from apps.surveys.services import SurveyDeliveryService
from apps.surveys.models import SurveyInstance

survey = SurveyInstance.objects.get(id=uuid)
url = SurveyDeliveryService.generate_survey_url(survey)
# Returns: http://localhost:8000/surveys/s/GRsOpbYmtpf-kiZ0fzrtR4GvgJc5Yv_WWGahMW4moB4/

2. generate_sms_message(patient_name: str, survey_url: str, hospital_name: str = None) -> str

Generates SMS message with survey link.

Parameters:

  • patient_name: Patient's first name
  • survey_url: Survey link
  • hospital_name: Optional hospital name

Returns:

  • SMS message text

Example Message:

Dear Fatima,

Thank you for choosing Al Hammadi Hospital. We value your feedback! Please take a moment to complete our patient experience survey:

http://localhost:8000/surveys/s/GRsOpbYmtpf-kiZ0fzrtR4GvgJc5Yv_WWGahMW4moB4/

This survey will take approximately 2-3 minutes.
Thank you for helping us improve our services!

3. generate_email_message(patient_name: str, survey_url: str, hospital_name: str = None) -> str

Generates email message with survey link.

Parameters:

  • patient_name: Patient's first name
  • survey_url: Survey link
  • hospital_name: Optional hospital name

Returns:

  • Email message text

Example Message:

Dear Fatima,

Thank you for choosing Al Hammadi Hospital.

We value your feedback and would appreciate it if you could take a moment to complete our patient experience survey.

Your feedback helps us improve our services and provide better care for all our patients.

Survey Link: http://localhost:8000/surveys/s/GRsOpbYmtpf-kiZ0fzrtR4GvgJc5Yv_WWGahMW4moB4/

This survey will take approximately 2-3 minutes to complete.

If you have any questions or concerns, please don't hesitate to contact us.

Thank you for helping us improve our services!

Best regards,
Patient Experience Team

4. send_survey_sms(survey_instance) -> bool

Sends survey via SMS.

Parameters:

  • survey_instance: SurveyInstance object

Returns:

  • True if sent successfully, False otherwise

Behavior:

  1. Validates recipient phone number
  2. Generates survey URL
  3. Generates SMS message
  4. Current: Logs message (TODO: Integrate with Twilio or SMS service)
  5. Updates survey status to SENT
  6. Sets sent_at timestamp

Example:

from apps.surveys.services import SurveyDeliveryService
from apps.surveys.models import SurveyInstance

survey = SurveyInstance.objects.get(id=uuid)
success = SurveyDeliveryService.send_survey_sms(survey)
if success:
    print("SMS sent successfully!")
else:
    print("SMS delivery failed")

5. send_survey_email(survey_instance) -> bool

Sends survey via Email.

Parameters:

  • survey_instance: SurveyInstance object

Returns:

  • True if sent successfully, False otherwise

Behavior:

  1. Validates recipient email address
  2. Generates survey URL
  3. Generates email message
  4. Sends email using Django's send_mail()
  5. Updates survey status to SENT
  6. Sets sent_at timestamp

Configuration:

# settings.py
DEFAULT_FROM_EMAIL = 'noreply@hospital.com'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@example.com'
EMAIL_HOST_PASSWORD = 'your-password'

Example:

from apps.surveys.services import SurveyDeliveryService
from apps.surveys.models import SurveyInstance

survey = SurveyInstance.objects.get(id=uuid)
success = SurveyDeliveryService.send_survey_email(survey)
if success:
    print("Email sent successfully!")
else:
    print("Email delivery failed")

6. deliver_survey(survey_instance) -> bool

Delivers survey based on configured delivery channel.

Parameters:

  • survey_instance: SurveyInstance object

Returns:

  • True if delivered successfully, False otherwise

Behavior:

  1. Ensures patient contact info is set
  2. Calls appropriate delivery method based on delivery_channel:
    • sms: send_survey_sms()
    • email: send_survey_email()
    • whatsapp: Not yet implemented (logs warning)
  3. Handles unknown channels with error logging

Example:

from apps.surveys.services import SurveyDeliveryService
from apps.surveys.models import SurveyInstance

survey = SurveyInstance.objects.get(id=uuid)
success = SurveyDeliveryService.deliver_survey(survey)

Integration with HISAdapter

Location

apps/integrations/services/his_adapter.py

Method: trigger_post_discharge_survey(journey: PatientJourneyInstance) -> Optional[SurveyInstance]

Purpose: Creates and delivers post-discharge survey when patient journey is completed.

Flow:

def trigger_post_discharge_survey(journey: PatientJourneyInstance) -> Optional[SurveyInstance]:
    # 1. Check if journey template has post-discharge survey enabled
    if not journey.journey_template.send_post_discharge_survey:
        return None
    
    # 2. Find appropriate survey template
    survey_template = SurveyTemplate.objects.filter(
        name__icontains="OPD",
        is_active=True
    ).first()
    
    # 3. Create survey instance (PENDING status)
    survey = SurveyInstance.objects.create(
        survey_template=survey_template,
        patient=journey.patient,
        journey_instance=journey,
        hospital=journey.hospital,
        status="PENDING",
        delivery_channel="SMS",
        recipient_phone=journey.patient.phone
    )
    
    # 4. Deliver survey using SurveyDeliveryService
    from apps.surveys.services import SurveyDeliveryService
    delivery_success = SurveyDeliveryService.deliver_survey(survey)
    
    # 5. Return survey (even if delivery fails)
    return survey

Called From: HISAdapter.process_his_data() when patient is discharged


Survey Lifecycle

Status Transitions

PENDING (Created)
    ↓
SENT (Delivered via SMS/Email)
    ↓
[Patient Opens Survey]
    ↓
VIEWED
    ↓
IN_PROGRESS
    ↓
COMPLETED
    ↓
[Score Calculated]
    ↓
[Marked Negative if Score < Threshold]
    ↓
[Follow-up if Negative]

Status Descriptions

Status Description
PENDING Survey created, waiting to be delivered
SENT Survey delivered to patient (SMS/Email sent)
VIEWED Patient opened survey link
IN_PROGRESS Patient started answering questions
COMPLETED Patient finished answering all questions
ABANDONED Patient started but didn't finish
EXPIRED Survey access token expired
CANCELLED Survey cancelled by admin

Configuration

Settings

Add these settings to your settings.py:

# Survey Base URL
# Used to generate survey links
SURVEY_BASE_URL = 'http://localhost:8000'  # Change to production URL

# Email Configuration
DEFAULT_FROM_EMAIL = 'noreply@hospital.com'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@example.com'
EMAIL_HOST_PASSWORD = 'your-password'

# Survey Settings
SURVEY_TOKEN_EXPIRATION_DAYS = 30  # Survey link validity
SURVEY_NEGATIVE_THRESHOLD = 3.0  # Score below which survey is marked negative

Journey Template Configuration

To enable post-discharge surveys for a journey:

from apps.journeys.models import PatientJourneyTemplate

template = PatientJourneyTemplate.objects.get(id=uuid)
template.send_post_discharge_survey = True  # Enable surveys
template.save()

Testing

Manual Testing

Test SMS Message Generation

python manage.py shell

from apps.surveys.models import SurveyInstance
from apps.surveys.services import SurveyDeliveryService

# Get a recent survey
survey = SurveyInstance.objects.filter(status='SENT').order_by('-sent_at').first()

# Generate SMS message
url = SurveyDeliveryService.generate_survey_url(survey)
message = SurveyDeliveryService.generate_sms_message(
    survey.patient.first_name,
    url,
    survey.hospital.name
)

print(message)

Test Email Delivery

python manage.py shell

from apps.surveys.models import SurveyInstance
from apps.surveys.services import SurveyDeliveryService

# Get a survey with email delivery
survey = SurveyInstance.objects.filter(
    delivery_channel='email'
).order_by('-created_at').first()

# Set recipient email (if needed)
survey.recipient_email = 'test@example.com'
survey.save()

# Send email
success = SurveyDeliveryService.send_survey_email(survey)
print(f"Email sent: {success}")

Test Full Survey Delivery

python manage.py shell

from apps.surveys.models import SurveyInstance
from apps.surveys.services import SurveyDeliveryService

# Get a pending survey
survey = SurveyInstance.objects.filter(status='PENDING').first()

# Deliver survey
success = SurveyDeliveryService.deliver_survey(survey)

# Verify
survey.refresh_from_db()
print(f"Status: {survey.status}")
print(f"Sent at: {survey.sent_at}")
print(f"Delivery successful: {success}")

Automated Testing

Run the HIS simulator to test end-to-end:

# Run simulator with test patients
python apps/simulator/his_simulator.py --max-patients 5 --delay 3

# Check surveys created
python manage.py shell -c "
from apps.surveys.models import SurveyInstance
surveys = SurveyInstance.objects.order_by('-created_at')[:5]
print(f'Recent Surveys:')
for s in surveys:
    print(f'Patient: {s.patient.first_name}, Status: {s.status}')
"

SMS Integration (Future Work)

Current Status

SMS messages are currently logged but not sent. The system is ready for SMS integration.

  1. Twilio (Recommended)

    • Reliable, global coverage
    • Excellent documentation
    • Easy Python integration
  2. MessageBird

    • Good for multi-channel
    • Competitive pricing
  3. AWS SNS

    • AWS native
    • Scalable

Twilio Integration Example

# requirements.txt
# twilio==8.11.0

# apps/surveys/services.py
from twilio.rest import Client
from django.conf import settings

class SurveyDeliveryService:
    # ... existing methods ...
    
    @staticmethod
    def send_survey_sms(survey_instance) -> bool:
        """Send survey via SMS using Twilio"""
        if not survey_instance.recipient_phone:
            logger.warning(f"No phone number for survey {survey_instance.id}")
            return False
        
        try:
            # Generate survey URL and message
            survey_url = SurveyDeliveryService.generate_survey_url(survey_instance)
            patient_name = survey_instance.patient.first_name
            hospital_name = survey_instance.hospital.name
            message = SurveyDeliveryService.generate_sms_message(
                patient_name, survey_url, hospital_name
            )
            
            # Initialize Twilio client
            client = Client(
                settings.TWILIO_ACCOUNT_SID,
                settings.TWILIO_AUTH_TOKEN
            )
            
            # Send SMS
            sms = client.messages.create(
                body=message,
                from_=settings.TWILIO_PHONE_NUMBER,
                to=survey_instance.recipient_phone
            )
            
            # Log success
            logger.info(f"SMS sent via Twilio. SID: {sms.sid}")
            
            # Update survey instance
            survey_instance.status = 'SENT'
            survey_instance.sent_at = timezone.now()
            survey_instance.save(update_fields=['status', 'sent_at'])
            
            return True
            
        except Exception as e:
            logger.error(f"Error sending SMS for survey {survey_instance.id}: {str(e)}")
            return False

Settings for Twilio

# settings.py

# Twilio Configuration
TWILIO_ACCOUNT_SID = 'your_account_sid'
TWILIO_AUTH_TOKEN = 'your_auth_token'
TWILIO_PHONE_NUMBER = '+1234567890'  # Your Twilio phone number

WhatsApp Integration (Future Work)

Status

WhatsApp delivery is not yet implemented but the framework is in place.

Implementation Plan

  1. Use WhatsApp Business API
  2. Integrate with provider (Twilio, MessageBird, or Meta)
  3. Add to SurveyDeliveryService.deliver_survey()
  4. Create WhatsApp message templates

Example Structure

@staticmethod
def send_survey_whatsapp(survey_instance) -> bool:
    """Send survey via WhatsApp"""
    # TODO: Implement WhatsApp delivery
    from apps.whatsapp.services import WhatsAppService
    
    survey_url = SurveyDeliveryService.generate_survey_url(survey_instance)
    message = SurveyDeliveryService.generate_whatsapp_message(
        survey_instance.patient.first_name,
        survey_url,
        survey_instance.hospital.name
    )
    
    success = WhatsAppService.send_message(
        to=survey_instance.recipient_phone,
        message=message
    )
    
    if success:
        survey_instance.status = 'SENT'
        survey_instance.sent_at = timezone.now()
        survey_instance.save(update_fields=['status', 'sent_at'])
    
    return success

Troubleshooting

Issue: Surveys not being delivered

Symptoms:

  • Survey status remains PENDING
  • No SMS/Email sent
  • No logs indicating delivery attempt

Solutions:

  1. Check journey template has send_post_discharge_survey=True
  2. Verify patient has valid contact info (phone/email)
  3. Check logs for errors
  4. Verify email/SMS service configuration

Issue: Email not sent

Symptoms:

  • Survey status changes to SENT
  • But patient doesn't receive email

Solutions:

  1. Check email backend configuration in settings.py
  2. Verify recipient_email is set on survey
  3. Check email server logs
  4. Test email configuration with Django's send_mail()

Issue: SMS not sent (after Twilio integration)

Symptoms:

  • Survey status changes to SENT
  • But patient doesn't receive SMS

Solutions:

  1. Verify Twilio credentials
  2. Check Twilio account balance
  3. Verify recipient phone number format (include country code)
  4. Check Twilio logs for delivery status

Issue: Survey URL not accessible

Symptoms:

  • Patient receives link but gets 404 or permission error

Solutions:

  1. Check SURVEY_BASE_URL setting
  2. Verify survey access_token is valid
  3. Check token hasn't expired
  4. Verify URL view is configured correctly

Monitoring and Logging

Log Messages

The SurveyDeliveryService logs all delivery attempts:

logger.info(f"SMS would be sent to {survey_instance.recipient_phone}")
logger.info(f"Message: {message}")
logger.info(f"Survey email sent to {survey_instance.recipient_email}")
logger.warning(f"WhatsApp delivery not yet implemented for survey {survey_instance.id}")
logger.error(f"Error sending SMS for survey {survey_instance.id}: {str(e)}")

Monitoring Metrics

Track these metrics for survey delivery health:

  • Delivery Success Rate: (SENT surveys / Total surveys) * 100
  • Response Rate: (COMPLETED surveys / SENT surveys) * 100
  • Delivery Channel Distribution: SMS vs Email vs WhatsApp
  • Delivery Time: Time between creation and SENT status
  • Response Time: Time between SENT and COMPLETED

Dashboard Queries

# Delivery success rate
from apps.surveys.models import SurveyInstance
from django.db.models import Count, Q

total = SurveyInstance.objects.count()
sent = SurveyInstance.objects.filter(status='SENT').count()
success_rate = (sent / total * 100) if total > 0 else 0

# Response rate
completed = SurveyInstance.objects.filter(status='COMPLETED').count()
response_rate = (completed / sent * 100) if sent > 0 else 0

# Channel distribution
channels = SurveyInstance.objects.values('delivery_channel').annotate(
    count=Count('id')
).order_by('-count')

Best Practices

1. Always Use Delivery Service

Bad:

survey.status = 'SENT'
survey.save()

Good:

from apps.surveys.services import SurveyDeliveryService
SurveyDeliveryService.deliver_survey(survey)

2. Validate Contact Information

Ensure patients have valid contact info before creating surveys:

if not patient.phone and not patient.email:
    logger.warning(f"Patient {patient.id} has no contact info")
    return None

3. Handle Delivery Failures Gracefully

Even if delivery fails, create the survey record:

delivery_success = SurveyDeliveryService.deliver_survey(survey)
if not delivery_success:
    logger.warning(f"Survey {survey.id} delivery failed, but record created")
# Still return survey so it can be retried
return survey

4. Use Appropriate Delivery Channel

Consider patient preferences and data availability:

# Prefer SMS if available (higher open rate)
if patient.phone:
    delivery_channel = 'sms'
elif patient.email:
    delivery_channel = 'email'
else:
    logger.warning(f"No contact info for patient {patient.id}")
    return None

Security Considerations

1. Secure Survey URLs

  • Survey URLs use secure, randomly generated access_token
  • Tokens expire after SURVEY_TOKEN_EXPIRATION_DAYS
  • URLs should be sent over HTTPS in production

2. Contact Information Protection

  • Phone numbers and emails should be stored securely
  • Limit access to contact information
  • Use encryption in transit (TLS for email, HTTPS for web)

3. Rate Limiting

Consider implementing rate limiting to prevent abuse:

from django.core.cache import cache

def can_send_survey(patient_id):
    """Check if patient can receive survey (rate limit)"""
    key = f'survey_rate_limit_{patient_id}'
    return cache.get(key) is None

def set_survey_sent(patient_id):
    """Mark survey as sent for rate limit"""
    key = f'survey_rate_limit_{patient_id}'
    cache.set(key, True, timeout=86400)  # 24 hours

Future Enhancements

1. Retry Failed Deliveries

Implement retry logic for failed deliveries:

@staticmethod
def deliver_survey_with_retry(survey_instance, max_retries=3):
    """Deliver survey with automatic retry"""
    for attempt in range(max_retries):
        success = SurveyDeliveryService.deliver_survey(survey_instance)
        if success:
            return True
        
        # Wait before retry
        time.sleep(60 * (attempt + 1))
    
    # All retries failed
    logger.error(f"Failed to deliver survey {survey_instance.id} after {max_retries} attempts")
    return False

2. Delivery Preferences

Allow patients to choose preferred delivery channel:

# Patient model
class Patient(models.Model):
    preferred_contact_method = models.CharField(
        choices=[('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp')]
    )

3. Scheduled Surveys

Send surveys at optimal times (e.g., 2 hours after discharge):

from django.utils import timezone
from django.db.models import F

def schedule_survey(journey):
    """Schedule survey for 2 hours after discharge"""
    from apps.surveys.tasks import deliver_scheduled_survey
    
    deliver_time = timezone.now() + timedelta(hours=2)
    deliver_scheduled_survey.apply_async(
        args=[journey.id],
        eta=deliver_time
    )

4. Multi-language Support

Send surveys in patient's preferred language:

@staticmethod
def generate_sms_message(patient_name: str, survey_url: str, 
                        hospital_name: str = None, language='en') -> str:
    """Generate SMS message in specified language"""
    if language == 'ar':
        return SurveyDeliveryService._generate_arabic_message(...)
    else:
        return SurveyDeliveryService._generate_english_message(...)

Summary

The Survey Delivery System is fully functional with:

SMS Delivery (message generation implemented, Twilio integration ready)
Email Delivery (fully implemented using Django mail)
Automatic Triggering (via HISAdapter on patient discharge)
Secure URLs (token-based access)
Delivery Tracking (status updates, timestamps)
Error Handling (graceful failure handling)
Logging (comprehensive activity logging)

Next Steps for Production:

  1. Integrate with SMS service (Twilio recommended)
  2. Configure email backend settings
  3. Set up monitoring dashboards
  4. Implement retry logic
  5. Add rate limiting
  6. Implement WhatsApp delivery if needed

Document Version: 1.0
Last Updated: January 28, 2026
Maintainer: PX360 Development Team