333 lines
12 KiB
Python
333 lines
12 KiB
Python
"""
|
|
Integrations Celery tasks
|
|
|
|
This module contains tasks for:
|
|
1. Fetching surveys from HIS systems (every 25 minutes)
|
|
2. Testing HIS connection
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from celery import shared_task
|
|
from django.utils import timezone
|
|
|
|
logger = logging.getLogger("apps.integrations")
|
|
|
|
|
|
def _notify_his_fetch_failure(result):
|
|
"""Send email notification to PX Admins when HIS survey fetch fails."""
|
|
try:
|
|
from apps.accounts.models import User
|
|
from apps.notifications.services import NotificationService
|
|
|
|
px_admins = User.objects.filter(groups__name="PX Admin", is_active=True)
|
|
if not px_admins.exists():
|
|
logger.warning("No PX Admin users found to notify about HIS fetch failure")
|
|
return
|
|
|
|
error_list = "\n".join(f" • {e}" for e in result.get("errors", []))
|
|
details = result.get("details", [])
|
|
failed_clients = [d for d in details if d.get("errors")]
|
|
client_summary = "\n".join(
|
|
f" • {d['config']}: {'; '.join(d['errors'])}" for d in failed_clients
|
|
) if failed_clients else "See errors above."
|
|
|
|
timestamp = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
subject = f"⚠️ HIS Survey Fetch Failed — PX360 ({timestamp})"
|
|
|
|
plain_text = (
|
|
f"HIS Survey Fetch Task — Failure Report\n"
|
|
f"Timestamp: {timestamp}\n\n"
|
|
f"Clients processed: {result.get('clients_processed', 0)}\n"
|
|
f"Total errors: {len(result.get('errors', []))}\n\n"
|
|
f"Errors:\n{error_list or 'None'}\n\n"
|
|
f"Failed clients:\n{client_summary}\n\n"
|
|
f"Action required: Check HIS API credentials, network connectivity, "
|
|
f"and IntegrationConfig status.\n"
|
|
)
|
|
|
|
html_message = f"""
|
|
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
|
|
<div style="background: #dc2626; padding: 16px 24px;">
|
|
<h2 style="color: white; margin: 0; font-size: 18px;">⚠️ HIS Survey Fetch Failed</h2>
|
|
<p style="color: rgba(255,255,255,0.8); margin: 4px 0 0; font-size: 13px;">{timestamp}</p>
|
|
</div>
|
|
<div style="padding: 20px;">
|
|
<p style="margin: 0 0 12px;"><strong>Clients processed:</strong> {result.get('clients_processed', 0)}</p>
|
|
<p style="margin: 0 0 12px;"><strong>Total errors:</strong> {len(result.get('errors', []))}</p>
|
|
<p style="margin: 0 0 8px; font-weight: bold;">Errors:</p>
|
|
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 12px; margin-bottom: 16px;">
|
|
<pre style="white-space: pre-wrap; font-size: 13px; color: #991b1b; margin: 0;">{error_list or 'None'}</pre>
|
|
</div>
|
|
<div style="background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px; padding: 12px; margin-bottom: 16px;">
|
|
<p style="font-size: 13px; color: #92400e; margin: 0;">
|
|
<strong>Action required:</strong> Check HIS API credentials, network connectivity,
|
|
and IntegrationConfig status in the PX360 admin panel.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
for admin in px_admins:
|
|
try:
|
|
NotificationService.send_email(
|
|
email=admin.email,
|
|
subject=subject,
|
|
message=plain_text,
|
|
html_message=html_message,
|
|
user=admin,
|
|
notification_type="system",
|
|
)
|
|
except Exception:
|
|
logger.error(f"Failed to send HIS failure notification to {admin.email}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send HIS fetch failure notification: {e}")
|
|
|
|
|
|
# =============================================================================
|
|
# HIS Survey Fetching Tasks
|
|
# =============================================================================
|
|
|
|
|
|
def _parse_his_date(date_str):
|
|
"""Parse HIS date format 'DD-Mon-YYYY HH:MM:SS' to timezone-aware datetime."""
|
|
if not date_str:
|
|
return None
|
|
for fmt in ("%d-%b-%Y %H:%M:%S", "%d-%b-%Y %H:%M"):
|
|
try:
|
|
naive = datetime.strptime(date_str, fmt)
|
|
return timezone.make_aware(naive)
|
|
except ValueError:
|
|
continue
|
|
return None
|
|
|
|
|
|
@shared_task
|
|
def fetch_his_surveys(from_date_str=None, to_date_str=None):
|
|
"""
|
|
Periodic task to fetch patient data from HIS system every 5 minutes.
|
|
|
|
This task:
|
|
1. Connects to configured HIS API endpoints
|
|
2. Fetches all patients from the last 5 minutes (or custom date range)
|
|
3. Saves patient and visit data for every patient (all types)
|
|
4. Creates surveys only for patients whose visit is complete:
|
|
- ED/IP: DischargeDate present
|
|
- OP: last visit event is older than configured delay hours
|
|
|
|
Args:
|
|
from_date_str: Optional override - fetch from this date (DD-Mon-YYYY HH:MM:SS)
|
|
to_date_str: Optional override - fetch until this date (DD-Mon-YYYY HH:MM:SS)
|
|
|
|
Scheduled to run every 5 minutes via Celery Beat.
|
|
|
|
Returns:
|
|
dict: Summary of fetched and processed data
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
|
|
from apps.integrations.services.his_adapter import HISAdapter
|
|
from apps.integrations.services.his_client import HISClient, HISClientFactory
|
|
|
|
logger.info("Starting HIS patient data fetch task")
|
|
|
|
result = {
|
|
"success": False,
|
|
"clients_processed": 0,
|
|
"total_patients": 0,
|
|
"visits_saved": 0,
|
|
"surveys_created": 0,
|
|
"errors": [],
|
|
"details": [],
|
|
}
|
|
|
|
try:
|
|
clients = HISClientFactory.get_all_active_clients()
|
|
|
|
if not clients:
|
|
msg = "No active HIS configurations found"
|
|
logger.warning(msg)
|
|
result["errors"].append(msg)
|
|
return result
|
|
|
|
if from_date_str and to_date_str:
|
|
fetch_since = _parse_his_date(from_date_str)
|
|
fetch_until = _parse_his_date(to_date_str)
|
|
if not fetch_since or not fetch_until:
|
|
msg = f"Invalid date format: from_date={from_date_str}, to_date={to_date_str}"
|
|
result["errors"].append(msg)
|
|
return result
|
|
logger.info(f"Using custom date range: {fetch_since} to {fetch_until}")
|
|
else:
|
|
fetch_since = timezone.now() - timedelta(minutes=5)
|
|
fetch_until = None
|
|
|
|
logger.info(f"Fetching patient data since {fetch_since}")
|
|
|
|
for client in clients:
|
|
client_result = {
|
|
"config": client.config.name if client.config else "Default",
|
|
"total_patients": 0,
|
|
"visits_saved": 0,
|
|
"surveys_created": 0,
|
|
"errors": [],
|
|
}
|
|
|
|
try:
|
|
connection_test = client.test_connection()
|
|
if not connection_test["success"]:
|
|
error_msg = f"HIS connection failed for {client_result['config']}: {connection_test['message']}"
|
|
logger.error(error_msg)
|
|
client_result["errors"].append(error_msg)
|
|
result["details"].append(client_result)
|
|
continue
|
|
|
|
logger.info(f"Fetching patient data from {client_result['config']}")
|
|
|
|
his_data = client.fetch_patient_data(since=fetch_since, until=fetch_until)
|
|
|
|
if not his_data:
|
|
logger.info(f"No data returned from {client_result['config']}")
|
|
result["details"].append(client_result)
|
|
continue
|
|
|
|
patient_list = his_data.get("FetchPatientDataTimeStampList", [])
|
|
client_result["total_patients"] = len(patient_list)
|
|
result["total_patients"] += len(patient_list)
|
|
|
|
if not patient_list:
|
|
logger.info(f"No patients found in {client_result['config']}")
|
|
result["details"].append(client_result)
|
|
continue
|
|
|
|
logger.info(f"Fetched {len(patient_list)} patients from {client_result['config']}")
|
|
|
|
process_result = HISAdapter.process_his_response(his_data)
|
|
|
|
client_result["visits_saved"] = process_result.get("visits_saved", 0)
|
|
client_result["surveys_created"] = process_result.get("surveys_created", 0)
|
|
client_result["errors"] = process_result.get("errors", [])
|
|
|
|
result["visits_saved"] += client_result["visits_saved"]
|
|
result["surveys_created"] += client_result["surveys_created"]
|
|
result["errors"].extend(client_result["errors"])
|
|
|
|
if client.config:
|
|
client.config.last_sync_at = timezone.now()
|
|
client.config.save(update_fields=["last_sync_at"])
|
|
|
|
result["clients_processed"] += 1
|
|
|
|
except Exception as e:
|
|
error_msg = f"Error processing HIS client {client_result['config']}: {str(e)}"
|
|
logger.error(error_msg, exc_info=True)
|
|
client_result["errors"].append(error_msg)
|
|
|
|
result["details"].append(client_result)
|
|
|
|
result["success"] = True
|
|
|
|
logger.info(
|
|
f"HIS fetch completed: {result['clients_processed']} clients, "
|
|
f"{result['total_patients']} patients, "
|
|
f"{result['visits_saved']} visits saved, "
|
|
f"{result['surveys_created']} surveys created"
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = f"Fatal error in fetch_his_surveys task: {str(e)}"
|
|
logger.error(error_msg, exc_info=True)
|
|
result["errors"].append(error_msg)
|
|
|
|
if result["errors"]:
|
|
_notify_his_fetch_failure(result)
|
|
|
|
return result
|
|
|
|
|
|
@shared_task
|
|
def test_his_connection(config_id=None):
|
|
"""
|
|
Test connectivity to HIS system.
|
|
|
|
Args:
|
|
config_id: Optional IntegrationConfig ID. If not provided,
|
|
tests the default HIS configuration.
|
|
|
|
Returns:
|
|
dict: Connection test results
|
|
"""
|
|
from apps.integrations.models import IntegrationConfig, SourceSystem
|
|
from apps.integrations.services.his_client import HISClient
|
|
|
|
logger.info(f"Testing HIS connection for config_id={config_id}")
|
|
|
|
try:
|
|
if config_id:
|
|
config = IntegrationConfig.objects.filter(id=config_id, source_system=SourceSystem.HIS).first()
|
|
if not config:
|
|
return {"success": False, "message": f"HIS configuration with ID {config_id} not found"}
|
|
client = HISClient(config)
|
|
else:
|
|
client = HISClient()
|
|
|
|
result = client.test_connection()
|
|
logger.info(f"HIS connection test result: {result}")
|
|
return result
|
|
|
|
except Exception as e:
|
|
error_msg = f"Error testing HIS connection: {str(e)}"
|
|
logger.error(error_msg, exc_info=True)
|
|
return {"success": False, "message": error_msg}
|
|
|
|
|
|
@shared_task
|
|
def sync_his_survey_mappings():
|
|
"""
|
|
Sync survey template mappings from HIS system.
|
|
|
|
This task can be scheduled periodically to sync any mapping
|
|
configurations from the HIS system.
|
|
|
|
Returns:
|
|
dict: Sync results
|
|
"""
|
|
from apps.integrations.models import SurveyTemplateMapping
|
|
from apps.organizations.models import Hospital
|
|
|
|
logger.info("Starting HIS survey mappings sync")
|
|
|
|
result = {"success": True, "mappings_synced": 0, "errors": []}
|
|
|
|
try:
|
|
# Get all active mappings
|
|
mappings = SurveyTemplateMapping.objects.filter(is_active=True)
|
|
|
|
for mapping in mappings:
|
|
try:
|
|
# Validate mapping
|
|
if not mapping.survey_template:
|
|
result["errors"].append(f"Mapping {mapping.id} has no survey template")
|
|
continue
|
|
|
|
# Additional sync logic can be added here
|
|
# e.g., fetching updated mapping rules from HIS
|
|
|
|
result["mappings_synced"] += 1
|
|
|
|
except Exception as e:
|
|
result["errors"].append(f"Error syncing mapping {mapping.id}: {str(e)}")
|
|
|
|
logger.info(f"HIS survey mappings sync completed: {result['mappings_synced']} mappings")
|
|
|
|
except Exception as e:
|
|
error_msg = f"Error syncing HIS mappings: {str(e)}"
|
|
logger.error(error_msg, exc_info=True)
|
|
result["success"] = False
|
|
result["errors"].append(error_msg)
|
|
|
|
return result
|