HH/apps/surveys/tasks.py
2025-12-28 20:01:22 +03:00

363 lines
14 KiB
Python

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