300 lines
10 KiB
Python
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)}
|