""" Notification Settings Service Integrates notification settings with the notification service. Checks settings before sending notifications. """ import logging from functools import wraps from datetime import datetime, timedelta from celery import shared_task from django.utils import timezone from .settings_models import HospitalNotificationSettings logger = logging.getLogger(__name__) def _get_quiet_hours_end_datetime(settings): now = timezone.now() end_time = settings.quiet_hours_end target = now.replace(hour=end_time.hour, minute=end_time.minute, second=0, microsecond=0) if target <= now: target += timedelta(days=1) return target @shared_task def _send_deferred_sms(phone, message, related_object_app=None, related_object_model=None, related_object_id=None): from .services import NotificationService related_object = None if related_object_app and related_object_model and related_object_id: from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get(app_label=related_object_app, model=related_object_model) related_object = ct.get_object_for_this_type(id=related_object_id) NotificationService.send_sms(phone=phone, message=message, related_object=related_object) logger.info(f"Deferred SMS sent to {phone}") @shared_task def _send_deferred_whatsapp(phone, message, related_object_app=None, related_object_model=None, related_object_id=None): from .services import NotificationService related_object = None if related_object_app and related_object_model and related_object_id: from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get(app_label=related_object_app, model=related_object_model) related_object = ct.get_object_for_this_type(id=related_object_id) NotificationService.send_whatsapp(phone=phone, message=message, related_object=related_object) logger.info(f"Deferred WhatsApp sent to {phone}") def _serialize_related_object(obj): if obj is None: return None, None, None from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get_for_model(obj) return ct.app_label, ct.model, str(obj.pk) def check_notification_settings(event, hospital_id=None): """ Decorator to check notification settings before sending. Usage: @check_notification_settings('explanation_requested', 'hospital_id') def send_explanation_request(..., hospital_id=None): ... """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Extract hospital_id from kwargs or args hid = kwargs.get(hospital_id) if hid is None and len(args) > 0: # Try to find hospital_id in args based on function signature import inspect sig = inspect.signature(func) params = list(sig.parameters.keys()) if hospital_id in params: idx = params.index(hospital_id) if idx < len(args): hid = args[idx] if hid is None: # Try to get from related_object related_object = kwargs.get("related_object") if related_object and hasattr(related_object, "hospital_id"): hid = related_object.hospital_id if hid: settings = HospitalNotificationSettings.get_for_hospital(hid) # Check if notifications are enabled if not settings.notifications_enabled: logger.info(f"Notifications disabled for hospital {hid}") return None # Check if this event is enabled for the channel # Channel is determined by the function name or a 'channel' kwarg channel = kwargs.get("channel", "email") if "sms" in func.__name__: channel = "sms" elif "whatsapp" in func.__name__: channel = "whatsapp" elif "email" in func.__name__: channel = "email" if not settings.is_channel_enabled(event, channel): logger.info(f"Notification {event} via {channel} disabled for hospital {hid}") return None if channel in ["sms", "whatsapp"] and settings.is_quiet_hours(): eta = _get_quiet_hours_end_datetime(settings) logger.info(f"Notification {event} deferred to {eta} due to quiet hours for hospital {hid}") return eta return func(*args, **kwargs) return wrapper return decorator class NotificationServiceWithSettings: """ Wrapper around NotificationService that respects hospital settings. Usage: from apps.notifications.settings_service import NotificationServiceWithSettings NotificationServiceWithSettings.send_explanation_requested( staff_email, complaint, hospital_id=hospital.id ) """ @staticmethod def _defer_sms_if_quiet_hours(settings, phone, message, related_object): from .services import NotificationService if settings.is_quiet_hours(): eta = _get_quiet_hours_end_datetime(settings) app, model, obj_id = _serialize_related_object(related_object) _send_deferred_sms.apply_async( eta=eta, kwargs={ "phone": phone, "message": message, "related_object_app": app, "related_object_model": model, "related_object_id": obj_id, }, ) return ("sms_deferred", eta.isoformat()) result = NotificationService.send_sms(phone=phone, message=message, related_object=related_object) return ("sms", result) @staticmethod def _defer_whatsapp_if_quiet_hours(settings, phone, message, related_object): from .services import NotificationService if settings.is_quiet_hours(): eta = _get_quiet_hours_end_datetime(settings) app, model, obj_id = _serialize_related_object(related_object) _send_deferred_whatsapp.apply_async( eta=eta, kwargs={ "phone": phone, "message": message, "related_object_app": app, "related_object_model": model, "related_object_id": obj_id, }, ) return ("whatsapp_deferred", eta.isoformat()) result = NotificationService.send_whatsapp(phone=phone, message=message, related_object=related_object) return ("whatsapp", result) @staticmethod def _get_hospital_id_from_complaint(complaint): """Extract hospital_id from complaint object""" if hasattr(complaint, "hospital_id"): return complaint.hospital_id return None @staticmethod def send_explanation_requested(staff_email, complaint, custom_message=None): """ Send explanation request notification to staff. Respects hospital notification settings. """ from .services import NotificationService # Build HTML with navy theme staff_name = getattr(complaint.assigned_to, 'get_full_name', 'Staff Member')() if hasattr(complaint, 'assigned_to') and complaint.assigned_to else 'Staff Member' html_message = f"""

