379 lines
13 KiB
Python
379 lines
13 KiB
Python
"""
|
|
HIS Client Service - Fetches patient data from external HIS systems
|
|
|
|
This service provides a client to fetch patient survey data from
|
|
Hospital Information Systems (HIS) via their APIs.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional
|
|
from urllib.parse import quote
|
|
|
|
import requests
|
|
from django.utils import timezone
|
|
|
|
from apps.integrations.models import IntegrationConfig, SourceSystem
|
|
|
|
logger = logging.getLogger("apps.integrations")
|
|
|
|
|
|
class HISClient:
|
|
"""
|
|
Client for fetching patient data from HIS systems.
|
|
|
|
This client connects to external HIS APIs and retrieves
|
|
patient demographic and visit data for survey processing.
|
|
"""
|
|
|
|
def __init__(self, config: Optional[IntegrationConfig] = None):
|
|
"""
|
|
Initialize HIS client.
|
|
|
|
Args:
|
|
config: IntegrationConfig instance. If None, will try to load
|
|
active HIS configuration from database.
|
|
"""
|
|
self.config = config or self._get_default_config()
|
|
self.session = requests.Session()
|
|
|
|
# Load credentials from environment if no config
|
|
self.username = os.getenv("HIS_API_USERNAME", "")
|
|
self.password = os.getenv("HIS_API_PASSWORD", "")
|
|
|
|
def _get_default_config(self) -> Optional[IntegrationConfig]:
|
|
"""Get default active HIS configuration from database."""
|
|
try:
|
|
return IntegrationConfig.objects.filter(source_system=SourceSystem.HIS, is_active=True).first()
|
|
except Exception as e:
|
|
logger.error(f"Error loading HIS configuration: {e}")
|
|
return None
|
|
|
|
def _get_headers(self) -> Dict[str, str]:
|
|
"""Get request headers with authentication."""
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
}
|
|
|
|
# Support for API key if configured
|
|
if self.config and self.config.api_key:
|
|
headers["X-API-Key"] = self.config.api_key
|
|
headers["Authorization"] = f"Bearer {self.config.api_key}"
|
|
|
|
return headers
|
|
|
|
def _get_api_url(self) -> Optional[str]:
|
|
"""Get API URL from configuration or environment."""
|
|
if not self.config:
|
|
# Fallback to environment variable
|
|
return os.getenv("HIS_API_URL", "https://his.alhammadi.med.sa/SSRCE/API/FetchPatientVisitTimeStamps")
|
|
return self.config.api_url
|
|
|
|
def _get_auth(self) -> Optional[tuple]:
|
|
"""Get Basic Auth credentials."""
|
|
if self.username and self.password:
|
|
return (self.username, self.password)
|
|
return None
|
|
|
|
def fetch_patient_data(self, since: Optional[datetime] = None, until: Optional[datetime] = None) -> Optional[Dict]:
|
|
"""
|
|
Fetch full patient data from HIS system including visit timelines.
|
|
|
|
Returns the complete HIS response dict with all arrays intact:
|
|
- FetchPatientDataTimeStampList
|
|
- FetchPatientDataTimeStampVisitEDDataList
|
|
- FetchPatientDataTimeStampVisitIPDataList
|
|
- FetchPatientDataTimeStampVisitOPDataList
|
|
|
|
Args:
|
|
since: Only fetch patients since this datetime
|
|
until: Only fetch patients until this datetime (optional)
|
|
|
|
Returns:
|
|
Full HIS response dict or None if error
|
|
"""
|
|
api_url = self._get_api_url()
|
|
if not api_url:
|
|
logger.error("No HIS API URL configured")
|
|
return None
|
|
|
|
try:
|
|
if since:
|
|
end_time = until if until else timezone.now()
|
|
else:
|
|
end_time = timezone.now()
|
|
since = end_time - timedelta(minutes=5)
|
|
|
|
from_date = self._format_datetime(since)
|
|
to_date = self._format_datetime(end_time)
|
|
|
|
url = f"{api_url}?FromDate={quote(from_date)}&ToDate={quote(to_date)}&SSN=0&MobileNo=0"
|
|
|
|
logger.info(f"Fetching patient data from HIS: {url}")
|
|
|
|
response = self.session.get(
|
|
url,
|
|
headers=self._get_headers(),
|
|
auth=self._get_auth(),
|
|
timeout=30,
|
|
verify=True,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if isinstance(data, dict):
|
|
patient_count = len(data.get("FetchPatientDataTimeStampList", []))
|
|
logger.info(f"Fetched {patient_count} patients from HIS")
|
|
return data
|
|
else:
|
|
logger.error(f"Unexpected HIS response type: {type(data)}")
|
|
return None
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Error fetching patient data from HIS: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error fetching patient data from HIS: {e}")
|
|
return None
|
|
|
|
def fetch_doctor_ratings(self, from_date: datetime, to_date: datetime) -> Optional[Dict]:
|
|
"""
|
|
Fetch doctor ratings from HIS FetchDoctorRatingMAPI1 endpoint.
|
|
|
|
Args:
|
|
from_date: Start date for ratings
|
|
to_date: End date for ratings
|
|
|
|
Returns:
|
|
HIS response dict with FetchDoctorRatingMAPI1List or None on error
|
|
"""
|
|
api_url = os.getenv("HIS_RATINGS_API_URL")
|
|
if not api_url:
|
|
logger.error("HIS_RATINGS_API_URL not configured in environment")
|
|
return None
|
|
|
|
try:
|
|
from_date_str = self._format_datetime(from_date)
|
|
to_date_str = self._format_datetime(to_date)
|
|
|
|
url = f"{api_url}?FromDate={quote(from_date_str)}&ToDate={quote(to_date_str)}&SSN=0&MobileNo=0"
|
|
|
|
logger.info(f"Fetching doctor ratings from HIS: {url}")
|
|
|
|
response = self.session.get(
|
|
url,
|
|
headers=self._get_headers(),
|
|
auth=self._get_auth(),
|
|
timeout=30,
|
|
verify=True,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if isinstance(data, dict):
|
|
rating_count = len(data.get("FetchDoctorRatingMAPI1List", []))
|
|
logger.info(f"Fetched {rating_count} doctor ratings from HIS")
|
|
return data
|
|
else:
|
|
logger.error(f"Unexpected HIS response type: {type(data)}")
|
|
return None
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Error fetching doctor ratings from HIS: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error fetching doctor ratings from HIS: {e}")
|
|
return None
|
|
|
|
def fetch_patient_by_identifier(self, ssn: Optional[str] = None, mobile_no: Optional[str] = None) -> Optional[Dict]:
|
|
"""
|
|
Search HIS for a patient by SSN or Mobile number.
|
|
|
|
Uses a wide date range (5 years) since we are searching by
|
|
identifier rather than date. Returns only the patient
|
|
demographics list from the HIS response.
|
|
|
|
Args:
|
|
ssn: Patient SSN/National ID
|
|
mobile_no: Patient mobile number
|
|
|
|
Returns:
|
|
List of patient demographic dicts from HIS, or None on error
|
|
"""
|
|
if not ssn and not mobile_no:
|
|
logger.warning("fetch_patient_by_identifier called without SSN or MobileNo")
|
|
return None
|
|
|
|
api_url = self._get_api_url()
|
|
if not api_url:
|
|
logger.error("No HIS API URL configured")
|
|
return None
|
|
|
|
try:
|
|
end_time = timezone.now()
|
|
since = end_time - timedelta(days=365 * 5)
|
|
|
|
from_date = self._format_datetime(since)
|
|
to_date = self._format_datetime(end_time)
|
|
ssn_param = ssn or "0"
|
|
mobile_param = mobile_no or "0"
|
|
|
|
url = (
|
|
f"{api_url}?FromDate={quote(from_date)}&ToDate={quote(to_date)}"
|
|
f"&SSN={quote(ssn_param)}&MobileNo={quote(mobile_param)}"
|
|
)
|
|
|
|
logger.info(f"Searching HIS by identifier: SSN={ssn_param}, Mobile={mobile_param}")
|
|
|
|
response = self.session.get(
|
|
url,
|
|
headers=self._get_headers(),
|
|
auth=self._get_auth(),
|
|
timeout=30,
|
|
verify=True,
|
|
)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if isinstance(data, dict):
|
|
patients = data.get("FetchPatientDataTimeStampList", [])
|
|
logger.info(f"HIS identifier search returned {len(patients)} patients")
|
|
return patients
|
|
else:
|
|
logger.error(f"Unexpected HIS response type: {type(data)}")
|
|
return None
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Error searching HIS by identifier: {e}")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error searching HIS by identifier: {e}")
|
|
return None
|
|
|
|
def fetch_patient_visits(self, patient_id: str) -> List[Dict]:
|
|
"""
|
|
Fetch visit data for a specific patient.
|
|
|
|
Args:
|
|
patient_id: Patient ID from HIS
|
|
|
|
Returns:
|
|
List of visit data dictionaries
|
|
"""
|
|
api_url = self._get_api_url()
|
|
if not api_url:
|
|
return []
|
|
|
|
visits_endpoint = f"{api_url.rstrip('/')}/{patient_id}/visits"
|
|
|
|
try:
|
|
response = self.session.get(visits_endpoint, headers=self._get_headers(), timeout=30)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if isinstance(data, list):
|
|
return data
|
|
elif isinstance(data, dict):
|
|
return data.get("FetchPatientDataTimeStampVisitDataList", [])
|
|
return []
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"Error fetching visits for patient {patient_id}: {e}")
|
|
return []
|
|
|
|
def test_connection(self) -> Dict:
|
|
"""
|
|
Test connectivity to HIS system.
|
|
|
|
Returns:
|
|
Dict with 'success' boolean and 'message' string
|
|
"""
|
|
api_url = self._get_api_url()
|
|
if not api_url:
|
|
return {"success": False, "message": "No API URL configured"}
|
|
|
|
try:
|
|
# Try to fetch patients for last 1 minute as a test
|
|
end_time = timezone.now()
|
|
start_time = end_time - timedelta(minutes=1)
|
|
|
|
from_date = self._format_datetime(start_time)
|
|
to_date = self._format_datetime(end_time)
|
|
|
|
url = f"{api_url}?FromDate={quote(from_date)}&ToDate={quote(to_date)}&SSN=0&MobileNo=0"
|
|
|
|
response = self.session.get(
|
|
url, headers=self._get_headers(), auth=self._get_auth(), timeout=10, verify=True
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return {
|
|
"success": True,
|
|
"message": "Successfully connected to HIS",
|
|
"config_name": self.config.name if self.config else "Default",
|
|
}
|
|
else:
|
|
return {"success": False, "message": f"HIS returned status {response.status_code}"}
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
return {"success": False, "message": "Could not connect to HIS server"}
|
|
except requests.exceptions.Timeout:
|
|
return {"success": False, "message": "Connection to HIS timed out"}
|
|
except Exception as e:
|
|
return {"success": False, "message": f"Error: {str(e)}"}
|
|
|
|
@staticmethod
|
|
def _format_datetime(dt: datetime) -> str:
|
|
"""Format datetime for HIS API (DD-Mon-YYYY HH:MM:SS format)."""
|
|
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
return f"{dt.day:02d}-{months[dt.month - 1]}-{dt.year} {dt.hour:02d}:{dt.minute:02d}:{dt.second:02d}"
|
|
|
|
|
|
class HISClientFactory:
|
|
"""Factory for creating HIS clients."""
|
|
|
|
@staticmethod
|
|
def get_client(hospital_id: Optional[str] = None) -> HISClient:
|
|
"""
|
|
Get HIS client for a specific hospital or default.
|
|
|
|
Args:
|
|
hospital_id: Optional hospital ID to get specific config
|
|
|
|
Returns:
|
|
HISClient instance
|
|
"""
|
|
config = None
|
|
|
|
if hospital_id:
|
|
# Try to find config for specific hospital
|
|
try:
|
|
from apps.organizations.models import Hospital
|
|
|
|
hospital = Hospital.objects.filter(id=hospital_id).first()
|
|
if hospital:
|
|
config = IntegrationConfig.objects.filter(
|
|
source_system=SourceSystem.HIS, is_active=True, config_json__hospital_id=str(hospital.id)
|
|
).first()
|
|
except Exception:
|
|
pass
|
|
|
|
return HISClient(config)
|
|
|
|
@staticmethod
|
|
def get_all_active_clients() -> List[HISClient]:
|
|
"""Get all active HIS clients for multi-hospital setups."""
|
|
configs = IntegrationConfig.objects.filter(source_system=SourceSystem.HIS, is_active=True)
|
|
|
|
if not configs.exists():
|
|
# Return default client with no config
|
|
return [HISClient()]
|
|
|
|
return [HISClient(config) for config in configs]
|