HH/docs/SURVEY_DELIVERY_IMPLEMENTATION.md

870 lines
22 KiB
Markdown

# 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:**
```python
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:**
```python
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:**
```python
# 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:**
```python
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:**
```python
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:**
```python
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`:
```python
# 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:
```python
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
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
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
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:
```bash
# 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.
### Recommended SMS Services
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
```python
# 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
```python
# 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
```python
@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:
```python
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
```python
# 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:**
```python
survey.status = 'SENT'
survey.save()
```
**✅ Good:**
```python
from apps.surveys.services import SurveyDeliveryService
SurveyDeliveryService.deliver_survey(survey)
```
### 2. Validate Contact Information
Ensure patients have valid contact info before creating surveys:
```python
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:
```python
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:
```python
# 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:
```python
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:
```python
@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:
```python
# 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):
```python
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:
```python
@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