HH/apps/notifications/services.py

338 lines
12 KiB
Python

"""
Notification services - Multi-channel notification delivery
This module provides a unified interface for sending notifications
via SMS, WhatsApp, and Email.
For now, implements console backends that log to database.
In production, integrate with actual providers (Twilio, WhatsApp Business API, SMTP).
"""
import logging
from django.conf import settings
from django.core.mail import send_mail
from .models import NotificationLog
logger = logging.getLogger(__name__)
class NotificationService:
"""
Unified notification service for all channels.
Usage:
NotificationService.send_sms('+966501234567', 'Your survey is ready')
NotificationService.send_email('user@email.com', 'Survey', 'Please complete...')
"""
@staticmethod
def send_sms(phone, message, related_object=None, metadata=None):
"""
Send SMS notification.
Args:
phone: Recipient phone number
message: SMS message text
related_object: Related model instance (optional)
metadata: Additional metadata dict (optional)
Returns:
NotificationLog instance
"""
# Create notification log
log = NotificationLog.objects.create(
channel='sms',
recipient=phone,
message=message,
content_object=related_object,
provider='console', # TODO: Replace with actual provider
metadata=metadata or {}
)
# Check if SMS is enabled
sms_config = settings.NOTIFICATION_CHANNELS.get('sms', {})
if not sms_config.get('enabled', False):
logger.info(f"[SMS Console] To: {phone} | Message: {message}")
log.mark_sent()
return log
# TODO: Integrate with actual SMS provider (Twilio, etc.)
# Example:
# try:
# from twilio.rest import Client
# client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
# message = client.messages.create(
# body=message,
# from_=settings.TWILIO_PHONE_NUMBER,
# to=phone
# )
# log.mark_sent(provider_message_id=message.sid)
# except Exception as e:
# log.mark_failed(str(e))
# Console backend for now
logger.info(f"[SMS Console] To: {phone} | Message: {message}")
log.mark_sent()
return log
@staticmethod
def send_whatsapp(phone, message, related_object=None, metadata=None):
"""
Send WhatsApp notification.
Args:
phone: Recipient phone number
message: WhatsApp message text
related_object: Related model instance (optional)
metadata: Additional metadata dict (optional)
Returns:
NotificationLog instance
"""
# Create notification log
log = NotificationLog.objects.create(
channel='whatsapp',
recipient=phone,
message=message,
content_object=related_object,
provider='console', # TODO: Replace with actual provider
metadata=metadata or {}
)
# Check if WhatsApp is enabled
whatsapp_config = settings.NOTIFICATION_CHANNELS.get('whatsapp', {})
if not whatsapp_config.get('enabled', False):
logger.info(f"[WhatsApp Console] To: {phone} | Message: {message}")
log.mark_sent()
return log
# TODO: Integrate with WhatsApp Business API
# Example:
# try:
# response = requests.post(
# f"{settings.WHATSAPP_API_URL}/messages",
# headers={'Authorization': f'Bearer {settings.WHATSAPP_API_KEY}'},
# json={
# 'to': phone,
# 'type': 'text',
# 'text': {'body': message}
# }
# )
# log.mark_sent(provider_message_id=response.json().get('id'))
# except Exception as e:
# log.mark_failed(str(e))
# Console backend for now
logger.info(f"[WhatsApp Console] To: {phone} | Message: {message}")
log.mark_sent()
return log
@staticmethod
def send_email(email, subject, message, html_message=None, related_object=None, metadata=None):
"""
Send Email notification.
Args:
email: Recipient email address
subject: Email subject
message: Email message (plain text)
html_message: Email message (HTML) (optional)
related_object: Related model instance (optional)
metadata: Additional metadata dict (optional)
Returns:
NotificationLog instance
"""
# Create notification log
log = NotificationLog.objects.create(
channel='email',
recipient=email,
subject=subject,
message=message,
content_object=related_object,
provider='console', # TODO: Replace with actual provider
metadata=metadata or {}
)
# Check if Email is enabled
email_config = settings.NOTIFICATION_CHANNELS.get('email', {})
if not email_config.get('enabled', True):
logger.info(f"[Email Console] To: {email} | Subject: {subject} | Message: {message}")
log.mark_sent()
return log
# Send email using Django's email backend
try:
send_mail(
subject=subject,
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[email],
html_message=html_message,
fail_silently=False
)
log.mark_sent()
logger.info(f"Email sent to {email}: {subject}")
except Exception as e:
log.mark_failed(str(e))
logger.error(f"Failed to send email to {email}: {str(e)}")
return log
@staticmethod
def send_notification(recipient, title, message, notification_type='general', related_object=None, metadata=None):
"""
Send generic notification to a user.
This method determines the best channel to use based on recipient preferences
or defaults to email.
Args:
recipient: User object
title: Notification title
message: Notification message
notification_type: Type of notification (e.g., 'complaint', 'survey', 'general')
related_object: Related model instance (optional)
metadata: Additional metadata dict (optional)
Returns:
NotificationLog instance
"""
# Determine the recipient's contact information
recipient_email = recipient.email if hasattr(recipient, 'email') else None
recipient_phone = recipient.phone if hasattr(recipient, 'phone') else None
# Try email first (most reliable)
if recipient_email:
try:
return NotificationService.send_email(
email=recipient_email,
subject=title,
message=message,
related_object=related_object,
metadata={
'notification_type': notification_type,
**(metadata or {})
}
)
except Exception as e:
logger.warning(f"Failed to send email notification to {recipient_email}: {str(e)}")
# Fallback to SMS if email failed or not available
if recipient_phone:
try:
# Combine title and message for SMS
sms_message = f"{title}\n\n{message}"
return NotificationService.send_sms(
phone=recipient_phone,
message=sms_message,
related_object=related_object,
metadata={
'notification_type': notification_type,
**(metadata or {})
}
)
except Exception as e:
logger.warning(f"Failed to send SMS notification to {recipient_phone}: {str(e)}")
# If all channels failed, log a console notification
logger.warning(
f"Could not send notification to {recipient}. "
f"No valid contact channels available. "
f"Title: {title}, Message: {message}"
)
# Create a log entry even if we couldn't send
return NotificationLog.objects.create(
channel='console',
recipient=str(recipient),
subject=title,
message=message,
content_object=related_object,
provider='console',
metadata={
'notification_type': notification_type,
**(metadata or {})
}
)
@staticmethod
def send_survey_invitation(survey_instance, language='en'):
"""
Send survey invitation to patient.
Args:
survey_instance: SurveyInstance object
language: Language code ('en' or 'ar')
Returns:
NotificationLog instance
"""
patient = survey_instance.patient
survey_url = survey_instance.get_survey_url()
# Determine recipient based on delivery channel
if survey_instance.delivery_channel == 'sms':
recipient = survey_instance.recipient_phone or patient.phone
if language == 'ar':
message = f"عزيزي {patient.get_full_name()},\n\nنرجو منك إكمال استبيان تجربتك:\n{survey_url}"
else:
message = f"Dear {patient.get_full_name()},\n\nPlease complete your experience survey:\n{survey_url}"
return NotificationService.send_sms(
phone=recipient,
message=message,
related_object=survey_instance,
metadata={'survey_id': str(survey_instance.id), 'language': language}
)
elif survey_instance.delivery_channel == 'whatsapp':
recipient = survey_instance.recipient_phone or patient.phone
if language == 'ar':
message = f"عزيزي {patient.get_full_name()},\n\nنرجو منك إكمال استبيان تجربتك:\n{survey_url}"
else:
message = f"Dear {patient.get_full_name()},\n\nPlease complete your experience survey:\n{survey_url}"
return NotificationService.send_whatsapp(
phone=recipient,
message=message,
related_object=survey_instance,
metadata={'survey_id': str(survey_instance.id), 'language': language}
)
else: # email
recipient = survey_instance.recipient_email or patient.email
if language == 'ar':
subject = f"استبيان تجربتك - {survey_instance.survey_template.name_ar or survey_instance.survey_template.name}"
message = f"عزيزي {patient.get_full_name()},\n\nنرجو منك إكمال استبيان تجربتك:\n{survey_url}"
else:
subject = f"Your Experience Survey - {survey_instance.survey_template.name}"
message = f"Dear {patient.get_full_name()},\n\nPlease complete your experience survey:\n{survey_url}"
return NotificationService.send_email(
email=recipient,
subject=subject,
message=message,
related_object=survey_instance,
metadata={'survey_id': str(survey_instance.id), 'language': language}
)
# Convenience functions
def send_sms(phone, message, **kwargs):
"""Send SMS notification"""
return NotificationService.send_sms(phone, message, **kwargs)
def send_whatsapp(phone, message, **kwargs):
"""Send WhatsApp notification"""
return NotificationService.send_whatsapp(phone, message, **kwargs)
def send_email(email, subject, message, **kwargs):
"""Send Email notification"""
return NotificationService.send_email(email, subject, message, **kwargs)