""" Core Celery tasks for notifications and general utilities. This module contains shared tasks for sending emails, SMS, WhatsApp messages, and creating in-app notifications. """ import logging from typing import Dict, List, Optional from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage, send_mail from django.template.loader import render_to_string from django.utils.html import strip_tags logger = logging.getLogger(__name__) @shared_task(bind=True, max_retries=3) def send_email_task( self, subject: str, message: str, recipient_list: List[str], from_email: Optional[str] = None, html_message: Optional[str] = None, fail_silently: bool = False, ) -> bool: """ Send an email asynchronously. Args: subject: Email subject message: Plain text message recipient_list: List of recipient email addresses from_email: Sender email (defaults to DEFAULT_FROM_EMAIL) html_message: HTML version of the message fail_silently: Whether to suppress exceptions Returns: bool: True if email was sent successfully """ try: from_email = from_email or settings.DEFAULT_FROM_EMAIL send_mail( subject=subject, message=message, from_email=from_email, recipient_list=recipient_list, html_message=html_message, fail_silently=fail_silently, ) logger.info(f"Email sent successfully to {recipient_list}") return True except Exception as exc: logger.error(f"Failed to send email: {exc}") if not fail_silently: raise self.retry(exc=exc, countdown=60) return False @shared_task(bind=True, max_retries=3) def send_template_email_task( self, subject: str, template_name: str, context: Dict, recipient_list: List[str], from_email: Optional[str] = None, ) -> bool: """ Send an email using a Django template. Args: subject: Email subject template_name: Path to email template context: Template context dictionary recipient_list: List of recipient email addresses from_email: Sender email (defaults to DEFAULT_FROM_EMAIL) Returns: bool: True if email was sent successfully """ try: from_email = from_email or settings.DEFAULT_FROM_EMAIL # Render HTML content html_content = render_to_string(template_name, context) text_content = strip_tags(html_content) # Create email message email = EmailMessage( subject=subject, body=text_content, from_email=from_email, to=recipient_list, ) email.content_subtype = 'html' email.send() logger.info(f"Template email sent successfully to {recipient_list}") return True except Exception as exc: logger.error(f"Failed to send template email: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task(bind=True, max_retries=3) def send_sms_task(self, phone_number: str, message: str) -> bool: """ Send an SMS message using Twilio. Args: phone_number: Recipient phone number (E.164 format) message: SMS message content Returns: bool: True if SMS was sent successfully """ try: # Check if Twilio is configured if not settings.TWILIO_ACCOUNT_SID or not settings.TWILIO_AUTH_TOKEN: logger.warning("Twilio not configured, skipping SMS") return False 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_number, ) logger.info(f"SMS sent successfully to {phone_number}, SID: {message.sid}") return True except Exception as exc: logger.error(f"Failed to send SMS to {phone_number}: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task(bind=True, max_retries=3) def send_whatsapp_task(self, phone_number: str, message: str) -> bool: """ Send a WhatsApp message using Twilio. Args: phone_number: Recipient phone number (E.164 format) message: WhatsApp message content Returns: bool: True if message was sent successfully """ try: # Check if Twilio is configured if not settings.TWILIO_ACCOUNT_SID or not settings.TWILIO_AUTH_TOKEN: logger.warning("Twilio not configured, skipping WhatsApp") return False from twilio.rest import Client client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN) # Format phone number for WhatsApp whatsapp_to = f"whatsapp:{phone_number}" message = client.messages.create( body=message, from_=settings.TWILIO_WHATSAPP_NUMBER, to=whatsapp_to, ) logger.info(f"WhatsApp sent successfully to {phone_number}, SID: {message.sid}") return True except Exception as exc: logger.error(f"Failed to send WhatsApp to {phone_number}: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task(bind=True, max_retries=3) def create_notification_task( self, user_id: str, title: str, message: str, notification_type: str = 'INFO', related_object_type: Optional[str] = None, related_object_id: Optional[str] = None, ) -> bool: """ Create an in-app notification for a user. Args: user_id: UUID of the user to notify title: Notification title message: Notification message notification_type: Type of notification (INFO, WARNING, ERROR, SUCCESS) related_object_type: Type of related object (e.g., 'appointment', 'invoice') related_object_id: UUID of related object Returns: bool: True if notification was created successfully """ try: from notifications.models import Notification from core.models import User user = User.objects.get(id=user_id) notification = Notification.objects.create( user=user, title=title, message=message, notification_type=notification_type, related_object_type=related_object_type, related_object_id=related_object_id, ) logger.info(f"Notification created for user {user_id}: {title}") return True except Exception as exc: logger.error(f"Failed to create notification: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task(bind=True, max_retries=3) def send_multi_channel_notification_task( self, user_id: str, title: str, message: str, channels: List[str] = None, email_subject: Optional[str] = None, email_template: Optional[str] = None, email_context: Optional[Dict] = None, ) -> Dict[str, bool]: """ Send notification through multiple channels (email, SMS, WhatsApp, in-app). Args: user_id: UUID of the user to notify title: Notification title message: Notification message channels: List of channels to use ('email', 'sms', 'whatsapp', 'in_app') email_subject: Subject for email (if email channel is used) email_template: Template for email (if email channel is used) email_context: Context for email template Returns: dict: Status of each channel (True/False) """ from core.models import User if channels is None: channels = ['in_app'] results = {} try: user = User.objects.get(id=user_id) # In-app notification if 'in_app' in channels: results['in_app'] = create_notification_task.delay( user_id=str(user.id), title=title, message=message, ).get() # Email notification if 'email' in channels and user.email: if email_template and email_context: results['email'] = send_template_email_task.delay( subject=email_subject or title, template_name=email_template, context=email_context, recipient_list=[user.email], ).get() else: results['email'] = send_email_task.delay( subject=email_subject or title, message=message, recipient_list=[user.email], ).get() # SMS notification if 'sms' in channels and user.phone: results['sms'] = send_sms_task.delay( phone_number=str(user.phone), message=f"{title}: {message}", ).get() # WhatsApp notification if 'whatsapp' in channels and user.phone: results['whatsapp'] = send_whatsapp_task.delay( phone_number=str(user.phone), message=f"{title}: {message}", ).get() logger.info(f"Multi-channel notification sent to user {user_id}: {results}") return results except Exception as exc: logger.error(f"Failed to send multi-channel notification: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task def cleanup_old_notifications(days: int = 90) -> int: """ Clean up old read notifications. Args: days: Number of days to keep notifications Returns: int: Number of notifications deleted """ from datetime import timedelta from django.utils import timezone from notifications.models import Notification cutoff_date = timezone.now() - timedelta(days=days) deleted_count, _ = Notification.objects.filter( is_read=True, created_at__lt=cutoff_date, ).delete() logger.info(f"Cleaned up {deleted_count} old notifications") return deleted_count