""" Integrations Celery tasks This module contains the core event processing logic that: 1. Processes inbound events from external systems 2. Finds matching journey instances 3. Completes journey stages 4. Triggers survey creation """ import logging from celery import shared_task from django.db import transaction logger = logging.getLogger('apps.integrations') @shared_task(bind=True, max_retries=3) def process_inbound_event(self, event_id): """ Process an inbound integration event. This is the core event processing task that: 1. Finds the journey instance by encounter_id 2. Finds the matching stage by trigger_event_code 3. Completes the stage 4. Creates survey instance if configured 5. Logs audit events Args: event_id: UUID of the InboundEvent to process Returns: dict: Processing result with status and details """ from apps.core.services import create_audit_log from apps.integrations.models import InboundEvent from apps.journeys.models import PatientJourneyInstance, PatientJourneyStageInstance, StageStatus from apps.organizations.models import Department, Staff try: # Get the event event = InboundEvent.objects.get(id=event_id) event.mark_processing() logger.info(f"Processing event {event.id}: {event.event_code} for encounter {event.encounter_id}") # Find journey instance by encounter_id try: journey_instance = PatientJourneyInstance.objects.select_related( 'journey_template', 'patient', 'hospital' ).get(encounter_id=event.encounter_id) except PatientJourneyInstance.DoesNotExist: error_msg = f"No journey instance found for encounter {event.encounter_id}" logger.warning(error_msg) event.mark_ignored(error_msg) return {'status': 'ignored', 'reason': error_msg} # Find matching stage by trigger_event_code matching_stages = journey_instance.stage_instances.filter( stage_template__trigger_event_code=event.event_code, status__in=[StageStatus.PENDING, StageStatus.IN_PROGRESS] ).select_related('stage_template') if not matching_stages.exists(): error_msg = f"No pending stage found with trigger {event.event_code}" logger.warning(error_msg) event.mark_ignored(error_msg) return {'status': 'ignored', 'reason': error_msg} # Get the first matching stage stage_instance = matching_stages.first() # Extract staff and department from event payload staff = None department = None if event.physician_license: try: staff = Staff.objects.get( license_number=event.physician_license, hospital=journey_instance.hospital ) except Staff.DoesNotExist: logger.warning(f"Staff member not found with license: {event.physician_license}") if event.department_code: try: department = Department.objects.get( code=event.department_code, hospital=journey_instance.hospital ) except Department.DoesNotExist: logger.warning(f"Department not found: {event.department_code}") # Complete the stage with transaction.atomic(): success = stage_instance.complete( event=event, staff=staff, department=department, metadata=event.payload_json ) if success: # Log stage completion create_audit_log( event_type='stage_completed', description=f"Stage {stage_instance.stage_template.name} completed for encounter {event.encounter_id}", content_object=stage_instance, metadata={ 'event_code': event.event_code, 'stage_name': stage_instance.stage_template.name, 'journey_type': journey_instance.journey_template.journey_type } ) # Check if survey should be sent if stage_instance.stage_template.auto_send_survey and stage_instance.stage_template.survey_template: # Queue survey creation task with delay from apps.surveys.tasks import create_and_send_survey delay_seconds = stage_instance.stage_template.survey_delay_hours * 3600 logger.info( f"Queuing survey for stage {stage_instance.stage_template.name} " f"(delay: {stage_instance.stage_template.survey_delay_hours}h)" ) create_and_send_survey.apply_async( args=[str(stage_instance.id)], countdown=delay_seconds ) # Mark event as processed event.mark_processed() logger.info( f"Successfully processed event {event.id}: " f"Completed stage {stage_instance.stage_template.name}" ) return { 'status': 'processed', 'stage_completed': stage_instance.stage_template.name, 'journey_completion': journey_instance.get_completion_percentage() } else: error_msg = "Failed to complete stage" event.mark_failed(error_msg) return {'status': 'failed', 'reason': error_msg} except InboundEvent.DoesNotExist: error_msg = f"Event {event_id} not found" logger.error(error_msg) return {'status': 'error', 'reason': error_msg} except Exception as e: error_msg = f"Error processing event: {str(e)}" logger.error(error_msg, exc_info=True) try: event.mark_failed(error_msg) except: pass # Retry the task raise self.retry(exc=e, countdown=60 * (self.request.retries + 1)) @shared_task def process_pending_events(): """ Periodic task to process pending events. This task runs every minute (configured in config/celery.py) and processes all pending events. """ from apps.integrations.models import InboundEvent pending_events = InboundEvent.objects.filter( status='pending' ).order_by('received_at')[:100] # Process max 100 at a time processed_count = 0 for event in pending_events: # Queue individual event for processing process_inbound_event.delay(str(event.id)) processed_count += 1 if processed_count > 0: logger.info(f"Queued {processed_count} pending events for processing") return {'queued': processed_count}