""" 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.db import models 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"""

Department Response Requested

Dear {staff_name},

A department response 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"Department Response Request - Complaint #{complaint.id}", message=custom_message or f"Please provide department response 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"Department Response Request - Complaint #{complaint.id}", message=custom_message or f"Please provide department response for complaint #{complaint.id}", html_message=html_message, related_object=complaint, ) results.append(("email", result)) return results @staticmethod def send_inquiry_department_assigned(department, inquiry, context_note_en="", context_note_ar="", recipient_type="staff"): from apps.accounts.models import User from .services import NotificationService context_note_parts = [] if context_note_en: context_note_parts.append(context_note_en) if context_note_ar: context_note_parts.append(context_note_ar) has_context = bool(context_note_parts) note_html = "" if has_context: note_section = "
".join(context_note_parts) note_html = f"""
Context / Summary:
{note_section}
""" plain_note = " ".join(context_note_parts) plain_message = f"A new inquiry has been assigned to {department.get_localized_name()}: {inquiry.subject}" if plain_note: plain_message += f"\n\nContext: {plain_note}" if recipient_type == "department_email": if department.email: html_message = f"""

New Inquiry Assigned to Your Department

Dear {department.get_localized_name()} Team,

A new inquiry has been assigned to the {department.get_localized_name()} department for response.

Reference: {inquiry.reference_number}
Subject: {inquiry.subject}
Category: {inquiry.get_category_display()}
Priority: {inquiry.get_priority_display()}
{note_html}

Please log in to review and respond to this inquiry.

""" NotificationService.send_email( email=department.email, subject=f"New Inquiry for {department.get_localized_name()} - {inquiry.reference_number}", message=plain_message, html_message=html_message, related_object=inquiry, ) else: respondents = User.objects.filter( is_active=True, department=department, ).filter( models.Q(groups__name="Champion") | models.Q(groups__name="Department Manager") ).distinct() for user in respondents: html_message = f"""

New Inquiry Assigned to Your Department

Dear {user.get_full_name()},

A new inquiry has been assigned to the {department.get_localized_name()} department for response.

Reference: {inquiry.reference_number}
Subject: {inquiry.subject}
Category: {inquiry.get_category_display()}
Priority: {inquiry.get_priority_display()}
{note_html}

Please log in to review and respond to this inquiry.

""" if user.email: NotificationService.send_email( email=user.email, subject=f"New Inquiry for {department.get_localized_name()} - {inquiry.reference_number}", message=plain_message, html_message=html_message, related_object=inquiry, ) from .services import create_in_app_notification in_app_msg = f"Inquiry {inquiry.reference_number}: {inquiry.subject[:100]}" in_app_msg_ar = f"استفسار {inquiry.reference_number}: {inquiry.subject[:100]}" if context_note_en: in_app_msg += f" — {context_note_en[:80]}" if context_note_ar: in_app_msg_ar += f" — {context_note_ar[:80]}" create_in_app_notification( user=user, title=f"New inquiry for {department.get_localized_name()}", title_ar=f"استفسار جديد لقسم {department.name_ar or department.get_localized_name()}", message=in_app_msg, message_ar=in_app_msg_ar, notification_type="inquiry_assigned", action_url=f"/inquiries/{inquiry.pk}/department-response/", ) @staticmethod def send_observation_department_assigned(department, observation, context_note_en="", context_note_ar="", recipient_type="staff"): from apps.accounts.models import User from .services import NotificationService context_note_parts = [] if context_note_en: context_note_parts.append(context_note_en) if context_note_ar: context_note_parts.append(context_note_ar) has_context = bool(context_note_parts) note_html = "" if has_context: note_section = "
".join(context_note_parts) note_html = f"""
Context / Summary:
{note_section}
""" plain_note = " ".join(context_note_parts) tracking = observation.tracking_code or str(observation.pk)[:8] plain_message = f"A new observation has been assigned to {department.get_localized_name()}: {observation.title or observation.description[:80]}" if plain_note: plain_message += f"\n\nContext: {plain_note}" if recipient_type == "department_email": if department.email: html_message = f"""

New Observation Assigned to Your Department

Dear {department.get_localized_name()} Team,

A new observation has been assigned to the {department.get_localized_name()} department for response.

