341 lines
10 KiB
Python
341 lines
10 KiB
Python
"""
|
|
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
|