""" Survey Celery tasks This module contains tasks for: - Creating and sending surveys - Sending survey reminders - Processing survey responses - Triggering actions based on negative feedback """ import logging from celery import shared_task from django.db import transaction from django.utils import timezone logger = logging.getLogger(__name__) @shared_task(bind=True, max_retries=3) def create_and_send_survey(self, stage_instance_id): """ Create survey instance and send invitation. This task is triggered when a journey stage completes and auto_send_survey=True. Args: stage_instance_id: UUID of PatientJourneyStageInstance Returns: dict: Result with survey_instance_id and delivery status """ from apps.core.services import create_audit_log from apps.journeys.models import PatientJourneyStageInstance from apps.notifications.services import NotificationService from apps.surveys.models import SurveyInstance try: # Get stage instance stage_instance = PatientJourneyStageInstance.objects.select_related( 'stage_template__survey_template', 'journey_instance__patient', 'journey_instance__hospital' ).get(id=stage_instance_id) # Verify survey template exists if not stage_instance.stage_template.survey_template: logger.warning(f"No survey template for stage {stage_instance.stage_template.name}") return {'status': 'skipped', 'reason': 'no_survey_template'} # Check if survey already created if stage_instance.survey_instance: logger.info(f"Survey already exists for stage instance {stage_instance_id}") return {'status': 'skipped', 'reason': 'already_exists'} patient = stage_instance.journey_instance.patient # Determine delivery channel and recipient delivery_channel = 'sms' # Default recipient_phone = patient.phone recipient_email = patient.email # Create survey instance with transaction.atomic(): survey_instance = SurveyInstance.objects.create( survey_template=stage_instance.stage_template.survey_template, patient=patient, journey_instance=stage_instance.journey_instance, journey_stage_instance=stage_instance, encounter_id=stage_instance.journey_instance.encounter_id, delivery_channel=delivery_channel, recipient_phone=recipient_phone, recipient_email=recipient_email ) # Link survey to stage stage_instance.survey_instance = survey_instance stage_instance.survey_sent_at = timezone.now() stage_instance.save(update_fields=['survey_instance', 'survey_sent_at']) # Send survey invitation notification_log = NotificationService.send_survey_invitation( survey_instance=survey_instance, language=patient.language if hasattr(patient, 'language') else 'en' ) # Update survey instance status survey_instance.status = 'active' survey_instance.sent_at = timezone.now() survey_instance.save(update_fields=['status', 'sent_at']) # Log audit event create_audit_log( event_type='survey_sent', description=f"Survey sent to {patient.get_full_name()} for stage {stage_instance.stage_template.name}", content_object=survey_instance, metadata={ 'survey_template': stage_instance.stage_template.survey_template.name, 'stage': stage_instance.stage_template.name, 'encounter_id': stage_instance.journey_instance.encounter_id, 'channel': delivery_channel } ) logger.info( f"Survey created and sent: {survey_instance.id} to {patient.get_full_name()} " f"via {delivery_channel}" ) return { 'status': 'sent', 'survey_instance_id': str(survey_instance.id), 'notification_log_id': str(notification_log.id) } except PatientJourneyStageInstance.DoesNotExist: error_msg = f"Stage instance {stage_instance_id} not found" logger.error(error_msg) return {'status': 'error', 'reason': error_msg} except Exception as e: error_msg = f"Error creating/sending survey: {str(e)}" logger.error(error_msg, exc_info=True) # Retry the task raise self.retry(exc=e, countdown=60 * (self.request.retries + 1)) @shared_task def send_survey_reminder(survey_instance_id): """ Send reminder for incomplete survey. Args: survey_instance_id: UUID of SurveyInstance Returns: dict: Result with delivery status """ from apps.notifications.services import NotificationService from apps.surveys.models import SurveyInstance try: survey_instance = SurveyInstance.objects.select_related( 'patient', 'survey_template' ).get(id=survey_instance_id) # Only send reminder if survey is still pending/active if survey_instance.status not in ['pending', 'active']: return {'status': 'skipped', 'reason': f'survey_status_{survey_instance.status}'} # Send reminder patient = survey_instance.patient language = patient.language if hasattr(patient, 'language') else 'en' notification_log = NotificationService.send_survey_invitation( survey_instance=survey_instance, language=language ) logger.info(f"Survey reminder sent for {survey_instance.id}") return { 'status': 'sent', 'notification_log_id': str(notification_log.id) } except SurveyInstance.DoesNotExist: error_msg = f"Survey instance {survey_instance_id} not found" logger.error(error_msg) return {'status': 'error', 'reason': error_msg} except Exception as e: error_msg = f"Error sending survey reminder: {str(e)}" logger.error(error_msg, exc_info=True) return {'status': 'error', 'reason': error_msg} @shared_task def process_survey_completion(survey_instance_id): """ Process completed survey. This task: 1. Calculates the survey score 2. Checks if score is negative 3. Creates PXAction if negative (Phase 6) Args: survey_instance_id: UUID of SurveyInstance Returns: dict: Result with score and action status """ from apps.core.services import create_audit_log from apps.surveys.models import SurveyInstance try: survey_instance = SurveyInstance.objects.select_related( 'survey_template', 'patient' ).get(id=survey_instance_id) # Calculate score score = survey_instance.calculate_score() # Log completion create_audit_log( event_type='survey_completed', description=f"Survey completed by {survey_instance.patient.get_full_name()} with score {score}", content_object=survey_instance, metadata={ 'score': float(score) if score else None, 'is_negative': survey_instance.is_negative, 'survey_template': survey_instance.survey_template.name } ) logger.info( f"Survey {survey_instance.id} completed with score {score} " f"(negative: {survey_instance.is_negative})" ) # If negative, create PXAction if survey_instance.is_negative: logger.info(f"Negative survey detected - creating PXAction") # Check if it's a complaint resolution survey if survey_instance.survey_template.survey_type == 'complaint_resolution': from apps.px_action_center.tasks import create_action_from_complaint_resolution create_action_from_complaint_resolution.delay(str(survey_instance.id)) else: # Regular survey from apps.px_action_center.tasks import create_action_from_survey create_action_from_survey.delay(str(survey_instance.id)) return { 'status': 'processed', 'score': float(score) if score else None, 'is_negative': survey_instance.is_negative } except SurveyInstance.DoesNotExist: error_msg = f"Survey instance {survey_instance_id} not found" logger.error(error_msg) return {'status': 'error', 'reason': error_msg} except Exception as e: error_msg = f"Error processing survey completion: {str(e)}" logger.error(error_msg, exc_info=True) return {'status': 'error', 'reason': error_msg} @shared_task(bind=True, max_retries=3) def send_satisfaction_feedback(self, survey_instance_id, user_id=None): """ Send satisfaction feedback form to patient after negative survey contact. This creates a feedback form linked to the survey and sends it to the patient to assess their satisfaction with the resolution. Args: survey_instance_id: UUID of SurveyInstance user_id: UUID of User who initiated the feedback (optional) Returns: dict: Result with feedback_id and delivery status """ from apps.core.services import create_audit_log from apps.feedback.models import Feedback, FeedbackType, FeedbackResponse from apps.notifications.services import NotificationService from apps.surveys.models import SurveyInstance try: # Get survey instance survey_instance = SurveyInstance.objects.select_related( 'patient', 'survey_template', 'journey_instance__hospital' ).get(id=survey_instance_id) # Check if feedback already sent if survey_instance.satisfaction_feedback_sent: logger.warning(f"Satisfaction feedback already sent for survey {survey_instance_id}") return {'status': 'skipped', 'reason': 'already_sent'} # Check if patient was contacted if not survey_instance.patient_contacted: logger.warning(f"Patient not contacted yet for survey {survey_instance_id}") return {'status': 'skipped', 'reason': 'patient_not_contacted'} patient = survey_instance.patient hospital = survey_instance.journey_instance.hospital if survey_instance.journey_instance else patient.primary_hospital # Create satisfaction feedback with transaction.atomic(): feedback = Feedback.objects.create( patient=patient, hospital=hospital, department=survey_instance.journey_stage_instance.stage_template.department if survey_instance.journey_stage_instance else None, feedback_type=FeedbackType.SATISFACTION_CHECK, title=f"Satisfaction Check - {survey_instance.survey_template.name}", message=f"Please rate your satisfaction with how we addressed your concerns regarding the survey.", category='communication', priority='medium', sentiment='neutral', status='submitted', related_survey=survey_instance, encounter_id=survey_instance.encounter_id, source='system', metadata={ 'survey_id': str(survey_instance.id), 'survey_score': float(survey_instance.total_score) if survey_instance.total_score else None, 'auto_generated': True } ) # Create initial response FeedbackResponse.objects.create( feedback=feedback, response_type='note', message=f"Satisfaction feedback automatically created following negative survey (Score: {survey_instance.total_score})", created_by_id=user_id, is_internal=True ) # Update survey instance survey_instance.satisfaction_feedback_sent = True survey_instance.satisfaction_feedback_sent_at = timezone.now() survey_instance.save(update_fields=['satisfaction_feedback_sent', 'satisfaction_feedback_sent_at']) # Send notification to patient # TODO: Implement feedback form link notification # For now, we'll log it logger.info(f"Satisfaction feedback {feedback.id} created for survey {survey_instance.id}") # Log audit event create_audit_log( event_type='satisfaction_feedback_sent', description=f"Satisfaction feedback sent to {patient.get_full_name()} for survey {survey_instance.survey_template.name}", content_object=feedback, metadata={ 'survey_id': str(survey_instance.id), 'survey_score': float(survey_instance.total_score) if survey_instance.total_score else None, 'feedback_id': str(feedback.id) } ) return { 'status': 'sent', 'feedback_id': str(feedback.id), 'survey_id': str(survey_instance.id) } except SurveyInstance.DoesNotExist: error_msg = f"Survey instance {survey_instance_id} not found" logger.error(error_msg) return {'status': 'error', 'reason': error_msg} except Exception as e: error_msg = f"Error sending satisfaction feedback: {str(e)}" logger.error(error_msg, exc_info=True) # Retry the task raise self.retry(exc=e, countdown=60 * (self.request.retries + 1))