Tracking: {tracking}
Title: {observation.title or 'N/A'}
Severity: {observation.get_severity_display()}
Category: {observation.category.name_en if observation.category else 'N/A'}
{note_html}

Please log in to review and respond to this observation.

""" NotificationService.send_email( email=department.email, subject=f"New Observation for {department.get_localized_name()} - {tracking}", message=plain_message, html_message=html_message, related_object=observation, ) else: respondents = User.objects.filter( is_active=True, department=department, ).filter( models.Q(groups__name="Champion") | models.Q(groups__name="Department Manager") ).distinct() for user in respondents: html_message = f"""

New Observation Assigned to Your Department

Dear {user.get_full_name()},

A new observation has been assigned to the {department.get_localized_name()} department for response.

Tracking: {tracking}
Title: {observation.title or 'N/A'}
Severity: {observation.get_severity_display()}
Category: {observation.category.name_en if observation.category else 'N/A'}
{note_html}

Please log in to review and respond to this observation.

""" if user.email: NotificationService.send_email( email=user.email, subject=f"New Observation for {department.get_localized_name()} - {tracking}", message=plain_message, html_message=html_message, related_object=observation, ) from .services import create_in_app_notification in_app_msg = f"Observation {tracking}: {(observation.title or observation.description[:80])[:100]}" in_app_msg_ar = f"ملاحظة {tracking}: {(observation.title or observation.description[:80])[:100]}" if context_note_en: in_app_msg += f" — {context_note_en[:80]}" if context_note_ar: in_app_msg_ar += f" — {context_note_ar[:80]}" create_in_app_notification( user=user, title=f"New observation for {department.get_localized_name()}", title_ar=f"ملاحظة جديدة لقسم {department.name_ar or department.get_localized_name()}", message=in_app_msg, message_ar=in_app_msg_ar, notification_type="observation_assigned", action_url=f"/observations/{observation.pk}/department-response/", ) @staticmethod def send_inquiry_assigned(inquiry): from .services import NotificationService, create_in_app_notification if not inquiry.assigned_to: return if inquiry.assigned_to.email: NotificationService.send_email( email=inquiry.assigned_to.email, subject=f"Inquiry Assigned to You - {inquiry.reference_number}", message=f"You have been assigned inquiry {inquiry.reference_number}: {inquiry.subject}", related_object=inquiry, ) create_in_app_notification( user=inquiry.assigned_to, title=f"Inquiry assigned to you", title_ar="تم تعيين استفسار لك", message=f"{inquiry.reference_number}: {inquiry.subject[:100]}", message_ar=f"{inquiry.reference_number}: {inquiry.subject[:100]}", notification_type="inquiry_assigned", action_url=f"/inquiries/{inquiry.pk}/", ) @staticmethod def send_inquiry_resolved(inquiry): from .services import NotificationService, create_in_app_notification from apps.accounts.models import User hospital_id = str(inquiry.hospital_id) if inquiry.hospital else None if hospital_id: admins = User.objects.filter( is_active=True, hospital_id=hospital_id ).filter( models.Q(groups__name="PX Admin") | models.Q(groups__name="Hospital Admin") ).distinct() else: admins = User.objects.none() for admin in admins: create_in_app_notification( user=admin, title=f"Inquiry resolved: {inquiry.reference_number}", title_ar=f"تم حل الاستفسار: {inquiry.reference_number}", message=f"{inquiry.subject[:100]}", message_ar=f"{inquiry.subject[:100]}", notification_type="inquiry_resolved", action_url=f"/inquiries/{inquiry.pk}/", ) @staticmethod def send_inquiry_reopened(inquiry): from .services import NotificationService, create_in_app_notification recipients = [] if inquiry.assigned_to: recipients.append(inquiry.assigned_to) from apps.accounts.models import User if inquiry.hospital: admins = User.objects.filter( is_active=True, hospital=inquiry.hospital ).filter( models.Q(groups__name="PX Admin") | models.Q(groups__name="Hospital Admin") ).distinct() recipients.extend(admins) for user in set(recipients): create_in_app_notification( user=user, title=f"Inquiry reopened: {inquiry.reference_number}", title_ar=f"تم إعادة فتح الاستفسار: {inquiry.reference_number}", message=f"{inquiry.subject[:100]}", message_ar=f"{inquiry.subject[:100]}", notification_type="inquiry_reopened", action_url=f"/inquiries/{inquiry.pk}/", ) @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