|
| 1 | +from datetime import datetime, timezone |
| 2 | +import json |
| 3 | +import time |
| 4 | +from typing import Literal |
| 5 | +from urllib.parse import urlencode |
| 6 | +import aiohttp |
| 7 | +from fake_useragent import UserAgent |
| 8 | +from fastapi import HTTPException |
| 9 | +from fastapi.logger import logger |
| 10 | +from fastapi.responses import JSONResponse |
| 11 | +from integrations.ecw.ecw_config import ECW_URLS, AuthTokens |
| 12 | +from integrations.ecw.ecw_utils import parse_appointments_xml |
| 13 | +from submodule_integrations.ecw.ecw_models import ( |
| 14 | + GetAppointmentsRequest, |
| 15 | + get_default_date, |
| 16 | +) |
| 17 | +from submodule_integrations.models.integration import Integration |
| 18 | +from submodule_integrations.utils.errors import ( |
| 19 | + IntegrationAPIError, |
| 20 | +) |
| 21 | + |
| 22 | + |
| 23 | +class ECWIntegration(Integration): |
| 24 | + def __init__(self, auth_tokens: AuthTokens, user_agent: str = UserAgent().chrome): |
| 25 | + super().__init__("ecw") |
| 26 | + self.user_agent = user_agent |
| 27 | + self.network_requester = None |
| 28 | + self.url = "https://nybukaapp.eclinicalweb.com/mobiledoc/jsp/catalog/xml" |
| 29 | + self.auth_tokens: AuthTokens = auth_tokens |
| 30 | + |
| 31 | + @classmethod |
| 32 | + async def create(cls, auth_tokens: AuthTokens, network_requester=None): |
| 33 | + instance = cls(auth_tokens) |
| 34 | + instance.network_requester = network_requester |
| 35 | + |
| 36 | + return instance |
| 37 | + |
| 38 | + async def _make_request(self, method: str, url: str, **kwargs): |
| 39 | + if self.network_requester: |
| 40 | + response = await self.network_requester.request(method, url, **kwargs) |
| 41 | + return response |
| 42 | + else: |
| 43 | + async with aiohttp.ClientSession() as session: |
| 44 | + async with session.request(method, url, **kwargs) as response: |
| 45 | + return await self._handle_response(response) |
| 46 | + |
| 47 | + async def _handle_response(self, response: aiohttp.ClientResponse): |
| 48 | + response_text = await response.text() |
| 49 | + status = response.status |
| 50 | + |
| 51 | + parsed_data = None |
| 52 | + |
| 53 | + try: |
| 54 | + if response_text.strip().startswith("<?xml"): |
| 55 | + parsed_data = parse_appointments_xml(response_text) |
| 56 | + else: |
| 57 | + parsed_data = json.loads(response_text) |
| 58 | + except Exception as e: |
| 59 | + logger.warning(f"Response parsing failed: {str(e)}") |
| 60 | + parsed_data = {"error": {"message": "Parsing error", "raw": response_text}} |
| 61 | + |
| 62 | + if 200 <= status < 300: |
| 63 | + return parsed_data |
| 64 | + |
| 65 | + error_message = parsed_data.get("error", {}).get("message", "Unknown error") |
| 66 | + error_code = parsed_data.get("error", {}).get("code", str(status)) |
| 67 | + |
| 68 | + logger.debug(f"{status} - {parsed_data}") |
| 69 | + |
| 70 | + if 400 <= status < 500: |
| 71 | + raise HTTPException(status_code=status, detail=parsed_data) |
| 72 | + elif status >= 500: |
| 73 | + raise IntegrationAPIError( |
| 74 | + self.integration_name, |
| 75 | + f"Downstream server error (translated to HTTP 501): {error_message}", |
| 76 | + 501, |
| 77 | + error_code, |
| 78 | + ) |
| 79 | + else: |
| 80 | + raise IntegrationAPIError( |
| 81 | + self.integration_name, |
| 82 | + f"{error_message} (HTTP {status})", |
| 83 | + status, |
| 84 | + error_code, |
| 85 | + ) |
| 86 | + |
| 87 | + async def _setup_headers(self, content_type: str = None): |
| 88 | + _headers = { |
| 89 | + "User-Agent": self.user_agent, |
| 90 | + "Cookie": self.auth_tokens.Cookie, |
| 91 | + "Sec-Ch-Ua": "'Chromium';v='134', 'Not:A-Brand';v='24', 'Google Chrome';v='134'", |
| 92 | + "x-csrf-token": self.auth_tokens.x_csrf_token, |
| 93 | + "Cip": self.auth_tokens.Cip, |
| 94 | + } |
| 95 | + if content_type: |
| 96 | + _headers["Content-type"] = content_type |
| 97 | + |
| 98 | + return _headers |
| 99 | + |
| 100 | + async def get_appointments(self, get_appointments_request: GetAppointmentsRequest): |
| 101 | + eDate = get_appointments_request.eDate or get_default_date() |
| 102 | + maxCount = get_appointments_request.maxCount or 100 |
| 103 | + |
| 104 | + logger.debug( |
| 105 | + f"Fetching {maxCount} doctor's appointments for user: {self.auth_tokens.TrUserId}" |
| 106 | + ) |
| 107 | + |
| 108 | + headers = await self._setup_headers( |
| 109 | + content_type="application/x-www-form-urlencoded; charset=UTF-8" |
| 110 | + ) |
| 111 | + |
| 112 | + # Format dynamic URL |
| 113 | + url = ECW_URLS["get appointments"].format( |
| 114 | + sessionDID=self.auth_tokens.sessionDID, |
| 115 | + TrUserId=self.auth_tokens.TrUserId, |
| 116 | + timestamp=int(time.time() * 1000), |
| 117 | + clientTimezone="UTC", |
| 118 | + ) |
| 119 | + |
| 120 | + payload = { |
| 121 | + "eDate": eDate, |
| 122 | + "doctorId": 0, |
| 123 | + "sortBy": "time", |
| 124 | + "facilityId": 0, |
| 125 | + "apptTime": 0, |
| 126 | + "checkinstatus": 0, |
| 127 | + "FacilityGrpId": 0, |
| 128 | + "maxCount": maxCount, |
| 129 | + "nCounter": 0, |
| 130 | + "DeptId": 0, |
| 131 | + "fromWeb": "yes", |
| 132 | + "fromAfterCare": "officeVisit", |
| 133 | + "tabId": 3, |
| 134 | + "toDate": "", |
| 135 | + "selectedChkShowASCvisits": "false", |
| 136 | + "includeResourceAppt": "true", |
| 137 | + } |
| 138 | + |
| 139 | + encoded_payload = urlencode(payload) |
| 140 | + |
| 141 | + return await self._make_request( |
| 142 | + method="POST", |
| 143 | + url=url, |
| 144 | + headers=headers, |
| 145 | + data=encoded_payload, |
| 146 | + ) |
0 commit comments