HH/apps/accounts/tasks.py

300 lines
10 KiB
Python

"""
Accounts Celery tasks
This module contains tasks for:
- Sending onboarding reminders to provisional users
- Cleaning up expired invitations
- Processing user acknowledgements
"""
import logging
from datetime import timedelta
from celery import shared_task
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.utils import timezone
from apps.notifications.settings_models import HospitalNotificationSettings
from apps.notifications.settings_service import NotificationServiceWithSettings
from .models import UserProvisionalLog
from .services import EmailService
logger = logging.getLogger(__name__)
User = get_user_model()
@shared_task(bind=True, max_retries=3)
def send_onboarding_reminders(self):
"""
Send reminder emails to provisional users whose invitations are about to expire.
Sends reminders at:
- 48 hours before expiry
- 24 hours before expiry
Respects hospital notification settings.
"""
now = timezone.now()
# Calculate time windows for reminders
# 24h reminder: expires between 23-25 hours from now
reminder_24h_start = now + timedelta(hours=23)
reminder_24h_end = now + timedelta(hours=25)
# 48h reminder: expires between 47-49 hours from now
reminder_48h_start = now + timedelta(hours=47)
reminder_48h_end = now + timedelta(hours=49)
logger.info(f"Checking for onboarding reminders at {now}")
# Find users needing 24h reminder
users_24h = User.objects.filter(
is_provisional=True,
acknowledgement_completed=False,
invitation_expires_at__gte=reminder_24h_start,
invitation_expires_at__lte=reminder_24h_end,
invitation_token__isnull=False
).exclude(
# Exclude users who already received 24h reminder
provisional_logs__event_type='onboarding_reminder_sent',
provisional_logs__description__contains='24h'
)
# Find users needing 48h reminder
users_48h = User.objects.filter(
is_provisional=True,
acknowledgement_completed=False,
invitation_expires_at__gte=reminder_48h_start,
invitation_expires_at__lte=reminder_48h_end,
invitation_token__isnull=False
).exclude(
# Exclude users who already received 48h reminder
provisional_logs__event_type='onboarding_reminder_sent',
provisional_logs__description__contains='48h'
)
sent_count_24h = 0
sent_count_48h = 0
skipped_count = 0
error_count = 0
# Send 24h reminders
for user in users_24h:
try:
hospital_id = getattr(user, 'hospital_id', None)
if hospital_id:
# Check notification settings
settings = HospitalNotificationSettings.get_for_hospital(hospital_id)
if not settings.notifications_enabled:
logger.info(f"Notifications disabled for hospital {hospital_id}, skipping 24h reminder for {user.email}")
skipped_count += 1
continue
if not settings.onboarding_reminder_email:
logger.info(f"Onboarding reminders disabled for hospital {hospital_id}, skipping {user.email}")
skipped_count += 1
continue
# Send reminder using new notification service
results = NotificationServiceWithSettings.send_onboarding_reminder(
user_email=user.email,
provisional_user=user
)
# Also send via legacy service for backward compatibility
EmailService.send_reminder_email(user)
# Log the reminder
UserProvisionalLog.objects.create(
user=user,
event_type='onboarding_reminder_sent',
description=f"24h reminder sent before invitation expiry",
metadata={
'hours_before_expiry': 24,
'expires_at': user.invitation_expires_at.isoformat(),
'channels': [r[0] for r in results] if results else ['email']
}
)
sent_count_24h += 1
logger.info(f"Sent 24h onboarding reminder to {user.email}")
except Exception as e:
logger.error(f"Failed to send 24h reminder to {user.email}: {str(e)}")
error_count += 1
# Send 48h reminders
for user in users_48h:
try:
hospital_id = getattr(user, 'hospital_id', None)
if hospital_id:
# Check notification settings
settings = HospitalNotificationSettings.get_for_hospital(hospital_id)
if not settings.notifications_enabled:
logger.info(f"Notifications disabled for hospital {hospital_id}, skipping 48h reminder for {user.email}")
skipped_count += 1
continue
if not settings.onboarding_reminder_email:
logger.info(f"Onboarding reminders disabled for hospital {hospital_id}, skipping {user.email}")
skipped_count += 1
continue
# Send reminder using new notification service
results = NotificationServiceWithSettings.send_onboarding_reminder(
user_email=user.email,
provisional_user=user
)
# Also send via legacy service for backward compatibility
EmailService.send_reminder_email(user)
# Log the reminder
UserProvisionalLog.objects.create(
user=user,
event_type='onboarding_reminder_sent',
description=f"48h reminder sent before invitation expiry",
metadata={
'hours_before_expiry': 48,
'expires_at': user.invitation_expires_at.isoformat(),
'channels': [r[0] for r in results] if results else ['email']
}
)
sent_count_48h += 1
logger.info(f"Sent 48h onboarding reminder to {user.email}")
except Exception as e:
logger.error(f"Failed to send 48h reminder to {user.email}: {str(e)}")
error_count += 1
summary = {
'24h_reminders_sent': sent_count_24h,
'48h_reminders_sent': sent_count_48h,
'total_reminders_sent': sent_count_24h + sent_count_48h,
'skipped_due_to_settings': skipped_count,
'errors': error_count
}
logger.info(f"Onboarding reminder task completed: {summary}")
return summary
@shared_task(bind=True, max_retries=3)
def cleanup_expired_invitations(self):
"""
Clean up expired provisional user invitations.
Marks users with expired invitations and logs the expiration.
Optionally sends a final notification about expiration.
"""
now = timezone.now()
# Find expired invitations
expired_users = User.objects.filter(
is_provisional=True,
acknowledgement_completed=False,
invitation_expires_at__lt=now,
invitation_token__isnull=False
).exclude(
# Exclude already logged as expired
provisional_logs__event_type='invitation_expired'
)
expired_count = 0
error_count = 0
for user in expired_users:
try:
# Log the expiration
UserProvisionalLog.objects.create(
user=user,
event_type='invitation_expired',
description=f"Invitation token expired for {user.email}",
metadata={
'expired_at': now.isoformat(),
'original_expiry': user.invitation_expires_at.isoformat() if user.invitation_expires_at else None
}
)
# Invalidate the token
user.invitation_token = None
user.save(update_fields=['invitation_token'])
expired_count += 1
logger.info(f"Marked invitation as expired for {user.email}")
except Exception as e:
logger.error(f"Failed to process expired invitation for {user.email}: {str(e)}")
error_count += 1
summary = {
'expired_invitations': expired_count,
'errors': error_count
}
logger.info(f"Expired invitation cleanup completed: {summary}")
return summary
@shared_task(bind=True, max_retries=3)
def send_final_expiration_notice(self, user_id):
"""
Send a final notification to user that their invitation has expired.
Args:
user_id: UUID of the provisional user
"""
try:
user = User.objects.get(id=user_id, is_provisional=True)
# Check notification settings
hospital_id = getattr(user, 'hospital_id', None)
if hospital_id:
settings = HospitalNotificationSettings.get_for_hospital(hospital_id)
if not settings.notifications_enabled or not settings.onboarding_reminder_email:
logger.info(f"Notifications disabled, skipping expiration notice for {user.email}")
return {'status': 'skipped', 'reason': 'notifications_disabled'}
# Send expiration email
from django.core.mail import send_mail
from django.conf import settings as django_settings
subject = 'Your PX360 Invitation Has Expired'
message = f"""
Dear {user.first_name},
Your invitation to join PX360 has expired.
Please contact your administrator to request a new invitation.
Best regards,
PX360 Team
"""
send_mail(
subject=subject,
message=message,
from_email=django_settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
fail_silently=True
)
logger.info(f"Sent expiration notice to {user.email}")
return {'status': 'sent', 'user_email': user.email}
except User.DoesNotExist:
logger.warning(f"User {user_id} not found for expiration notice")
return {'status': 'error', 'reason': 'user_not_found'}
except Exception as e:
logger.error(f"Failed to send expiration notice: {str(e)}")
return {'status': 'error', 'reason': str(e)}