HH/apps/integrations/services/his_client.py
2026-03-28 14:03:56 +03:00

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]