Explanation Requested

Dear {staff_name},

An explanation has been requested for complaint #{complaint.id}.

Title: {getattr(complaint, 'title', 'N/A')}

Deadline: Please submit as soon as possible

{f'

Note: {custom_message}

' if custom_message else ''}
""" hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) if not hospital_id: # Fallback: send without settings check return NotificationService.send_email( email=staff_email, subject=f"Explanation Request - Complaint #{complaint.id}", message=custom_message or f"Please provide explanation for complaint #{complaint.id}", html_message=html_message, related_object=complaint, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] # Check master switch if not settings.notifications_enabled: logger.info(f"Notifications disabled for hospital {hospital_id}") return results # Email if settings.explanation_requested_email: result = NotificationService.send_email( email=staff_email, subject=f"Explanation Request - Complaint #{complaint.id}", message=custom_message or f"Please provide explanation for complaint #{complaint.id}", html_message=html_message, related_object=complaint, ) results.append(("email", result)) if ( settings.explanation_requested_sms and hasattr(complaint, "staff") and complaint.staff and complaint.staff.phone ): results.append( NotificationServiceWithSettings._defer_sms_if_quiet_hours( settings, complaint.staff.phone, f"PX360: Explanation requested for Complaint #{complaint.id}. Check your email for details.", complaint, ) ) if ( settings.explanation_requested_whatsapp and hasattr(complaint, "staff") and complaint.staff and complaint.staff.phone ): results.append( NotificationServiceWithSettings._defer_whatsapp_if_quiet_hours( settings, complaint.staff.phone, f"PX360: Explanation requested for Complaint #{complaint.id}. Check your email for details.", complaint, ) ) return results @staticmethod def send_explanation_reminder(staff_email, complaint): """Send explanation reminder notification""" from .services import NotificationService hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) if not hospital_id: return NotificationService.send_email( email=staff_email, subject=f"Reminder: Explanation Due - Complaint #{complaint.id}", message=f"Reminder: Please submit your explanation for complaint #{complaint.id} within 24 hours.", related_object=complaint, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.explanation_reminder_email: result = NotificationService.send_email( email=staff_email, subject=f"Reminder: Explanation Due - Complaint #{complaint.id}", message=f"Reminder: Please submit your explanation for complaint #{complaint.id} within 24 hours.", related_object=complaint, ) results.append(("email", result)) if ( settings.explanation_reminder_sms and hasattr(complaint, "staff") and complaint.staff and complaint.staff.phone ): results.append( NotificationServiceWithSettings._defer_sms_if_quiet_hours( settings, complaint.staff.phone, f"PX360 Reminder: Explanation due for Complaint #{complaint.id} in 24h.", complaint, ) ) return results @staticmethod def send_explanation_overdue(manager_email, complaint, staff_name): """Send explanation overdue/escalation notification to manager""" from .services import NotificationService hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) if not hospital_id: return NotificationService.send_email( email=manager_email, subject=f"ESCALATION: Explanation Overdue - Complaint #{complaint.id}", message=f"Staff member {staff_name} has not submitted explanation for complaint #{complaint.id}.", related_object=complaint, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.explanation_overdue_email: result = NotificationService.send_email( email=manager_email, subject=f"ESCALATION: Explanation Overdue - Complaint #{complaint.id}", message=f"Staff member {staff_name} has not submitted explanation for complaint #{complaint.id}.", related_object=complaint, ) results.append(("email", result)) if settings.explanation_overdue_sms: # Get manager phone if available result = NotificationService.send_sms( phone=manager_email, # This would need actual phone lookup message=f"PX360 ESCALATION: Explanation overdue for Complaint #{complaint.id} by {staff_name}", related_object=complaint, ) results.append(("sms", result)) return results @staticmethod def send_explanation_received(assignee_email, complaint, explanation): """Send notification when explanation is received""" from .services import NotificationService hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) if not hospital_id: return NotificationService.send_email( email=assignee_email, subject=f"New Explanation Received - Complaint #{complaint.id}", message=f"A new explanation has been submitted for complaint #{complaint.id}.", related_object=complaint, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.explanation_received_email: result = NotificationService.send_email( email=assignee_email, subject=f"New Explanation Received - Complaint #{complaint.id}", message=f"A new explanation has been submitted for complaint #{complaint.id}.", related_object=complaint, ) results.append(("email", result)) return results @staticmethod def send_complaint_assigned(assignee_email, complaint): """Send notification when complaint is assigned""" from django.template.loader import render_to_string from .services import NotificationService # Build template context context = { 'assignee_name': getattr(complaint.assigned_to, 'get_full_name', 'Staff Member')() if hasattr(complaint, 'assigned_to') and complaint.assigned_to else 'Staff Member', 'complaint_id': complaint.id, 'complaint_title': getattr(complaint, 'title', 'N/A'), 'department': getattr(complaint.department, 'name_en', 'N/A') if hasattr(complaint, 'department') and complaint.department else 'N/A', } # Render HTML template with navy theme html_message = f"""

Complaint Assigned to You

Dear {context['assignee_name']},

A complaint has been assigned to you for review and response.

Complaint: #{context['complaint_id']} - {context['complaint_title']}
Department: {context['department']}

Please review and provide your explanation.

""" hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) if not hospital_id: return NotificationService.send_email( email=assignee_email, subject=f"Complaint Assigned - #{complaint.id}", message=f"You have been assigned to complaint #{complaint.id}: {complaint.title}", html_message=html_message, related_object=complaint, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.complaint_assigned_email: result = NotificationService.send_email( email=assignee_email, subject=f"Complaint Assigned - #{complaint.id}", message=f"You have been assigned to complaint #{complaint.id}: {complaint.title}", html_message=html_message, related_object=complaint, ) results.append(("email", result)) return results @staticmethod def send_onboarding_invitation(user_email, provisional_user, custom_message=None): """ Send onboarding invitation to new provisional user. Respects hospital notification settings. """ from .services import NotificationService hospital_id = getattr(provisional_user, "hospital_id", None) if not hospital_id: # Fallback: send without settings check return NotificationService.send_email( email=user_email, subject="Welcome to PX360 - Complete Your Registration", message=custom_message or f"Please complete your registration at PX360.", related_object=provisional_user, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] # Check master switch if not settings.notifications_enabled: logger.info(f"Notifications disabled for hospital {hospital_id}") return results # Email invitation if settings.onboarding_invitation_email: result = NotificationService.send_email( email=user_email, subject="Welcome to PX360 - Complete Your Registration", message=custom_message or f"Please complete your registration at PX360.", related_object=provisional_user, ) results.append(("email", result)) if settings.onboarding_invitation_sms and provisional_user.phone: results.append( NotificationServiceWithSettings._defer_sms_if_quiet_hours( settings, provisional_user.phone, "Welcome to PX360! Check your email to complete your registration.", provisional_user, ) ) return results @staticmethod def send_onboarding_reminder(user_email, provisional_user): """Send onboarding reminder notification""" from .services import NotificationService hospital_id = getattr(provisional_user, "hospital_id", None) if not hospital_id: return NotificationService.send_email( email=user_email, subject="Reminder: Complete Your PX360 Registration", message="Please complete your PX360 registration. Your invitation will expire soon.", related_object=provisional_user, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.onboarding_reminder_email: result = NotificationService.send_email( email=user_email, subject="Reminder: Complete Your PX360 Registration", message="Please complete your PX360 registration. Your invitation will expire soon.", related_object=provisional_user, ) results.append(("email", result)) if settings.onboarding_reminder_sms and provisional_user.phone: results.append( NotificationServiceWithSettings._defer_sms_if_quiet_hours( settings, provisional_user.phone, "Reminder: Complete your PX360 registration. Invitation expires soon.", provisional_user, ) ) return results @staticmethod def send_onboarding_completion_notification(admin_email, completed_user): """Send notification to admin when user completes onboarding""" from .services import NotificationService hospital_id = getattr(completed_user, "hospital_id", None) if not hospital_id: return NotificationService.send_email( email=admin_email, subject=f"Onboarding Complete - {completed_user.get_full_name()}", message=f"{completed_user.get_full_name()} has completed their onboarding.", related_object=completed_user, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.onboarding_completion_email: result = NotificationService.send_email( email=admin_email, subject=f"Onboarding Complete - {completed_user.get_full_name()}", message=f"{completed_user.get_full_name()} has completed their onboarding.", related_object=completed_user, ) results.append(("email", result)) return results @staticmethod def send_complaint_status_changed(recipient_email, complaint, old_status, new_status): """Send notification when complaint status changes""" from .services import NotificationService # Build HTML with navy theme html_message = f"""

Complaint Status Updated

Dear Stakeholder,

The status of complaint #{complaint.id} has been updated.

Previous Status: {old_status}

New Status: {new_status}

""" hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) if not hospital_id: return NotificationService.send_email( email=recipient_email, subject=f"Complaint Status Updated - #{complaint.id}", message=f"Complaint #{complaint.id} status changed from {old_status} to {new_status}.", html_message=html_message, related_object=complaint, ) settings = HospitalNotificationSettings.get_for_hospital(hospital_id) results = [] if not settings.notifications_enabled: return results if settings.complaint_status_changed_email: result = NotificationService.send_email( email=recipient_email, subject=f"Complaint Status Updated - #{complaint.id}", message=f"Complaint #{complaint.id} status changed from {old_status} to {new_status}.", html_message=html_message, related_object=complaint, ) results.append(("email", result)) return results