""" Observations Celery tasks - SLA tracking and notifications. This module implements tasks for: - Checking overdue observations - Sending SLA reminder emails - Escalation handling """ import logging from celery import shared_task from django.utils import timezone logger = logging.getLogger(__name__) @shared_task def check_overdue_observations(): """ Periodic task to check for overdue observations. Runs every 15 minutes (configured in config/celery.py). Updates is_overdue flag and sets breached_at for observations past their SLA deadline. """ from apps.observations.models import Observation, ObservationStatus active_statuses = [ ObservationStatus.NEW, ObservationStatus.TRIAGED, ObservationStatus.ASSIGNED, ObservationStatus.IN_PROGRESS, ] active_observations = Observation.objects.filter( status__in=active_statuses, due_at__isnull=False, ).select_related("hospital", "assigned_department") overdue_count = 0 for observation in active_observations: if observation.check_overdue(): overdue_count += 1 logger.warning( f"Observation {observation.id} is overdue: {observation.tracking_code} (due: {observation.due_at})" ) if overdue_count > 0: logger.info(f"Found {overdue_count} overdue observations") return {"overdue_count": overdue_count} @shared_task def send_observation_sla_reminders(): """ Periodic task to send SLA reminder emails for observations approaching deadline. Runs every hour (configured in config/celery.py). """ from apps.observations.models import Observation, ObservationStatus, ObservationSLAConfig from apps.notifications.services import NotificationService now = timezone.now() active_statuses = [ ObservationStatus.NEW, ObservationStatus.TRIAGED, ObservationStatus.ASSIGNED, ObservationStatus.IN_PROGRESS, ] active_observations = Observation.objects.filter( status__in=active_statuses, due_at__isnull=False, is_overdue=False, ).select_related("hospital", "assigned_to") first_reminder_count = 0 second_reminder_count = 0 for observation in active_observations: config = observation.get_sla_config() if not config: continue first_reminder_hours = config.get_first_reminder_hours_after() second_reminder_hours = config.get_second_reminder_hours_after() if first_reminder_hours > 0 and observation.reminder_sent_at is None: hours_since_creation = (now - observation.created_at).total_seconds() / 3600 if hours_since_creation >= first_reminder_hours: if observation.assigned_to and observation.assigned_to.email: try: NotificationService.send_email( email=observation.assigned_to.email, subject=f"SLA Reminder - Observation {observation.tracking_code}", message=f"Observation '{observation.title or observation.description[:50]}' is due at {observation.due_at}. Please take action.", related_object=observation, ) observation.reminder_sent_at = now observation.save(update_fields=["reminder_sent_at"]) first_reminder_count += 1 except Exception as e: logger.error(f"Failed to send observation reminder: {e}") if second_reminder_hours > 0 and observation.second_reminder_sent_at is None: hours_since_creation = (now - observation.created_at).total_seconds() / 3600 if hours_since_creation >= second_reminder_hours: if observation.assigned_to and observation.assigned_to.email: try: NotificationService.send_email( email=observation.assigned_to.email, subject=f"URGENT: SLA Reminder - Observation {observation.tracking_code}", message=f"Observation '{observation.title or observation.description[:50]}' is due at {observation.due_at}. URGENT action required.", related_object=observation, ) observation.second_reminder_sent_at = now observation.save(update_fields=["second_reminder_sent_at"]) second_reminder_count += 1 except Exception as e: logger.error(f"Failed to send second observation reminder: {e}") logger.info( f"Sent {first_reminder_count} first reminders and {second_reminder_count} second reminders for observations" ) return { "first_reminder_count": first_reminder_count, "second_reminder_count": second_reminder_count, }