agdar/core/tasks.py
2025-11-02 14:35:35 +03:00

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