""" 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 """ # Check if SMS API is enabled and use it (simulator or external API) sms_api_config = settings.EXTERNAL_NOTIFICATION_API.get('sms', {}) if sms_api_config.get('enabled', False): return NotificationService.send_sms_via_api( message=message, phone=phone, related_object=related_object, metadata=metadata ) # 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 """ # Check if Email API is enabled and use it (simulator or external API) email_api_config = settings.EXTERNAL_NOTIFICATION_API.get('email', {}) if email_api_config.get('enabled', False): return NotificationService.send_email_via_api( message=message, email=email, subject=subject, html_message=html_message, related_object=related_object, metadata=metadata ) # 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_email_via_api(message, email, subject, html_message=None, related_object=None, metadata=None): """ Send email via external API endpoint with retry logic. Args: message: Email message (plain text) email: Recipient email address subject: Email subject html_message: Email message (HTML) (optional) related_object: Related model instance (optional) metadata: Additional metadata dict (optional) Returns: NotificationLog instance """ import requests import time # Check if enabled email_config = settings.EXTERNAL_NOTIFICATION_API.get('email', {}) if not email_config.get('enabled', False): logger.warning("Email API is disabled. Skipping send_email_via_api") return None # Create notification log log = NotificationLog.objects.create( channel='email', recipient=email, subject=subject, message=message, content_object=related_object, provider='api', metadata={ 'api_url': email_config.get('url'), 'auth_method': email_config.get('auth_method'), **(metadata or {}) } ) # Prepare request payload payload = { 'to': email, 'subject': subject, 'message': message, } if html_message: payload['html_message'] = html_message # Prepare headers headers = {'Content-Type': 'application/json'} api_key = email_config.get('api_key', '') auth_method = email_config.get('auth_method', 'bearer') if auth_method == 'bearer': headers['Authorization'] = f'Bearer {api_key}' elif auth_method == 'api_key': headers['X-API-KEY'] = api_key # Retry logic max_retries = email_config.get('max_retries', 3) retry_delay = email_config.get('retry_delay', 2) timeout = email_config.get('timeout', 10) for attempt in range(max_retries): try: logger.info(f"Sending email via API (attempt {attempt + 1}/{max_retries}) to {email}") response = requests.post( email_config.get('url'), json=payload, headers=headers, timeout=timeout ) # API runs in background, accept any 2xx response if 200 <= response.status_code < 300: log.mark_sent() logger.info(f"Email sent via API to {email}: {subject}") return log else: logger.warning(f"API returned status {response.status_code}") if attempt == max_retries - 1: log.mark_failed(f"API returned status {response.status_code}") continue except requests.exceptions.Timeout: logger.warning(f"Timeout on attempt {attempt + 1}") if attempt == max_retries - 1: log.mark_failed("Request timeout") except requests.exceptions.ConnectionError: logger.warning(f"Connection error on attempt {attempt + 1}") if attempt == max_retries - 1: log.mark_failed("Connection error") except Exception as e: logger.error(f"Unexpected error: {str(e)}") if attempt == max_retries - 1: log.mark_failed(str(e)) # Wait before retry (exponential backoff) if attempt < max_retries - 1: time.sleep(retry_delay * (2 ** attempt)) return log @staticmethod def send_sms_via_api(message, phone, related_object=None, metadata=None): """ Send SMS via external API endpoint with retry logic. Args: message: SMS message text phone: Recipient phone number related_object: Related model instance (optional) metadata: Additional metadata dict (optional) Returns: NotificationLog instance """ import requests import time # Check if enabled sms_config = settings.EXTERNAL_NOTIFICATION_API.get('sms', {}) if not sms_config.get('enabled', False): logger.warning("SMS API is disabled. Skipping send_sms_via_api") return None # Create notification log log = NotificationLog.objects.create( channel='sms', recipient=phone, message=message, content_object=related_object, provider='api', metadata={ 'api_url': sms_config.get('url'), 'auth_method': sms_config.get('auth_method'), **(metadata or {}) } ) # Prepare request payload payload = { 'to': phone, 'message': message, } # Prepare headers headers = {'Content-Type': 'application/json'} api_key = sms_config.get('api_key', '') auth_method = sms_config.get('auth_method', 'bearer') if auth_method == 'bearer': headers['Authorization'] = f'Bearer {api_key}' elif auth_method == 'api_key': headers['X-API-KEY'] = api_key # Retry logic max_retries = sms_config.get('max_retries', 3) retry_delay = sms_config.get('retry_delay', 2) timeout = sms_config.get('timeout', 10) for attempt in range(max_retries): try: logger.info(f"Sending SMS via API (attempt {attempt + 1}/{max_retries}) to {phone}") response = requests.post( sms_config.get('url'), json=payload, headers=headers, timeout=timeout ) # API runs in background, accept any 2xx response if 200 <= response.status_code < 300: log.mark_sent() logger.info(f"SMS sent via API to {phone}") return log else: logger.warning(f"API returned status {response.status_code}") if attempt == max_retries - 1: log.mark_failed(f"API returned status {response.status_code}") continue except requests.exceptions.Timeout: logger.warning(f"Timeout on attempt {attempt + 1}") if attempt == max_retries - 1: log.mark_failed("Request timeout") except requests.exceptions.ConnectionError: logger.warning(f"Connection error on attempt {attempt + 1}") if attempt == max_retries - 1: log.mark_failed("Connection error") except Exception as e: logger.error(f"Unexpected error: {str(e)}") if attempt == max_retries - 1: log.mark_failed(str(e)) # Wait before retry (exponential backoff) if attempt < max_retries - 1: time.sleep(retry_delay * (2 ** attempt)) 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)