medicafe 0.240613.0__py3-none-any.whl → 0.240809.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +37 -5
- MediBot/MediBot.py +13 -2
- MediBot/MediBot_Crosswalk_Library.py +15 -8
- MediBot/MediBot_Preprocessor_lib.py +14 -2
- MediBot/MediBot_docx_decoder.py +13 -5
- MediLink/MediLink.py +42 -77
- MediLink/MediLink_837p_encoder.py +64 -47
- MediLink/MediLink_837p_encoder_library.py +24 -35
- MediLink/MediLink_API_Generator.py +246 -0
- MediLink/MediLink_API_v2.py +2 -0
- MediLink/MediLink_API_v3.py +429 -0
- MediLink/MediLink_ClaimStatus.py +144 -0
- MediLink/MediLink_ConfigLoader.py +13 -7
- MediLink/MediLink_DataMgmt.py +4 -4
- MediLink/MediLink_Decoder.py +122 -20
- MediLink/MediLink_Deductible.py +210 -0
- MediLink/MediLink_Down.py +97 -66
- MediLink/MediLink_Parser.py +106 -24
- MediLink/MediLink_UI.py +12 -26
- MediLink/MediLink_Up.py +181 -111
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/METADATA +2 -1
- medicafe-0.240809.0.dist-info/RECORD +47 -0
- medicafe-0.240613.0.dist-info/RECORD +0 -43
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/WHEEL +0 -0
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
import yaml
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from MediLink import MediLink_ConfigLoader
|
|
9
|
+
except ImportError:
|
|
10
|
+
import MediLink_ConfigLoader
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
TODO At some point it might make sense to test their acknoledgment endpoint. body is transactionId.
|
|
14
|
+
This API is used to extract the claim acknowledgement details for the given transactionid which was
|
|
15
|
+
generated for 837 requests in claim submission process. Claims Acknowledgement (277CA) will provide
|
|
16
|
+
a status of claim-level acknowledgement of all claims received in the front-end processing system and
|
|
17
|
+
adjudication system.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
class ConfigLoader:
|
|
21
|
+
@staticmethod
|
|
22
|
+
def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'config.json'),
|
|
23
|
+
crosswalk_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'crosswalk.json')):
|
|
24
|
+
return MediLink_ConfigLoader.load_configuration(config_path, crosswalk_path)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def load_swagger_file(swagger_path):
|
|
28
|
+
try:
|
|
29
|
+
print("Attempting to load Swagger file: {}".format(swagger_path))
|
|
30
|
+
with open(swagger_path, 'r') as swagger_file:
|
|
31
|
+
if swagger_path.endswith('.yaml') or swagger_path.endswith('.yml'):
|
|
32
|
+
print("Parsing YAML file: {}".format(swagger_path))
|
|
33
|
+
swagger_data = yaml.safe_load(swagger_file)
|
|
34
|
+
elif swagger_path.endswith('.json'):
|
|
35
|
+
print("Parsing JSON file: {}".format(swagger_path))
|
|
36
|
+
swagger_data = json.load(swagger_file)
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("Unsupported Swagger file format.")
|
|
39
|
+
print("Successfully loaded Swagger file: {}".format(swagger_path))
|
|
40
|
+
return swagger_data
|
|
41
|
+
except ValueError as e:
|
|
42
|
+
print("Error parsing Swagger file {}: {}".format(swagger_path, e))
|
|
43
|
+
MediLink_ConfigLoader.log("Error parsing Swagger file {}: {}".format(swagger_path, e), level="ERROR")
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
print("Swagger file not found: {}".format(swagger_path))
|
|
46
|
+
MediLink_ConfigLoader.log("Swagger file not found: {}".format(swagger_path), level="ERROR")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print("Unexpected error loading Swagger file {}: {}".format(swagger_path, e))
|
|
49
|
+
MediLink_ConfigLoader.log("Unexpected error loading Swagger file {}: {}".format(swagger_path, e), level="ERROR")
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# Function to ensure numeric type
|
|
53
|
+
def ensure_numeric(value):
|
|
54
|
+
if isinstance(value, str):
|
|
55
|
+
try:
|
|
56
|
+
value = float(value)
|
|
57
|
+
except ValueError:
|
|
58
|
+
raise ValueError("Cannot convert {} to a numeric type".format(value))
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
class TokenCache:
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.tokens = {}
|
|
64
|
+
|
|
65
|
+
def get(self, endpoint_name, current_time):
|
|
66
|
+
token_info = self.tokens.get(endpoint_name, {})
|
|
67
|
+
if token_info and token_info['expires_at'] > current_time:
|
|
68
|
+
return token_info['access_token']
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def set(self, endpoint_name, access_token, expires_in, current_time):
|
|
72
|
+
# Ensure types are correct
|
|
73
|
+
current_time = ensure_numeric(current_time)
|
|
74
|
+
expires_in = ensure_numeric(expires_in)
|
|
75
|
+
|
|
76
|
+
self.tokens[endpoint_name] = {
|
|
77
|
+
'access_token': access_token,
|
|
78
|
+
'expires_at': current_time + expires_in - 120
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class BaseAPIClient:
|
|
82
|
+
def __init__(self, config):
|
|
83
|
+
self.config = config
|
|
84
|
+
self.token_cache = TokenCache()
|
|
85
|
+
|
|
86
|
+
def get_access_token(self, endpoint_name):
|
|
87
|
+
raise NotImplementedError("Subclasses should implement this!")
|
|
88
|
+
|
|
89
|
+
def make_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None, headers=None):
|
|
90
|
+
raise NotImplementedError("Subclasses should implement this!")
|
|
91
|
+
|
|
92
|
+
class APIClient(BaseAPIClient):
|
|
93
|
+
def __init__(self):
|
|
94
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
95
|
+
super().__init__(config)
|
|
96
|
+
|
|
97
|
+
def get_access_token(self, endpoint_name):
|
|
98
|
+
current_time = time.time()
|
|
99
|
+
cached_token = self.token_cache.get(endpoint_name, current_time)
|
|
100
|
+
if cached_token:
|
|
101
|
+
MediLink_ConfigLoader.log("Using cached token for endpoint: {}".format(endpoint_name), level="INFO")
|
|
102
|
+
return cached_token
|
|
103
|
+
|
|
104
|
+
endpoint_config = self.config['MediLink_Config']['endpoints'][endpoint_name]
|
|
105
|
+
token_url = endpoint_config['token_url']
|
|
106
|
+
data = {
|
|
107
|
+
'grant_type': 'client_credentials',
|
|
108
|
+
'client_id': endpoint_config['client_id'],
|
|
109
|
+
'client_secret': endpoint_config['client_secret']
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Add scope if specified in the configuration
|
|
113
|
+
if 'scope' in endpoint_config:
|
|
114
|
+
data['scope'] = endpoint_config['scope']
|
|
115
|
+
|
|
116
|
+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
117
|
+
|
|
118
|
+
response = requests.post(token_url, headers=headers, data=data)
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
token_data = response.json()
|
|
121
|
+
access_token = token_data['access_token']
|
|
122
|
+
expires_in = token_data.get('expires_in', 3600)
|
|
123
|
+
|
|
124
|
+
self.token_cache.set(endpoint_name, access_token, expires_in, current_time)
|
|
125
|
+
MediLink_ConfigLoader.log("Obtained new token for endpoint: {}".format(endpoint_name), level="INFO")
|
|
126
|
+
return access_token
|
|
127
|
+
|
|
128
|
+
def make_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None, headers=None):
|
|
129
|
+
token = self.get_access_token(endpoint_name)
|
|
130
|
+
if headers is None:
|
|
131
|
+
headers = {}
|
|
132
|
+
headers.update({'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'})
|
|
133
|
+
url = self.config['MediLink_Config']['endpoints'][endpoint_name]['api_url'] + url_extension
|
|
134
|
+
|
|
135
|
+
# Debug: Print request details
|
|
136
|
+
# print("Request URL: {}".format(url))
|
|
137
|
+
# print("Request Headers: {}".format(headers))
|
|
138
|
+
# print("Request Params: {}".format(params))
|
|
139
|
+
# print("Request Data: {}".format(data))
|
|
140
|
+
|
|
141
|
+
if call_type == 'GET':
|
|
142
|
+
response = requests.get(url, headers=headers, params=params)
|
|
143
|
+
elif call_type == 'POST':
|
|
144
|
+
headers['Content-Type'] = 'application/json'
|
|
145
|
+
response = requests.post(url, headers=headers, json=data)
|
|
146
|
+
elif call_type == 'DELETE':
|
|
147
|
+
response = requests.delete(url, headers=headers)
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError("Unsupported call type")
|
|
150
|
+
|
|
151
|
+
if response.status_code >= 400:
|
|
152
|
+
error_message = "Error {}: {}".format(response.status_code, response.text)
|
|
153
|
+
MediLink_ConfigLoader.log(error_message, level="ERROR")
|
|
154
|
+
response.raise_for_status()
|
|
155
|
+
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
def fetch_payer_name_from_api(payer_id, config, primary_endpoint='AVAILITY'):
|
|
159
|
+
client = APIClient()
|
|
160
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
161
|
+
endpoints = config['MediLink_Config']['endpoints']
|
|
162
|
+
|
|
163
|
+
if primary_endpoint and primary_endpoint in endpoints:
|
|
164
|
+
endpoint_order = [primary_endpoint] + [endpoint for endpoint in endpoints if endpoint != primary_endpoint]
|
|
165
|
+
else:
|
|
166
|
+
endpoint_order = list(endpoints.keys())
|
|
167
|
+
|
|
168
|
+
for endpoint_name in endpoint_order:
|
|
169
|
+
try:
|
|
170
|
+
response = client.make_api_call(endpoint_name, 'GET', config['MediLink_Config']['endpoints'][endpoint_name].get('payer_list_endpoint', '/availity-payer-list'), {'payerId': payer_id})
|
|
171
|
+
payers = response.get('payers', [])
|
|
172
|
+
if payers:
|
|
173
|
+
payer_name = payers[0].get('displayName', payers[0].get('name'))
|
|
174
|
+
MediLink_ConfigLoader.log("Successfully found payer at {} for ID {}: {}".format(endpoint_name, payer_id, payer_name), level="INFO")
|
|
175
|
+
return payer_name
|
|
176
|
+
else:
|
|
177
|
+
MediLink_ConfigLoader.log("No payer found at {} for ID: {}. Trying next available endpoint.".format(endpoint_name, payer_id), level="INFO")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
MediLink_ConfigLoader.log("API call to {} failed: {}".format(endpoint_name, e), level="ERROR")
|
|
180
|
+
|
|
181
|
+
error_message = "All endpoints exhausted for Payer ID {}.".format(payer_id)
|
|
182
|
+
MediLink_ConfigLoader.log(error_message, level="CRITICAL")
|
|
183
|
+
raise ValueError(error_message)
|
|
184
|
+
|
|
185
|
+
def get_claim_summary_by_provider(client, tin, first_service_date, last_service_date, payer_id, get_standard_error='false'):
|
|
186
|
+
endpoint_name = 'UHCAPI'
|
|
187
|
+
url_extension = client.config['MediLink_Config']['endpoints'][endpoint_name]['additional_endpoints']['claim_summary_by_provider']
|
|
188
|
+
headers = {
|
|
189
|
+
'tin': tin,
|
|
190
|
+
'firstServiceDt': first_service_date,
|
|
191
|
+
'lastServiceDt': last_service_date,
|
|
192
|
+
'payerId': payer_id,
|
|
193
|
+
'getStandardError': get_standard_error,
|
|
194
|
+
'Accept': 'application/json'
|
|
195
|
+
}
|
|
196
|
+
return client.make_api_call(endpoint_name, 'GET', url_extension, params=None, data=None, headers=headers)
|
|
197
|
+
|
|
198
|
+
def get_eligibility(client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi):
|
|
199
|
+
endpoint_name = 'UHCAPI'
|
|
200
|
+
url_extension = client.config['MediLink_Config']['endpoints'][endpoint_name]['additional_endpoints']['eligibility']
|
|
201
|
+
url_extension = url_extension + '?payerID={}&providerLastName={}&searchOption={}&dateOfBirth={}&memberId={}&npi={}'.format(
|
|
202
|
+
payer_id, provider_last_name, search_option, date_of_birth, member_id, npi)
|
|
203
|
+
return client.make_api_call(endpoint_name, 'GET', url_extension)
|
|
204
|
+
|
|
205
|
+
def get_eligibility_v3(client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi,
|
|
206
|
+
first_name=None, last_name=None, payer_label=None, payer_name=None, service_start=None, service_end=None,
|
|
207
|
+
middle_name=None, gender=None, ssn=None, city=None, state=None, zip=None, group_number=None,
|
|
208
|
+
service_type_code=None, provider_first_name=None, tax_id_number=None, provider_name_id=None,
|
|
209
|
+
corporate_tax_owner_id=None, corporate_tax_owner_name=None, organization_name=None,
|
|
210
|
+
organization_id=None, identify_service_level_deductible=True):
|
|
211
|
+
|
|
212
|
+
# Ensure all required parameters have values
|
|
213
|
+
if not all([client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi]):
|
|
214
|
+
raise ValueError("All required parameters must have values: client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi")
|
|
215
|
+
|
|
216
|
+
# Validate payer_id
|
|
217
|
+
valid_payer_ids = ["87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400", "03432", "86050", "86047", "95378", "95467"]
|
|
218
|
+
if payer_id not in valid_payer_ids:
|
|
219
|
+
raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
|
|
220
|
+
|
|
221
|
+
endpoint_name = 'UHCAPI'
|
|
222
|
+
url_extension = client.config['MediLink_Config']['endpoints'][endpoint_name]['additional_endpoints']['eligibility_v3']
|
|
223
|
+
|
|
224
|
+
# Construct request body
|
|
225
|
+
body = {
|
|
226
|
+
"memberId": member_id,
|
|
227
|
+
"lastName": last_name,
|
|
228
|
+
"firstName": first_name,
|
|
229
|
+
"dateOfBirth": date_of_birth,
|
|
230
|
+
"payerID": payer_id,
|
|
231
|
+
"payerLabel": payer_label,
|
|
232
|
+
"payerName": payer_name,
|
|
233
|
+
"serviceStart": service_start,
|
|
234
|
+
"serviceEnd": service_end,
|
|
235
|
+
"middleName": middle_name,
|
|
236
|
+
"gender": gender,
|
|
237
|
+
"ssn": ssn,
|
|
238
|
+
"city": city,
|
|
239
|
+
"state": state,
|
|
240
|
+
"zip": zip,
|
|
241
|
+
"groupNumber": group_number,
|
|
242
|
+
"serviceTypeCode": service_type_code,
|
|
243
|
+
"providerLastName": provider_last_name,
|
|
244
|
+
"providerFirstName": provider_first_name,
|
|
245
|
+
"taxIdNumber": tax_id_number,
|
|
246
|
+
"providerNameID": provider_name_id,
|
|
247
|
+
"npi": npi,
|
|
248
|
+
"corporateTaxOwnerID": corporate_tax_owner_id,
|
|
249
|
+
"corporateTaxOwnerName": corporate_tax_owner_name,
|
|
250
|
+
"organizationName": organization_name,
|
|
251
|
+
"organizationID": organization_id,
|
|
252
|
+
"searchOption": search_option,
|
|
253
|
+
"identifyServiceLevelDeductible": identify_service_level_deductible
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Remove None values from the body
|
|
257
|
+
body = {k: v for k, v in body.items() if v is not None}
|
|
258
|
+
|
|
259
|
+
# Log the request body
|
|
260
|
+
MediLink_ConfigLoader.log("Request body: {}".format(json.dumps(body, indent=4)), level="DEBUG")
|
|
261
|
+
|
|
262
|
+
return client.make_api_call(endpoint_name, 'POST', url_extension, params=None, data=body)
|
|
263
|
+
|
|
264
|
+
def is_test_mode(client, body, endpoint_type):
|
|
265
|
+
"""
|
|
266
|
+
Checks if Test Mode is enabled in the client's configuration and simulates the response if it is.
|
|
267
|
+
|
|
268
|
+
:param client: An instance of APIClient
|
|
269
|
+
:param body: The intended request body
|
|
270
|
+
:param endpoint_type: The type of endpoint being accessed ('claim_submission' or 'claim_details')
|
|
271
|
+
:return: A dummy response simulating the real API call if Test Mode is enabled, otherwise None
|
|
272
|
+
"""
|
|
273
|
+
if client.config.get("MediLink_Config", {}).get("TestMode", True):
|
|
274
|
+
print("Test Mode is enabled! API Call not executed.")
|
|
275
|
+
print("\nIntended request body:", body)
|
|
276
|
+
MediLink_ConfigLoader.log("Test Mode is enabled! Simulating API response for {}.".format(endpoint_type), level="INFO")
|
|
277
|
+
MediLink_ConfigLoader.log("Intended request body: {}".format(body), level="INFO")
|
|
278
|
+
|
|
279
|
+
if endpoint_type == 'claim_submission':
|
|
280
|
+
dummy_response = {
|
|
281
|
+
"transactionId": "CS07180420240328013411240", # This is the tID for the sandbox Claim Acknowledgement endpoint.
|
|
282
|
+
"x12ResponseData": "ISA*00* *00* *ZZ*TEST1234567890 *33*TEST *210101*0101*^*00501*000000001*0*P*:~GS*HC*TEST1234567890*TEST*20210101*0101*1*X*005010X222A1~ST*837*000000001*005010X222A1~BHT*0019*00*00001*20210101*0101*CH~NM1*41*2*TEST SUBMITTER*****46*TEST~PER*IC*TEST CONTACT*TE*1234567890~NM1*40*2*TEST RECEIVER*****46*TEST~HL*1**20*1~NM1*85*2*TEST PROVIDER*****XX*1234567890~N3*TEST ADDRESS~N4*TEST CITY*TEST STATE*12345~REF*EI*123456789~PER*IC*TEST PROVIDER*TE*1234567890~NM1*87*2~N3*TEST ADDRESS~N4*TEST CITY*TEST STATE*12345~HL*2*1*22*0~SBR*P*18*TEST GROUP******CI~NM1*IL*1*TEST PATIENT****MI*123456789~N3*TEST ADDRESS~N4*TEST CITY*TEST STATE*12345~DMG*D8*19800101*M~NM1*PR*2*TEST INSURANCE*****PI*12345~CLM*TESTCLAIM*100***12:B:1*Y*A*Y*Y*P~REF*D9*TESTREFERENCE~HI*ABK:TEST~NM1*DN*1*TEST DOCTOR****XX*1234567890~LX*1~SV1*HC:TEST*100*UN*1***1~DTP*472*RD8*20210101-20210101~REF*6R*TESTREFERENCE~SE*30*000000001~GE*1*1~IEA*1*000000001~",
|
|
283
|
+
"responseType": "dummy_response_837999",
|
|
284
|
+
"message": "Test Mode: Claim validated and sent for further processing"
|
|
285
|
+
}
|
|
286
|
+
elif endpoint_type == 'claim_details':
|
|
287
|
+
dummy_response = {
|
|
288
|
+
"responseType": "dummy_response_277CA-CH",
|
|
289
|
+
"x12ResponseData": "ISA*00* *00* *ZZ*841162764 *ZZ*UB920086 *240318*0921*^*00501*000165687*0*T*:~GS*HN*841162764*UB920086*20240318*0921*0165687*X*005010X214~ST*277*000000006*005010X214~…………….. SE*116*000000006~GE*1*0165687~IEA*1*000165687~",
|
|
290
|
+
"statuscode": "000",
|
|
291
|
+
"message:": ""
|
|
292
|
+
}
|
|
293
|
+
return dummy_response
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def submit_uhc_claim(client, x12_request_data):
|
|
297
|
+
"""
|
|
298
|
+
Submits a UHC claim and retrieves the claim acknowledgement details.
|
|
299
|
+
|
|
300
|
+
This function first submits the claim using the provided x12 837p data. If the client is in Test Mode,
|
|
301
|
+
it returns a simulated response. If Test Mode is not enabled, it submits the claim and then retrieves
|
|
302
|
+
the claim acknowledgement details using the transaction ID from the initial response.
|
|
303
|
+
|
|
304
|
+
:param client: An instance of APIClient
|
|
305
|
+
:param x12_request_data: The x12 837p data as a string
|
|
306
|
+
:return: The final response containing the claim acknowledgement details or a dummy response if in Test Mode
|
|
307
|
+
"""
|
|
308
|
+
endpoint_name = 'UHCAPI'
|
|
309
|
+
claim_submission_url = client.config['MediLink_Config']['endpoints'][endpoint_name]['additional_endpoints']['claim_submission']
|
|
310
|
+
claim_details_url = client.config['MediLink_Config']['endpoints'][endpoint_name]['additional_endpoints']['claim_details']
|
|
311
|
+
|
|
312
|
+
# Headers for the request
|
|
313
|
+
headers = {'Content-Type': 'application/json'}
|
|
314
|
+
|
|
315
|
+
# Request body for claim submission
|
|
316
|
+
claim_body = {'x12RequestData': x12_request_data}
|
|
317
|
+
|
|
318
|
+
# Check if Test Mode is enabled and return simulated response if so
|
|
319
|
+
test_mode_response = is_test_mode(client, claim_body, 'claim_submission')
|
|
320
|
+
if test_mode_response:
|
|
321
|
+
return test_mode_response
|
|
322
|
+
|
|
323
|
+
# Make the API call to submit the claim
|
|
324
|
+
try:
|
|
325
|
+
submission_response = client.make_api_call(endpoint_name, 'POST', claim_submission_url, data=claim_body, headers=headers)
|
|
326
|
+
|
|
327
|
+
# Extract the transaction ID from the submission response
|
|
328
|
+
transaction_id = submission_response.get('transactionId')
|
|
329
|
+
if not transaction_id:
|
|
330
|
+
raise ValueError("transactionId not found in the submission response")
|
|
331
|
+
|
|
332
|
+
# Prepare the request body for the claim acknowledgement retrieval
|
|
333
|
+
acknowledgement_body = {'transactionId': transaction_id}
|
|
334
|
+
|
|
335
|
+
# Check if Test Mode is enabled and return simulated response if so
|
|
336
|
+
test_mode_response = is_test_mode(client, acknowledgement_body, 'claim_details')
|
|
337
|
+
if test_mode_response:
|
|
338
|
+
return test_mode_response
|
|
339
|
+
|
|
340
|
+
# Make the API call to retrieve the claim acknowledgement details
|
|
341
|
+
acknowledgement_response = client.make_api_call(endpoint_name, 'POST', claim_details_url, data=acknowledgement_body, headers=headers)
|
|
342
|
+
return acknowledgement_response
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print("Error during claim processing: {}".format(e))
|
|
346
|
+
raise
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
client = APIClient()
|
|
350
|
+
|
|
351
|
+
# Define a configuration to enable or disable tests
|
|
352
|
+
test_config = {
|
|
353
|
+
'test_fetch_payer_name': False,
|
|
354
|
+
'test_claim_summary': False,
|
|
355
|
+
'test_eligibility': False,
|
|
356
|
+
'test_eligibility_v3': False,
|
|
357
|
+
'test_claim_submission': True,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
api_test_cases = client.config['MediLink_Config']['API Test Case']
|
|
362
|
+
|
|
363
|
+
# Test 1: Fetch Payer Name
|
|
364
|
+
if test_config.get('test_fetch_payer_name', False):
|
|
365
|
+
try:
|
|
366
|
+
for case in api_test_cases:
|
|
367
|
+
payer_name = fetch_payer_name_from_api(case['payer_id'], client.config)
|
|
368
|
+
print("TEST API: Payer Name: {}".format(payer_name))
|
|
369
|
+
except Exception as e:
|
|
370
|
+
print("TEST API: Error in Fetch Payer Name Test: {}".format(e))
|
|
371
|
+
|
|
372
|
+
# Test 2: Get Claim Summary
|
|
373
|
+
if test_config.get('test_claim_summary', False):
|
|
374
|
+
try:
|
|
375
|
+
for case in api_test_cases:
|
|
376
|
+
claim_summary = get_claim_summary_by_provider(client, case['provider_tin'], '05/01/2024', '06/23/2024', case['payer_id'])
|
|
377
|
+
print("TEST API: Claim Summary: {}".format(claim_summary))
|
|
378
|
+
except Exception as e:
|
|
379
|
+
print("TEST API: Error in Claim Summary Test: {}".format(e))
|
|
380
|
+
|
|
381
|
+
# Test 3: Get Eligibility
|
|
382
|
+
if test_config.get('test_eligibility', False):
|
|
383
|
+
try:
|
|
384
|
+
for case in api_test_cases:
|
|
385
|
+
eligibility = get_eligibility(client, case['payer_id'], case['provider_last_name'], case['search_option'],
|
|
386
|
+
case['date_of_birth'], case['member_id'], case['npi'])
|
|
387
|
+
print("TEST API: Eligibility: {}".format(eligibility))
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print("TEST API: Error in Eligibility Test: {}".format(e))
|
|
390
|
+
|
|
391
|
+
# Test 4: Get Eligibility v3
|
|
392
|
+
if test_config.get('test_eligibility_v3', False):
|
|
393
|
+
try:
|
|
394
|
+
for case in api_test_cases:
|
|
395
|
+
eligibility_v3 = get_eligibility_v3(client, payer_id=case['payer_id'], provider_last_name=case['provider_last_name'],
|
|
396
|
+
search_option=case['search_option'], date_of_birth=case['date_of_birth'],
|
|
397
|
+
member_id=case['member_id'], npi=case['npi'])
|
|
398
|
+
print("TEST API: Eligibility v3: {}".format(eligibility_v3))
|
|
399
|
+
except Exception as e:
|
|
400
|
+
print("TEST API: Error in Eligibility v3 Test: {}".format(e))
|
|
401
|
+
|
|
402
|
+
"""
|
|
403
|
+
# Example of iterating over multiple patients (if needed)
|
|
404
|
+
patients = [
|
|
405
|
+
{'payer_id': '87726', 'provider_last_name': 'VIDA', 'search_option': 'MemberIDDateOfBirth', 'date_of_birth': '1980-01-01', 'member_id': '123456789', 'npi': '9876543210'},
|
|
406
|
+
{'payer_id': '87726', 'provider_last_name': 'SMITH', 'search_option': 'MemberIDDateOfBirth', 'date_of_birth': '1970-02-02', 'member_id': '987654321', 'npi': '1234567890'},
|
|
407
|
+
# Add more patients as needed
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
for patient in patients:
|
|
411
|
+
try:
|
|
412
|
+
eligibility = get_eligibility(client, patient['payer_id'], patient['provider_last_name'], patient['search_option'], patient['date_of_birth'], patient['member_id'], patient['npi'])
|
|
413
|
+
print("Eligibility for {}: {}".format(patient['provider_last_name'], eligibility))
|
|
414
|
+
except Exception as e:
|
|
415
|
+
print("Error in getting eligibility for {}: {}".format(patient['provider_last_name'], e))
|
|
416
|
+
"""
|
|
417
|
+
# Test 5: UHC Claim Submission
|
|
418
|
+
if test_config.get('test_claim_submission', False):
|
|
419
|
+
try:
|
|
420
|
+
x12_request_data = (
|
|
421
|
+
"ISA*00* *00* *ZZ*BRT219991205 *33*87726 *170417*1344*^*00501*019160001*0*P*:~GS*HC*BRT219991205*B2BRTA*20170417*134455*19160001*X*005010X222A1~ST*837*000000001*005010X222A1~BHT*0019*00*00001*20170417*134455*CH~NM1*41*2*B00099999819*****46*BB2B~PER*IC*NO NAME*TE*1234567890~NM1*40*2*TIGER*****46*87726~HL*1**20*1~NM1*85*2*XYZ ADDRESS*****XX*1073511762~N3*123 CITY#680~N4*STATE*TG*98765~REF*EI*943319804~PER*IC*XYZ ADDRESS*TE*8008738385*TE*9142862043*FX*1234567890~NM1*87*2~N3*PO BOX 277500~N4*STATE*TS*303847000~HL*2*1*22*0~SBR*P*18*701648******CI~NM1*IL*1*FNAME*LNAME****MI*00123456789~N3*2020 CITY~N4*STATE*TG*80001~DMG*D8*19820220*M~NM1*PR*2*PROVIDER XYZ*****PI*87726~\nCLM*TOSRTA-SPL1*471***12:B:1*Y*A*Y*Y*P~REF*D9*H4HZMH0R4P0104~HI*ABK:Z12~NM1*DN*1*DN*SKO****XX*1255589300~LX*1~SV1*HC:73525*471*UN*1***1~DTP*472*RD8*0190701-20190701~REF*6R*2190476543Z1~SE*30*000000001~GE*1*19160001~IEA*1*019160001~"
|
|
422
|
+
)
|
|
423
|
+
response = submit_uhc_claim(client, x12_request_data)
|
|
424
|
+
print("\nTEST API: Claim Submission Response:\n", response)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
print("\nTEST API: Error in Claim Submission Test:\n", e)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
print("TEST API: Unexpected Error: {}".format(e))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
import MediLink_API_v3
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from MediLink import MediLink_ConfigLoader
|
|
6
|
+
except ImportError:
|
|
7
|
+
import MediLink_ConfigLoader
|
|
8
|
+
|
|
9
|
+
# Load configuration
|
|
10
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
11
|
+
|
|
12
|
+
# Calculate start_date as 60 days before today's date and end_date as today's date
|
|
13
|
+
end_date = datetime.today()
|
|
14
|
+
start_date = end_date - timedelta(days=60)
|
|
15
|
+
end_date_str = end_date.strftime('%m/%d/%Y')
|
|
16
|
+
start_date_str = start_date.strftime('%m/%d/%Y')
|
|
17
|
+
|
|
18
|
+
# Get billing provider TIN from configuration
|
|
19
|
+
billing_provider_tin = config['MediLink_Config'].get('billing_provider_tin')
|
|
20
|
+
|
|
21
|
+
# Define the list of payer_id's to iterate over
|
|
22
|
+
payer_ids = ['87726'] # Default value
|
|
23
|
+
# Allowed payer id's for UHC
|
|
24
|
+
# payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602']
|
|
25
|
+
|
|
26
|
+
# Initialize the API client
|
|
27
|
+
client = MediLink_API_v3.APIClient()
|
|
28
|
+
|
|
29
|
+
# Function to process and display the data in a compact, tabular format
|
|
30
|
+
def display_claim_summary(claim_summary, payer_id):
|
|
31
|
+
claims = claim_summary.get('claims', [])
|
|
32
|
+
|
|
33
|
+
# Display header
|
|
34
|
+
header = "Payer ID: {} | Start Date: {} | End Date: {}".format(payer_id, start_date_str, end_date_str)
|
|
35
|
+
print(header)
|
|
36
|
+
print("=" * len(header))
|
|
37
|
+
|
|
38
|
+
# Table header
|
|
39
|
+
table_header = "{:<10} | {:<10} | {:<20} | {:<6} | {:<6} | {:<7} | {:<7} | {:<7} | {:<7}".format(
|
|
40
|
+
"Claim #", "Status", "Patient", "Proc.", "Serv.", "Allowed", "Paid", "Pt Resp", "Charged")
|
|
41
|
+
print(table_header)
|
|
42
|
+
print("-" * len(table_header))
|
|
43
|
+
|
|
44
|
+
# Process each claim and display it in a compact format
|
|
45
|
+
claims_dict = {}
|
|
46
|
+
for claim in claims:
|
|
47
|
+
claim_number = claim['claimNumber'] # String: e.g., "29285698"
|
|
48
|
+
claim_status = claim['claimStatus'] # String: e.g., "Finalized"
|
|
49
|
+
patient_first_name = claim['memberInfo']['ptntFn'] # String: e.g., "FRANK"
|
|
50
|
+
patient_last_name = claim['memberInfo']['ptntLn'] # String: e.g., "LOHR"
|
|
51
|
+
processed_date = claim['claimSummary']['processedDt'] # String (Date in "MM/DD/YYYY" format): e.g., "06/10/2024"
|
|
52
|
+
first_service_date = claim['claimSummary']['firstSrvcDt'] # String (Date in "MM/DD/YYYY" format): e.g., "05/13/2024"
|
|
53
|
+
total_charged_amount = claim['claimSummary']['totalChargedAmt'] # String (Decimal as String): e.g., "450.00"
|
|
54
|
+
total_allowed_amount = claim['claimSummary']['totalAllowdAmt'] # String (Decimal as String): e.g., "108.95"
|
|
55
|
+
total_paid_amount = claim['claimSummary']['totalPaidAmt'] # String (Decimal as String): e.g., "106.78"
|
|
56
|
+
total_patient_responsibility_amount = claim['claimSummary']['totalPtntRespAmt'] # String (Decimal as String): e.g., "0.00"
|
|
57
|
+
|
|
58
|
+
patient_name = "{} {}".format(patient_first_name, patient_last_name)
|
|
59
|
+
|
|
60
|
+
# Store claims in a dictionary to handle duplicate claim numbers
|
|
61
|
+
if claim_number not in claims_dict:
|
|
62
|
+
claims_dict[claim_number] = []
|
|
63
|
+
claims_dict[claim_number].append({
|
|
64
|
+
'claim_status': claim_status,
|
|
65
|
+
'patient_name': patient_name,
|
|
66
|
+
'processed_date': processed_date,
|
|
67
|
+
'first_service_date': first_service_date,
|
|
68
|
+
'total_charged_amount': total_charged_amount,
|
|
69
|
+
'total_allowed_amount': total_allowed_amount,
|
|
70
|
+
'total_paid_amount': total_paid_amount,
|
|
71
|
+
'total_patient_responsibility_amount': total_patient_responsibility_amount,
|
|
72
|
+
'claim_xwalk_data': claim['claimSummary']['clmXWalkData']
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Sort claims by first_service_date
|
|
76
|
+
sorted_claims = sorted(claims_dict.items(), key=lambda x: x[1][0]['first_service_date'])
|
|
77
|
+
|
|
78
|
+
for claim_number, claim_data_list in sorted_claims:
|
|
79
|
+
# Check for repeated claim numbers and validate data
|
|
80
|
+
if len(claim_data_list) > 1:
|
|
81
|
+
# Validate data
|
|
82
|
+
unique_claims = {tuple(claim_data.items()) for claim_data in claim_data_list}
|
|
83
|
+
if len(unique_claims) == 1:
|
|
84
|
+
# Data is the same, only print once
|
|
85
|
+
claim_data = claim_data_list[0]
|
|
86
|
+
table_row = "{:<10} | {:<10} | {:<20} | {:<6} | {:<6} | {:<7} | {:<7} | {:<7} | {:<7}".format(
|
|
87
|
+
claim_number, claim_data['claim_status'], claim_data['patient_name'][:20],
|
|
88
|
+
claim_data['processed_date'][:5], claim_data['first_service_date'][:5],
|
|
89
|
+
claim_data['total_allowed_amount'], claim_data['total_paid_amount'],
|
|
90
|
+
claim_data['total_patient_responsibility_amount'], claim_data['total_charged_amount']
|
|
91
|
+
)
|
|
92
|
+
print(table_row)
|
|
93
|
+
|
|
94
|
+
if claim_data['total_paid_amount'] == '0.00':
|
|
95
|
+
for xwalk in claim_data['claim_xwalk_data']:
|
|
96
|
+
clm507Cd = xwalk['clm507Cd'] # String: e.g., "F1"
|
|
97
|
+
clm507CdDesc = xwalk['clm507CdDesc'] # String: e.g., "Finalized/Payment-The claim/line has been paid."
|
|
98
|
+
clm508Cd = xwalk['clm508Cd'] # String: e.g., "104"
|
|
99
|
+
clm508CdDesc = xwalk['clm508CdDesc'] # String: e.g., "Processed according to plan provisions..."
|
|
100
|
+
clmIcnSufxCd = xwalk['clmIcnSufxCd'] # String: e.g., "01"
|
|
101
|
+
print(" 507: {} ({}) | 508: {} ({}) | ICN Suffix: {}".format(clm507Cd, clm507CdDesc, clm508Cd, clm508CdDesc, clmIcnSufxCd))
|
|
102
|
+
else:
|
|
103
|
+
# Data is different, print all
|
|
104
|
+
for claim_data in claim_data_list:
|
|
105
|
+
table_row = "{:<10} | {:<10} | {:<20} | {:<6} | {:<6} | {:<7} | {:<7} | {:<7} | {:<7}".format(
|
|
106
|
+
claim_number, claim_data['claim_status'], claim_data['patient_name'][:20],
|
|
107
|
+
claim_data['processed_date'][:5], claim_data['first_service_date'][:5],
|
|
108
|
+
claim_data['total_allowed_amount'], claim_data['total_paid_amount'],
|
|
109
|
+
claim_data['total_patient_responsibility_amount'], claim_data['total_charged_amount']
|
|
110
|
+
)
|
|
111
|
+
print(table_row + " (Duplicate with different data)")
|
|
112
|
+
|
|
113
|
+
if claim_data['total_paid_amount'] == '0.00':
|
|
114
|
+
for xwalk in claim_data['claim_xwalk_data']:
|
|
115
|
+
clm507Cd = xwalk['clm507Cd'] # String: e.g., "F1"
|
|
116
|
+
clm507CdDesc = xwalk['clm507CdDesc'] # String: e.g., "Finalized/Payment-The claim/line has been paid."
|
|
117
|
+
clm508Cd = xwalk['clm508Cd'] # String: e.g., "104"
|
|
118
|
+
clm508CdDesc = xwalk['clm508CdDesc'] # String: e.g., "Processed according to plan provisions..."
|
|
119
|
+
clmIcnSufxCd = xwalk['clmIcnSufxCd'] # String: e.g., "01"
|
|
120
|
+
print(" 507: {} ({}) | 508: {} ({}) | ICN Suffix: {}".format(clm507Cd, clm507CdDesc, clm508Cd, clm508CdDesc, clmIcnSufxCd))
|
|
121
|
+
else:
|
|
122
|
+
# Only one claim, print normally
|
|
123
|
+
claim_data = claim_data_list[0]
|
|
124
|
+
table_row = "{:<10} | {:<10} | {:<20} | {:<6} | {:<6} | {:<7} | {:<7} | {:<7} | {:<7}".format(
|
|
125
|
+
claim_number, claim_data['claim_status'], claim_data['patient_name'][:20],
|
|
126
|
+
claim_data['processed_date'][:5], claim_data['first_service_date'][:5],
|
|
127
|
+
claim_data['total_allowed_amount'], claim_data['total_paid_amount'],
|
|
128
|
+
claim_data['total_patient_responsibility_amount'], claim_data['total_charged_amount']
|
|
129
|
+
)
|
|
130
|
+
print(table_row)
|
|
131
|
+
|
|
132
|
+
if claim_data['total_paid_amount'] == '0.00':
|
|
133
|
+
for xwalk in claim_data['claim_xwalk_data']:
|
|
134
|
+
clm507Cd = xwalk['clm507Cd'] # String: e.g., "F1"
|
|
135
|
+
clm507CdDesc = xwalk['clm507CdDesc'] # String: e.g., "Finalized/Payment-The claim/line has been paid."
|
|
136
|
+
clm508Cd = xwalk['clm508Cd'] # String: e.g., "104"
|
|
137
|
+
clm508CdDesc = xwalk['clm508CdDesc'] # String: e.g., "Processed according to plan provisions..."
|
|
138
|
+
clmIcnSufxCd = xwalk['clmIcnSufxCd'] # String: e.g., "01"
|
|
139
|
+
print(" 507: {} ({}) | 508: {} ({}) | ICN Suffix: {}".format(clm507Cd, clm507CdDesc, clm508Cd, clm508CdDesc, clmIcnSufxCd))
|
|
140
|
+
|
|
141
|
+
# Loop through each payer_id and call the API, then display the claim summary
|
|
142
|
+
for payer_id in payer_ids:
|
|
143
|
+
claim_summary = MediLink_API_v3.get_claim_summary_by_provider(client, billing_provider_tin, start_date_str, end_date_str, payer_id=payer_id)
|
|
144
|
+
display_claim_summary(claim_summary, payer_id)
|
|
@@ -5,18 +5,19 @@ from datetime import datetime
|
|
|
5
5
|
from collections import OrderedDict
|
|
6
6
|
import sys
|
|
7
7
|
import platform
|
|
8
|
+
import yaml
|
|
8
9
|
|
|
9
10
|
"""
|
|
10
11
|
This function should be generalizable to have a initialization script over all the Medi* functions
|
|
11
12
|
"""
|
|
12
13
|
def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'config.json'), crosswalk_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'crosswalk.json')):
|
|
13
14
|
"""
|
|
14
|
-
Loads endpoint configuration, credentials, and other settings from JSON files.
|
|
15
|
+
Loads endpoint configuration, credentials, and other settings from JSON or YAML files.
|
|
15
16
|
|
|
16
17
|
Returns: A tuple containing dictionaries with configuration settings for the main config and crosswalk.
|
|
17
18
|
"""
|
|
18
19
|
# TODO (Low Config Upgrade) The Medicare / Private differentiator flag probably needs to be pulled or passed to this.
|
|
19
|
-
# BUG
|
|
20
|
+
# BUG Hardcode sucks. This should probably be some local env variable.
|
|
20
21
|
# Detect the operating system
|
|
21
22
|
if platform.system() == 'Windows' and platform.release() == 'XP':
|
|
22
23
|
# Use F: paths for Windows XP
|
|
@@ -29,10 +30,15 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
|
29
30
|
|
|
30
31
|
try:
|
|
31
32
|
with open(config_path, 'r') as config_file:
|
|
32
|
-
|
|
33
|
+
if config_path.endswith('.yaml') or config_path.endswith('.yml'):
|
|
34
|
+
config = yaml.safe_load(config_file)
|
|
35
|
+
elif config_path.endswith('.json'):
|
|
36
|
+
config = json.load(config_file, object_pairs_hook=OrderedDict)
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("Unsupported configuration format.")
|
|
39
|
+
|
|
33
40
|
if 'MediLink_Config' not in config:
|
|
34
41
|
raise KeyError("MediLink_Config key is missing from the loaded configuration.")
|
|
35
|
-
# MediLink_config = config['MediLink_Config']
|
|
36
42
|
|
|
37
43
|
with open(crosswalk_path, 'r') as crosswalk_file:
|
|
38
44
|
crosswalk = json.load(crosswalk_file)
|
|
@@ -40,12 +46,12 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
|
40
46
|
return config, crosswalk
|
|
41
47
|
except ValueError as e:
|
|
42
48
|
if isinstance(e, UnicodeDecodeError):
|
|
43
|
-
print("Error decoding
|
|
49
|
+
print("Error decoding file: {}".format(e))
|
|
44
50
|
else:
|
|
45
|
-
print("Error parsing
|
|
51
|
+
print("Error parsing file: {}".format(e))
|
|
46
52
|
sys.exit(1) # Exit the script due to a critical error in configuration loading
|
|
47
53
|
except FileNotFoundError:
|
|
48
|
-
print("One or both
|
|
54
|
+
print("One or both configuration files not found. Config: {}, Crosswalk: {}".format(config_path, crosswalk_path))
|
|
49
55
|
sys.exit(1) # Exit the script due to a critical error in configuration loading
|
|
50
56
|
except KeyError as e:
|
|
51
57
|
print("Critical configuration is missing: {}".format(e))
|
MediLink/MediLink_DataMgmt.py
CHANGED
|
@@ -219,11 +219,11 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path, c
|
|
|
219
219
|
except KeyError as e:
|
|
220
220
|
# Log the missing key information
|
|
221
221
|
missing_key = str(e)
|
|
222
|
-
message = "
|
|
222
|
+
message = "Critical Error: Endpoint config is missing key: {}".format(missing_key)
|
|
223
223
|
MediLink_ConfigLoader.log(message)
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
# Raise an exception to halt execution
|
|
225
|
+
raise RuntimeError("Configuration error: The endpoint configuration is missing definitions for the required remote directories. Please check the configuration and try again.")
|
|
226
|
+
|
|
227
227
|
# Command building
|
|
228
228
|
command = [
|
|
229
229
|
winscp_path,
|