517 lines
14 KiB
Markdown
517 lines
14 KiB
Markdown
# External API Notification Service
|
||
|
||
## Overview
|
||
|
||
The PX360 notification system has been extended to support sending emails and SMS via external API endpoints. This allows integration with third-party notification services while maintaining backward compatibility with existing Django SMTP functionality.
|
||
|
||
## Features
|
||
|
||
- ✅ **Separate API Methods**: `send_email_via_api()` and `send_sms_via_api()`
|
||
- ✅ **Generic Request Format**: Simple JSON structure compatible with most APIs
|
||
- ✅ **Flexible Authentication**: Supports both `Bearer` token and `X-API-KEY` header
|
||
- ✅ **Fire-and-Forget**: Accepts any 2xx response (API processes in background)
|
||
- ✅ **Retry Logic**: Exponential backoff with configurable max retries
|
||
- ✅ **Comprehensive Logging**: All attempts logged to Django logger and NotificationLog
|
||
- ✅ **Error Handling**: Handles timeouts, connection errors, and HTTP errors
|
||
- ✅ **Configurable**: All settings via environment variables
|
||
- ✅ **Non-Breaking**: Existing code continues to work unchanged
|
||
|
||
## Configuration
|
||
|
||
### Environment Variables
|
||
|
||
Add these to your `.env` file:
|
||
|
||
```bash
|
||
# External Email API
|
||
EMAIL_API_ENABLED=false # Enable/disable email API
|
||
EMAIL_API_URL=https://api.example.com/send-email
|
||
EMAIL_API_KEY=your-api-key-here
|
||
EMAIL_API_AUTH_METHOD=bearer # Options: bearer, api_key
|
||
EMAIL_API_METHOD=POST
|
||
EMAIL_API_TIMEOUT=10 # Request timeout in seconds
|
||
EMAIL_API_MAX_RETRIES=3 # Maximum retry attempts
|
||
EMAIL_API_RETRY_DELAY=2 # Initial delay in seconds
|
||
|
||
# External SMS API
|
||
SMS_API_ENABLED=false # Enable/disable SMS API
|
||
SMS_API_URL=https://api.example.com/send-sms
|
||
SMS_API_KEY=your-api-key-here
|
||
SMS_API_AUTH_METHOD=bearer # Options: bearer, api_key
|
||
SMS_API_METHOD=POST
|
||
SMS_API_TIMEOUT=10 # Request timeout in seconds
|
||
SMS_API_MAX_RETRIES=3 # Maximum retry attempts
|
||
SMS_API_RETRY_DELAY=2 # Initial delay in seconds
|
||
```
|
||
|
||
### Settings Structure
|
||
|
||
The configuration is automatically loaded into `settings.EXTERNAL_NOTIFICATION_API`:
|
||
|
||
```python
|
||
EXTERNAL_NOTIFICATION_API = {
|
||
'email': {
|
||
'enabled': env.bool('EMAIL_API_ENABLED', default=False),
|
||
'url': env('EMAIL_API_URL', default=''),
|
||
'api_key': env('EMAIL_API_KEY', default=''),
|
||
'auth_method': env('EMAIL_API_AUTH_METHOD', default='bearer'),
|
||
'method': env('EMAIL_API_METHOD', default='POST'),
|
||
'timeout': env.int('EMAIL_API_TIMEOUT', default=10),
|
||
'max_retries': env.int('EMAIL_API_MAX_RETRIES', default=3),
|
||
'retry_delay': env.int('EMAIL_API_RETRY_DELAY', default=2),
|
||
},
|
||
'sms': {
|
||
'enabled': env.bool('SMS_API_ENABLED', default=False),
|
||
'url': env('SMS_API_URL', default=''),
|
||
'api_key': env('SMS_API_KEY', default=''),
|
||
'auth_method': env('SMS_API_AUTH_METHOD', default='bearer'),
|
||
'method': env('SMS_API_METHOD', default='POST'),
|
||
'timeout': env.int('SMS_API_TIMEOUT', default=10),
|
||
'max_retries': env.int('SMS_API_MAX_RETRIES', default=3),
|
||
'retry_delay': env.int('SMS_API_RETRY_DELAY', default=2),
|
||
},
|
||
}
|
||
```
|
||
|
||
## API Request Format
|
||
|
||
### Email API Request
|
||
|
||
**Endpoint**: POST to `EMAIL_API_URL`
|
||
|
||
**Headers**:
|
||
```
|
||
Content-Type: application/json
|
||
Authorization: Bearer {EMAIL_API_KEY} # or X-API-KEY: {EMAIL_API_KEY}
|
||
```
|
||
|
||
**Request Body**:
|
||
```json
|
||
{
|
||
"to": "recipient@example.com",
|
||
"subject": "Email Subject",
|
||
"message": "Plain text message",
|
||
"html_message": "<html>Optional HTML content</html>"
|
||
}
|
||
```
|
||
|
||
**Expected Response**: Any 2xx status code (200-299)
|
||
|
||
### SMS API Request
|
||
|
||
**Endpoint**: POST to `SMS_API_URL`
|
||
|
||
**Headers**:
|
||
```
|
||
Content-Type: application/json
|
||
Authorization: Bearer {SMS_API_KEY} # or X-API-KEY: {SMS_API_KEY}
|
||
```
|
||
|
||
**Request Body**:
|
||
```json
|
||
{
|
||
"to": "+966501234567",
|
||
"message": "SMS message text"
|
||
}
|
||
```
|
||
|
||
**Expected Response**: Any 2xx status code (200-299)
|
||
|
||
## Usage
|
||
|
||
### Basic Email via API
|
||
|
||
```python
|
||
from apps.notifications.services import NotificationService
|
||
|
||
# Simple email
|
||
log = NotificationService.send_email_via_api(
|
||
message='Your account has been created',
|
||
email='user@example.com',
|
||
subject='Welcome to PX360'
|
||
)
|
||
|
||
# Email with HTML
|
||
log = NotificationService.send_email_via_api(
|
||
message='Plain text version',
|
||
email='user@example.com',
|
||
subject='Welcome',
|
||
html_message='<h1>Welcome</h1><p>Your account is ready!</p>'
|
||
)
|
||
|
||
# Email with tracking
|
||
complaint = Complaint.objects.get(id=123)
|
||
log = NotificationService.send_email_via_api(
|
||
message='You have a new complaint...',
|
||
email='staff@example.com',
|
||
subject='New Complaint',
|
||
related_object=complaint,
|
||
metadata={'complaint_id': str(complaint.id)}
|
||
)
|
||
```
|
||
|
||
### Basic SMS via API
|
||
|
||
```python
|
||
from apps.notifications.services import NotificationService
|
||
|
||
# Simple SMS
|
||
log = NotificationService.send_sms_via_api(
|
||
message='Your verification code is 123456',
|
||
phone='+966501234567'
|
||
)
|
||
|
||
# SMS with tracking
|
||
survey_instance = SurveyInstance.objects.get(id=123)
|
||
log = NotificationService.send_sms_via_api(
|
||
message='Please complete your survey: https://...',
|
||
phone='+966501234567',
|
||
related_object=survey_instance,
|
||
metadata={'survey_id': str(survey_instance.id)}
|
||
)
|
||
```
|
||
|
||
### Comparison with Existing Methods
|
||
|
||
```python
|
||
from apps.notifications.services import NotificationService
|
||
|
||
# Existing method - uses Django SMTP
|
||
NotificationService.send_email(
|
||
email='user@example.com',
|
||
subject='Welcome',
|
||
message='Hello!'
|
||
)
|
||
|
||
# New method - uses external API
|
||
NotificationService.send_email_via_api(
|
||
message='Hello!',
|
||
email='user@example.com',
|
||
subject='Welcome'
|
||
)
|
||
```
|
||
|
||
## Retry Logic
|
||
|
||
Both API methods implement exponential backoff retry:
|
||
|
||
1. **Initial attempt** (retry 0)
|
||
2. Wait 2 seconds (retry_delay)
|
||
3. **First retry** (retry 1)
|
||
4. Wait 4 seconds (retry_delay × 2^1)
|
||
5. **Second retry** (retry 2)
|
||
6. Wait 8 seconds (retry_delay × 2^2)
|
||
7. **Final retry** (retry 3)
|
||
|
||
Configurable via:
|
||
- `EMAIL_API_MAX_RETRIES` / `SMS_API_MAX_RETRIES` (default: 3)
|
||
- `EMAIL_API_RETRY_DELAY` / `SMS_API_RETRY_DELAY` (default: 2 seconds)
|
||
|
||
## Error Handling
|
||
|
||
All errors are handled gracefully:
|
||
|
||
### Connection Errors
|
||
- Logged as "Connection error"
|
||
- Retry attempted if retries available
|
||
|
||
### Timeout Errors
|
||
- Logged as "Request timeout"
|
||
- Retry attempted if retries available
|
||
|
||
### HTTP Errors
|
||
- Non-2xx status codes logged with status number
|
||
- Retry attempted if retries available
|
||
|
||
### Unexpected Errors
|
||
- Full error message logged
|
||
- Retry attempted if retries available
|
||
|
||
### Final State
|
||
- After all retries exhausted, NotificationLog marked as `failed`
|
||
- Error details stored in log's error_message field
|
||
|
||
## Database Tracking
|
||
|
||
All API notifications are tracked in the `NotificationLog` model:
|
||
|
||
```python
|
||
log = NotificationService.send_email_via_api(
|
||
message='Hello',
|
||
email='user@example.com',
|
||
subject='Test'
|
||
)
|
||
|
||
# Check status
|
||
print(log.status) # 'sent' or 'failed'
|
||
|
||
# Check metadata
|
||
print(log.metadata)
|
||
# {
|
||
# 'api_url': 'https://api.example.com/send-email',
|
||
# 'auth_method': 'bearer',
|
||
# ...
|
||
# }
|
||
```
|
||
|
||
## Logging
|
||
|
||
All API calls are logged to Django logger at different levels:
|
||
|
||
### INFO Level
|
||
- Successful API calls
|
||
- API request initiation
|
||
|
||
### WARNING Level
|
||
- Non-2xx HTTP responses
|
||
- Connection errors (during retries)
|
||
- Timeout errors (during retries)
|
||
|
||
### ERROR Level
|
||
- Unexpected exceptions
|
||
- Final failure after all retries
|
||
|
||
Logs are written to:
|
||
- Console (in development)
|
||
- `logs/px360.log` (file)
|
||
- `logs/integrations.log` (integration-specific)
|
||
|
||
## Examples
|
||
|
||
### Example 1: Send Welcome Email via API
|
||
|
||
```python
|
||
def send_welcome_email(user):
|
||
from apps.notifications.services import NotificationService
|
||
|
||
log = NotificationService.send_email_via_api(
|
||
message=f'Welcome {user.get_full_name()}!',
|
||
email=user.email,
|
||
subject='Welcome to PX360'
|
||
)
|
||
|
||
if log and log.status == 'sent':
|
||
print("Email sent successfully via API")
|
||
else:
|
||
print("Failed to send email")
|
||
```
|
||
|
||
### Example 2: Send SMS Verification Code
|
||
|
||
```python
|
||
def send_verification_code(phone, code):
|
||
from apps.notifications.services import NotificationService
|
||
|
||
log = NotificationService.send_sms_via_api(
|
||
message=f'Your verification code is: {code}',
|
||
phone=phone
|
||
)
|
||
|
||
return log.status == 'sent'
|
||
```
|
||
|
||
### Example 3: Send Complaint Notification via API
|
||
|
||
```python
|
||
def notify_complaint_department(complaint):
|
||
from apps.notifications.services import NotificationService
|
||
|
||
department = complaint.department
|
||
log = NotificationService.send_email_via_api(
|
||
message=f'New complaint received from {complaint.patient.get_full_name()}',
|
||
email=department.contact_email,
|
||
subject=f'New Complaint: {complaint.ticket_number}',
|
||
related_object=complaint,
|
||
metadata={
|
||
'complaint_id': str(complaint.id),
|
||
'ticket_number': complaint.ticket_number
|
||
}
|
||
)
|
||
|
||
return log
|
||
```
|
||
|
||
### Example 4: Send Survey Invitation via API
|
||
|
||
```python
|
||
def send_survey_invitation_via_api(survey_instance):
|
||
from apps.notifications.services import NotificationService
|
||
|
||
patient = survey_instance.patient
|
||
survey_url = survey_instance.get_survey_url()
|
||
|
||
if survey_instance.delivery_channel == 'email':
|
||
log = NotificationService.send_email_via_api(
|
||
message=f'Please complete your survey: {survey_url}',
|
||
email=patient.email,
|
||
subject='Experience Survey',
|
||
related_object=survey_instance
|
||
)
|
||
elif survey_instance.delivery_channel == 'sms':
|
||
log = NotificationService.send_sms_via_api(
|
||
message=f'Complete your survey: {survey_url}',
|
||
phone=patient.phone,
|
||
related_object=survey_instance
|
||
)
|
||
|
||
return log
|
||
```
|
||
|
||
## Troubleshooting
|
||
|
||
### API Disabled
|
||
|
||
**Problem**: Emails/SMS not being sent via API
|
||
|
||
**Solution**: Check that `EMAIL_API_ENABLED` or `SMS_API_ENABLED` is set to `true` in `.env`
|
||
|
||
```bash
|
||
# Check environment variable
|
||
echo $EMAIL_API_ENABLED
|
||
|
||
# Enable if disabled
|
||
EMAIL_API_ENABLED=true
|
||
```
|
||
|
||
### Authentication Failures
|
||
|
||
**Problem**: API returning 401 or 403 errors
|
||
|
||
**Solution**: Verify API key and auth method
|
||
|
||
```bash
|
||
# Check auth method
|
||
EMAIL_API_AUTH_METHOD=bearer # or api_key
|
||
|
||
# Verify API key is correct
|
||
EMAIL_API_KEY=your-actual-api-key
|
||
```
|
||
|
||
### Connection Timeout
|
||
|
||
**Problem**: Requests timing out
|
||
|
||
**Solution**: Increase timeout value
|
||
|
||
```bash
|
||
EMAIL_API_TIMEOUT=30 # Increase from 10 to 30 seconds
|
||
```
|
||
|
||
### Frequent Failures
|
||
|
||
**Problem**: API calls failing consistently
|
||
|
||
**Solution**: Check logs for specific error messages
|
||
|
||
```bash
|
||
# View logs
|
||
tail -f logs/px360.log | grep "API"
|
||
|
||
# Check NotificationLog in Django admin
|
||
# Navigate to /admin/notifications/notificationlog/
|
||
```
|
||
|
||
### No API Calls Being Made
|
||
|
||
**Problem**: Methods returning `None`
|
||
|
||
**Solution**: Verify configuration is loaded
|
||
|
||
```python
|
||
from django.conf import settings
|
||
|
||
# Check configuration
|
||
print(settings.EXTERNAL_NOTIFICATION_API)
|
||
# Should show 'email' and 'sms' keys with configuration
|
||
```
|
||
|
||
## Migration Guide
|
||
|
||
### Migrating from Django SMTP to API
|
||
|
||
**Before** (using Django SMTP):
|
||
```python
|
||
NotificationService.send_email(
|
||
email='user@example.com',
|
||
subject='Welcome',
|
||
message='Hello!'
|
||
)
|
||
```
|
||
|
||
**After** (using external API):
|
||
```python
|
||
NotificationService.send_email_via_api(
|
||
message='Hello!',
|
||
email='user@example.com',
|
||
subject='Welcome'
|
||
)
|
||
```
|
||
|
||
Note: The parameter order is slightly different - `message` comes before `email` for API methods.
|
||
|
||
### Gradual Migration Strategy
|
||
|
||
You can use both methods in parallel:
|
||
|
||
```python
|
||
def send_email(email, subject, message):
|
||
from apps.notifications.services import NotificationService
|
||
from django.conf import settings
|
||
|
||
# Use API if enabled
|
||
if settings.EXTERNAL_NOTIFICATION_API['email']['enabled']:
|
||
return NotificationService.send_email_via_api(
|
||
message=message,
|
||
email=email,
|
||
subject=subject
|
||
)
|
||
# Fall back to Django SMTP
|
||
else:
|
||
return NotificationService.send_email(
|
||
email=email,
|
||
subject=subject,
|
||
message=message
|
||
)
|
||
```
|
||
|
||
## Security Considerations
|
||
|
||
1. **API Keys**: Never commit `.env` file to version control
|
||
2. **HTTPS**: Always use HTTPS URLs for API endpoints
|
||
3. **Authentication**: Use Bearer tokens or API keys, never basic auth
|
||
4. **Logging**: API keys are not logged in full (metadata stores auth_method, not key)
|
||
5. **Timeouts**: Set reasonable timeouts to prevent hanging requests
|
||
|
||
## Performance Impact
|
||
|
||
- **Network Latency**: Each API call adds network round-trip time
|
||
- **Retries**: Failed requests are retried with exponential backoff
|
||
- **Logging**: Minimal overhead from logging to database
|
||
- **Async Consideration**: For high-volume scenarios, consider using Celery tasks
|
||
|
||
## Future Enhancements
|
||
|
||
Potential improvements for the API notification service:
|
||
|
||
1. **Async Support**: Integrate with Celery for background processing
|
||
2. **Webhooks**: Support for delivery status callbacks
|
||
3. **Templates**: Built-in email/SMS template support
|
||
4. **Rate Limiting**: Implement API rate limiting
|
||
5. **Bulk Operations**: Support for batch email/SMS sending
|
||
6. **Provider SDKs**: Integrate provider-specific SDKs (SendGrid, Twilio, etc.)
|
||
|
||
## Support
|
||
|
||
For issues or questions:
|
||
|
||
1. Check logs: `logs/px360.log` and `logs/integrations.log`
|
||
2. Verify `.env` configuration
|
||
3. Check `NotificationLog` entries in Django admin
|
||
4. Test API endpoint independently (curl, Postman, etc.)
|
||
|
||
## Related Documentation
|
||
|
||
- [Notification Service](../apps/notifications/README.md)
|
||
- [Environment Configuration](../config/settings/DJANGO_ENVIRON_CONFIG.md)
|
||
- [Logging Configuration](../config/settings/base.py#logging-configuration)
|