""" 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)}