medicafe 0.240419.2__zip
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.
- medicafe-0.240419.2/LICENSE +21 -0
- medicafe-0.240419.2/MANIFEST.in +2 -0
- medicafe-0.240419.2/MediBot/MediBot.bat +70 -0
- medicafe-0.240419.2/MediBot/MediBot.py +316 -0
- medicafe-0.240419.2/MediBot/MediBot_Charges.py +28 -0
- medicafe-0.240419.2/MediBot/MediBot_Preprocessor.py +283 -0
- medicafe-0.240419.2/MediBot/MediBot_UI.py +190 -0
- medicafe-0.240419.2/MediBot/MediBot_dataformat_library.py +145 -0
- medicafe-0.240419.2/MediBot/MediPost.py +5 -0
- medicafe-0.240419.2/MediBot/PDF_to_CSV_Cleaner.py +211 -0
- medicafe-0.240419.2/MediBot/__init__.py +0 -0
- medicafe-0.240419.2/MediBot/update_json.py +43 -0
- medicafe-0.240419.2/MediBot/update_medicafe.py +19 -0
- medicafe-0.240419.2/MediLink/MediLink.py +277 -0
- medicafe-0.240419.2/MediLink/MediLink_277_decoder.py +92 -0
- medicafe-0.240419.2/MediLink/MediLink_837p_encoder.py +392 -0
- medicafe-0.240419.2/MediLink/MediLink_837p_encoder_library.py +679 -0
- medicafe-0.240419.2/MediLink/MediLink_ConfigLoader.py +69 -0
- medicafe-0.240419.2/MediLink/MediLink_DataMgmt.py +206 -0
- medicafe-0.240419.2/MediLink/MediLink_Down.py +151 -0
- medicafe-0.240419.2/MediLink/MediLink_ERA_decoder.py +192 -0
- medicafe-0.240419.2/MediLink/MediLink_Gmail.py +4 -0
- medicafe-0.240419.2/MediLink/MediLink_Scheduler.py +132 -0
- medicafe-0.240419.2/MediLink/MediLink_StatusCheck.py +4 -0
- medicafe-0.240419.2/MediLink/MediLink_UI.py +116 -0
- medicafe-0.240419.2/MediLink/MediLink_Up.py +117 -0
- medicafe-0.240419.2/MediLink/MediLink_batch.bat +7 -0
- medicafe-0.240419.2/MediLink/Soumit_api.py +22 -0
- medicafe-0.240419.2/MediLink/__init__.py +0 -0
- medicafe-0.240419.2/PKG-INFO +11 -0
- medicafe-0.240419.2/README.md +28 -0
- medicafe-0.240419.2/medicafe.egg-info/PKG-INFO +11 -0
- medicafe-0.240419.2/medicafe.egg-info/SOURCES.txt +37 -0
- medicafe-0.240419.2/medicafe.egg-info/dependency_links.txt +1 -0
- medicafe-0.240419.2/medicafe.egg-info/not-zip-safe +1 -0
- medicafe-0.240419.2/medicafe.egg-info/requires.txt +5 -0
- medicafe-0.240419.2/medicafe.egg-info/top_level.txt +2 -0
- medicafe-0.240419.2/setup.cfg +5 -0
- medicafe-0.240419.2/setup.py +28 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
import time
|
|
5
|
+
import sys
|
|
6
|
+
import MediLink_ConfigLoader
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
1. Code Refactoring: Increase modularity and clarity, particularly in segment creation functions (e.g., `create_st_segment`, `create_nm1_billing_provider_segment`), for better maintenance and readability.
|
|
10
|
+
2. Endpoint Support: Extend support within segment creation for additional endpoints with attention to their unique claim submission requirements.
|
|
11
|
+
3. Payer Identification Mechanism: Refine the mechanism for dynamically identifying payers, leveraging payer mappings and integrating with external APIs like Availity for precise payer information retrieval.
|
|
12
|
+
4. Adherence to Endpoint-Specific Standards: Implement and verify the compliance of claim data formatting and inclusion based on the specific demands of each target endpoint within the segment creation logic.
|
|
13
|
+
5. De-persisting Intermediate Files.
|
|
14
|
+
6. Get an API for Optum "Entered As".
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Logs messages with optional error type and claim data.
|
|
18
|
+
def log(message, config='', level="INFO", error_type=None, claim=None):
|
|
19
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
20
|
+
level = level.upper() # Ensure the log level is in uppercase for consistency
|
|
21
|
+
claim_data = "- Claim Data: {}\n".format(claim) if claim else ""
|
|
22
|
+
error_info = "- Error Type: {}\n".format(error_type) if error_type else ""
|
|
23
|
+
full_message = "[{}] {} - {} {} {}\n".format(timestamp, level, message, claim_data, error_info).strip()
|
|
24
|
+
log_file_path = 'process_log.txt' if config == '' else config.get('logFilePath', 'process_log.txt')
|
|
25
|
+
with open(log_file_path, 'a') as log_file:
|
|
26
|
+
log_file.write(full_message)
|
|
27
|
+
|
|
28
|
+
# Converts date format from one format to another.
|
|
29
|
+
def convert_date_format(date_str):
|
|
30
|
+
# Parse the input date string into a datetime object using the input format
|
|
31
|
+
# Determine the input date format based on the length of the input string
|
|
32
|
+
input_format = "%m-%d-%Y" if len(date_str) == 10 else "%m-%d-%y"
|
|
33
|
+
date_obj = datetime.strptime(date_str, input_format)
|
|
34
|
+
# Format the datetime object into the desired output format and return
|
|
35
|
+
return date_obj.strftime("%Y%m%d")
|
|
36
|
+
|
|
37
|
+
# Formats date and time according to the specified format.
|
|
38
|
+
def format_datetime(dt=None, format_type='date'):
|
|
39
|
+
if dt is None:
|
|
40
|
+
dt = datetime.now()
|
|
41
|
+
if format_type == 'date':
|
|
42
|
+
return dt.strftime('%Y%m%d')
|
|
43
|
+
elif format_type == 'isa':
|
|
44
|
+
return dt.strftime('%y%m%d')
|
|
45
|
+
elif format_type == 'time':
|
|
46
|
+
return dt.strftime('%H%M')
|
|
47
|
+
|
|
48
|
+
# Constructs the ST segment for transaction set.
|
|
49
|
+
def create_st_segment(transaction_set_control_number):
|
|
50
|
+
return "ST*837*{:04d}*005010X222A1~".format(transaction_set_control_number)
|
|
51
|
+
|
|
52
|
+
# Constructs the BHT segment based on parsed data.
|
|
53
|
+
def create_bht_segment(parsed_data):
|
|
54
|
+
chart_number = parsed_data.get('CHART', 'UNKNOWN')
|
|
55
|
+
return "BHT*0019*00*{}*{}*{}*CH~".format(
|
|
56
|
+
chart_number, format_datetime(), format_datetime(format_type='time'))
|
|
57
|
+
|
|
58
|
+
# Constructs the HL segment for billing provider.
|
|
59
|
+
def create_hl_billing_provider_segment():
|
|
60
|
+
return "HL*1**20*1~"
|
|
61
|
+
|
|
62
|
+
# Constructs the NM1 segment for billing provider and includes address and Tax ID.
|
|
63
|
+
def create_nm1_billing_provider_segment(config, endpoint):
|
|
64
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
65
|
+
|
|
66
|
+
# Billing provider details
|
|
67
|
+
billing_provider_entity_code = endpoint_config.get('billing_provider_entity_code', '85')
|
|
68
|
+
billing_provider_npi_qualifier = endpoint_config.get('billing_provider_npi_qualifier', 'XX')
|
|
69
|
+
#billing_provider_lastname = endpoint_config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
|
|
70
|
+
#billing_provider_firstname = endpoint_config.get('billing_provider_firstname', '')
|
|
71
|
+
billing_provider_lastname = config.get('billing_provider_lastname', config.get('default_billing_provider_name', 'DEFAULT NAME'))
|
|
72
|
+
billing_provider_firstname = config.get('billing_provider_firstname', '')
|
|
73
|
+
billing_provider_npi = endpoint_config.get('billing_provider_npi', config.get('default_billing_provider_npi', 'DEFAULT NPI'))
|
|
74
|
+
|
|
75
|
+
# Determine billing_entity_type_qualifier based on the presence of billing_provider_firstname
|
|
76
|
+
billing_entity_type_qualifier = '1' if billing_provider_firstname else '2'
|
|
77
|
+
|
|
78
|
+
# Construct NM1 segment for the billing provider
|
|
79
|
+
nm1_segment = "NM1*{}*{}*{}*{}****{}*{}~".format(
|
|
80
|
+
billing_provider_entity_code,
|
|
81
|
+
billing_entity_type_qualifier,
|
|
82
|
+
billing_provider_lastname,
|
|
83
|
+
billing_provider_firstname,
|
|
84
|
+
billing_provider_npi_qualifier,
|
|
85
|
+
billing_provider_npi
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Construct address segments
|
|
89
|
+
address_segments = []
|
|
90
|
+
if config.get('billing_provider_address'):
|
|
91
|
+
# N3 segment for address line
|
|
92
|
+
address_segments.append("N3*{}~".format(config.get('billing_provider_address', 'NO ADDRESS')))
|
|
93
|
+
# N4 segment for City, State, ZIP
|
|
94
|
+
address_segments.append("N4*{}*{}*{}~".format(
|
|
95
|
+
config.get('billing_provider_city', 'NO CITY'),
|
|
96
|
+
config.get('billing_provider_state', 'NO STATE'),
|
|
97
|
+
config.get('billing_provider_zip', 'NO ZIP')
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
# Assuming Tax ID is part of the same loop, otherwise move REF segment to the correct loop
|
|
101
|
+
ref_segment = "REF*EI*{}~".format(config.get('billing_provider_tin', 'NO TAX ID'))
|
|
102
|
+
|
|
103
|
+
# Construct PRV segment if provider taxonomy is needed, are these just for Medicaid??
|
|
104
|
+
#prv_segment = ""
|
|
105
|
+
#if config.get('billing_provider_taxonomy'):
|
|
106
|
+
# prv_segment = "PRV*BI*PXC*{}~".format(config.get('billing_provider_taxonomy'))
|
|
107
|
+
|
|
108
|
+
# Combine all the segments in the correct order, I think the PRV goes after the address and/or after ref
|
|
109
|
+
segments = [nm1_segment]
|
|
110
|
+
#if prv_segment:
|
|
111
|
+
# segments.append(prv_segment)
|
|
112
|
+
segments.extend(address_segments)
|
|
113
|
+
segments.append(ref_segment)
|
|
114
|
+
|
|
115
|
+
return segments
|
|
116
|
+
|
|
117
|
+
# Constructs the NM1 segment and accompanying details for the service facility location.
|
|
118
|
+
def create_service_facility_location_npi_segment(config):
|
|
119
|
+
"""
|
|
120
|
+
Constructs segments for the service facility location, including the NM1 segment for identification
|
|
121
|
+
and accompanying N3 and N4 segments for address details.
|
|
122
|
+
"""
|
|
123
|
+
facility_npi = config.get('service_facility_npi', 'DEFAULT FACILITY NPI')
|
|
124
|
+
facility_name = config.get('service_facility_name', 'DEFAULT FACILITY NAME')
|
|
125
|
+
address_line_1 = config.get('service_facility_address', 'NO ADDRESS')
|
|
126
|
+
city = config.get('service_facility_city', 'NO CITY')
|
|
127
|
+
state = config.get('service_facility_state', 'NO STATE')
|
|
128
|
+
zip_code = config.get('service_facility_zip', 'NO ZIP')
|
|
129
|
+
|
|
130
|
+
# NM1 segment for facility identification
|
|
131
|
+
nm1_segment = "NM1*77*2*{}*****XX*{}~".format(facility_name, facility_npi)
|
|
132
|
+
# N3 segment for facility address
|
|
133
|
+
n3_segment = "N3*{}~".format(address_line_1)
|
|
134
|
+
# N4 segment for facility city, state, and ZIP
|
|
135
|
+
n4_segment = "N4*{}*{}*{}~".format(city, state, zip_code)
|
|
136
|
+
|
|
137
|
+
return [nm1_segment, n3_segment, n4_segment]
|
|
138
|
+
|
|
139
|
+
# Constructs the NM1 segment for submitter name and includes PER segment for contact information.
|
|
140
|
+
def create_1000A_submitter_name_segment(config, endpoint):
|
|
141
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
142
|
+
submitter_id_qualifier = endpoint_config.get('submitter_id_qualifier', '46') # '46' for ETIN or 'XX' for NPI
|
|
143
|
+
submitter_name = endpoint_config.get('nm_103_value', 'DEFAULT NAME') # Default name if not in config
|
|
144
|
+
submitter_id = endpoint_config.get('nm_109_value', 'DEFAULT ID') # Default ID if not specified in endpoint
|
|
145
|
+
|
|
146
|
+
# Submitter contact details
|
|
147
|
+
contact_name = config.get('submitter_contact_name', 'RAFAEL OLIVERVIDAUD')
|
|
148
|
+
contact_telephone_number = config.get('submitter_contact_tel', '9543821782')
|
|
149
|
+
|
|
150
|
+
# Construct NM1 segment for the submitter
|
|
151
|
+
nm1_segment = "NM1*41*2*{}*****{}*{}~".format(submitter_name, submitter_id_qualifier, submitter_id)
|
|
152
|
+
|
|
153
|
+
# Construct PER segment for the submitter's contact information
|
|
154
|
+
per_segment = "PER*IC*{}*TE*{}~".format(contact_name, contact_telephone_number)
|
|
155
|
+
|
|
156
|
+
return [nm1_segment, per_segment]
|
|
157
|
+
|
|
158
|
+
# Constructs the NM1 segment for the receiver (1000B).
|
|
159
|
+
def create_1000B_receiver_name_segment(config, endpoint):
|
|
160
|
+
# Retrieve endpoint specific configuration
|
|
161
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
162
|
+
|
|
163
|
+
# Set the entity identifier code to '40' for receiver and qualifier to '46' for EDI,
|
|
164
|
+
# unless specified differently in the endpoint configuration.
|
|
165
|
+
receiver_entity_code = '40'
|
|
166
|
+
receiver_id_qualifier = endpoint_config.get('receiver_id_qualifier', '46')
|
|
167
|
+
receiver_name = endpoint_config.get('receiver_name', 'DEFAULT RECEIVER NAME')
|
|
168
|
+
receiver_edi = endpoint_config.get('receiver_edi', 'DEFAULT EDI')
|
|
169
|
+
|
|
170
|
+
return "NM1*{entity_code}*2*{receiver_name}*****{id_qualifier}*{receiver_edi}~".format(
|
|
171
|
+
entity_code=receiver_entity_code,
|
|
172
|
+
receiver_name=receiver_name,
|
|
173
|
+
id_qualifier=receiver_id_qualifier,
|
|
174
|
+
receiver_edi=receiver_edi
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def map_insurance_name_to_payer_id(insurance_name, config):
|
|
178
|
+
"""
|
|
179
|
+
Maps the insurance name provided by Medisoft to the corresponding payer ID by referencing
|
|
180
|
+
a crosswalk mapping stored in the "crosswalk" JSON file. This file must be located at a path
|
|
181
|
+
specified in the config under the key 'crosswalk_path'.
|
|
182
|
+
|
|
183
|
+
Inputs:
|
|
184
|
+
- insurance_name: The name of the insurance as provided by Medisoft.
|
|
185
|
+
|
|
186
|
+
Outputs:
|
|
187
|
+
- payer_id: The unique identifier (ID) corresponding to the insurance name, if the mapping
|
|
188
|
+
is successful. It will be a 5-character alphanumeric code.
|
|
189
|
+
|
|
190
|
+
Functionality:
|
|
191
|
+
- This function retrieves the mapping from a "crosswalk" JSON file.
|
|
192
|
+
- If the mapping is successful, it returns the payer ID.
|
|
193
|
+
- If the mapping fails or the payer ID cannot be retrieved, it raises an error indicating
|
|
194
|
+
the inability to extract the payer ID.
|
|
195
|
+
"""
|
|
196
|
+
_, crosswalk = MediLink_ConfigLoader.load_configuration(None, config.get('crosswalkPath', 'crosswalk.json'))
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
payer_id = crosswalk['medisoft_insurance_to_payer_id'].get(insurance_name)
|
|
200
|
+
if not payer_id:
|
|
201
|
+
raise ValueError("No payer ID found for insurance name: {}".format(insurance_name))
|
|
202
|
+
return payer_id
|
|
203
|
+
except json.JSONDecodeError:
|
|
204
|
+
raise ValueError("Error decoding the crosswalk JSON file")
|
|
205
|
+
|
|
206
|
+
def fetch_payer_name_from_crosswalk(payer_id, config, endpoint):
|
|
207
|
+
"""
|
|
208
|
+
Retrieves the payer name corresponding to a given payer ID from a crosswalk mapping.
|
|
209
|
+
|
|
210
|
+
Parameters:
|
|
211
|
+
- payer_id: The unique identifier for the payer whose name is to be resolved.
|
|
212
|
+
- config: Configuration dictionary including the path to the crosswalk JSON.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
- The payer name corresponding to the payer ID.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
- ValueError if the payer ID is not found in the crosswalk mapping.
|
|
219
|
+
"""
|
|
220
|
+
# Ensure the 'crosswalkPath' key exists in config and contains the path to the crosswalk JSON file
|
|
221
|
+
if 'crosswalkPath' not in config:
|
|
222
|
+
raise ValueError("Crosswalk path not defined in configuration.")
|
|
223
|
+
|
|
224
|
+
crosswalk_path = config.get('crosswalkPath')
|
|
225
|
+
try:
|
|
226
|
+
with open(crosswalk_path, 'r') as file:
|
|
227
|
+
crosswalk_mappings = json.load(file)
|
|
228
|
+
# Select the endpoint-specific section from the mappings
|
|
229
|
+
endpoint_mappings = crosswalk_mappings['payer_id_to_name'].get(endpoint, {})
|
|
230
|
+
payer_name = endpoint_mappings.get(payer_id)
|
|
231
|
+
|
|
232
|
+
if not payer_name:
|
|
233
|
+
raise ValueError("Payer name not found for payer ID: {} in {} mappings.".format(payer_id, endpoint))
|
|
234
|
+
|
|
235
|
+
return payer_name
|
|
236
|
+
except FileNotFoundError:
|
|
237
|
+
raise FileNotFoundError("Crosswalk file not found at {}".format(crosswalk_path))
|
|
238
|
+
except json.JSONDecodeError as e:
|
|
239
|
+
raise ValueError("Error decoding the crosswalk JSON file: {}".format(e))
|
|
240
|
+
|
|
241
|
+
def create_2010BB_payer_information_segment(parsed_data, config, endpoint):
|
|
242
|
+
"""
|
|
243
|
+
Dynamically generates the NM1 segment for the payer in the 2010BB loop of an 837P transaction.
|
|
244
|
+
This process involves mapping the insurance name to the correct payer ID and then resolving the payer name.
|
|
245
|
+
The function prioritizes the Availity API for payer name resolution (currently the only API integrated)
|
|
246
|
+
and falls back to crosswalk mapping if necessary or when additional APIs are not available.
|
|
247
|
+
|
|
248
|
+
Parameters:
|
|
249
|
+
- parsed_data: Dictionary containing parsed claim data, including the insurance name.
|
|
250
|
+
- config: Configuration dictionary including endpoint-specific settings and crosswalk paths.
|
|
251
|
+
- endpoint: Target endpoint for submission, e.g., 'Availity'. Future integrations may include 'Optum', 'PNT_DATA'.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
- A formatted NM1*PR segment string for the 2010BB loop, correctly identifying the payer with its name and ID.
|
|
255
|
+
|
|
256
|
+
Process:
|
|
257
|
+
1. Maps the insurance name to a payer ID using a crosswalk mapping or configuration.
|
|
258
|
+
2. Attempts to resolve the payer name using the Availity API based on the payer ID.
|
|
259
|
+
3. Falls back to crosswalk mapping for payer name resolution if the API call fails or is not applicable.
|
|
260
|
+
|
|
261
|
+
Raises an error if the payer ID extraction or payer name resolution fails, ensuring transaction integrity.
|
|
262
|
+
|
|
263
|
+
A reference for a payer ID to payer name mapping for Optum can be found here:
|
|
264
|
+
https://iedi.optum.com/iedi/enspublic/Download/Payerlists/Medicalpayerlist.pdf
|
|
265
|
+
This or similar resources could be used to populate the initial configuration mapping.
|
|
266
|
+
"""
|
|
267
|
+
# Step 1: Map insurance name to Payer ID
|
|
268
|
+
insurance_name = parsed_data.get('INAME', '')
|
|
269
|
+
payer_id = map_insurance_name_to_payer_id(insurance_name, config)
|
|
270
|
+
|
|
271
|
+
payer_name = ''
|
|
272
|
+
# Step 2: Attempt to Retrieve Payer Name from Availity API (current API integration)
|
|
273
|
+
endpoint = 'availity' # Force availity API because none of the other ones are connected BUG
|
|
274
|
+
if True: #endpoint.lower() == 'availity':
|
|
275
|
+
try:
|
|
276
|
+
payer_name = fetch_payer_name_from_api(payer_id, config, endpoint)
|
|
277
|
+
log("Resolved payer name {} via {} API for Payer ID: {}".format(payer_name, endpoint, payer_id), config, level="INFO")
|
|
278
|
+
except Exception as api_error:
|
|
279
|
+
print("{} API call failed for Payer ID '{}': {}".format(endpoint, payer_id, api_error))
|
|
280
|
+
|
|
281
|
+
# Placeholder for future API integrations with other endpoints
|
|
282
|
+
# elif endpoint.lower() == 'optum':
|
|
283
|
+
# payer_name = fetch_payer_name_from_optum_api(payer_id, config)
|
|
284
|
+
# elif endpoint.lower() == 'pnt_data':
|
|
285
|
+
# payer_name = fetch_payer_name_from_pnt_data_api(payer_id, config)
|
|
286
|
+
|
|
287
|
+
# Step 3: Fallback to Crosswalk Mapping if API resolution fails or is not applicable
|
|
288
|
+
if not payer_name:
|
|
289
|
+
payer_name = fetch_payer_name_from_crosswalk(payer_id, config, endpoint)
|
|
290
|
+
|
|
291
|
+
# Construct and return the NM1*PR segment for the 2010BB loop with the payer name and ID
|
|
292
|
+
# Is this supposed to be PI instead of PR?
|
|
293
|
+
return "NM1*PR*2*{}*****PI*{}~".format(payer_name, payer_id)
|
|
294
|
+
|
|
295
|
+
def create_nm1_payto_address_segments(config):
|
|
296
|
+
"""
|
|
297
|
+
Constructs the NM1 segment for the Pay-To Address, N3 for street address, and N4 for city, state, and ZIP.
|
|
298
|
+
This is used if the Pay-To Address is different from the Billing Provider Address.
|
|
299
|
+
"""
|
|
300
|
+
payto_provider_name = config.get('payto_provider_name', 'DEFAULT PAY-TO NAME')
|
|
301
|
+
payto_address = config.get('payto_address', 'DEFAULT PAY-TO ADDRESS')
|
|
302
|
+
payto_city = config.get('payto_city', 'DEFAULT PAY-TO CITY')
|
|
303
|
+
payto_state = config.get('payto_state', 'DEFAULT PAY-TO STATE')
|
|
304
|
+
payto_zip = config.get('payto_zip', 'DEFAULT PAY-TO ZIP')
|
|
305
|
+
|
|
306
|
+
nm1_segment = "NM1*87*2*{}~".format(payto_provider_name) # '87' indicates Pay-To Provider
|
|
307
|
+
n3_segment = "N3*{}~".format(payto_address)
|
|
308
|
+
n4_segment = "N4*{}*{}*{}~".format(payto_city, payto_state, payto_zip)
|
|
309
|
+
|
|
310
|
+
return [nm1_segment, n3_segment, n4_segment]
|
|
311
|
+
|
|
312
|
+
def create_payer_address_segments(config):
|
|
313
|
+
"""
|
|
314
|
+
Constructs the N3 and N4 segments for the payer's address.
|
|
315
|
+
|
|
316
|
+
"""
|
|
317
|
+
payer_address_line_1 = config.get('payer_address_line_1', '')
|
|
318
|
+
payer_city = config.get('payer_city', '')
|
|
319
|
+
payer_state = config.get('payer_state', '')
|
|
320
|
+
payer_zip = config.get('payer_zip', '')
|
|
321
|
+
|
|
322
|
+
n3_segment = "N3*{}~".format(payer_address_line_1)
|
|
323
|
+
n4_segment = "N4*{}*{}*{}~".format(payer_city, payer_state, payer_zip)
|
|
324
|
+
|
|
325
|
+
return [n3_segment, n4_segment]
|
|
326
|
+
|
|
327
|
+
# Fetches the payer name from API based on the payer ID.
|
|
328
|
+
# Initialize a global dictionary to store the access token and its expiry time
|
|
329
|
+
# BUG This will need to get setup for each endpoint separately.
|
|
330
|
+
token_cache = {
|
|
331
|
+
'access_token': None,
|
|
332
|
+
'expires_at': 0 # Timestamp of when the token expires
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
def get_access_token(endpoint_config):
|
|
336
|
+
current_time = time.time()
|
|
337
|
+
|
|
338
|
+
# Check if the cached token is still valid
|
|
339
|
+
if token_cache['access_token'] and token_cache['expires_at'] > current_time:
|
|
340
|
+
return token_cache['access_token']
|
|
341
|
+
|
|
342
|
+
# Validate endpoint configuration
|
|
343
|
+
if not endpoint_config or not all(k in endpoint_config for k in ['client_id', 'client_secret', 'token_url']):
|
|
344
|
+
raise ValueError("Endpoint configuration is incomplete or missing necessary fields.")
|
|
345
|
+
|
|
346
|
+
# Extract credentials and URL from the config
|
|
347
|
+
CLIENT_ID = endpoint_config.get("client_id")
|
|
348
|
+
CLIENT_SECRET = endpoint_config.get("client_secret")
|
|
349
|
+
url = endpoint_config.get("token_url")
|
|
350
|
+
|
|
351
|
+
# Setup the data payload and headers for the HTTP request
|
|
352
|
+
data = {
|
|
353
|
+
'grant_type': 'client_credentials',
|
|
354
|
+
'client_id': CLIENT_ID,
|
|
355
|
+
'client_secret': CLIENT_SECRET,
|
|
356
|
+
'scope': 'hipaa'
|
|
357
|
+
}
|
|
358
|
+
headers = {
|
|
359
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
# Perform the HTTP request to get the access token
|
|
364
|
+
response = requests.post(url, headers=headers, data=data)
|
|
365
|
+
response.raise_for_status() # This will raise an exception for HTTP error statuses
|
|
366
|
+
json_response = response.json()
|
|
367
|
+
access_token = json_response.get('access_token')
|
|
368
|
+
expires_in = json_response.get('expires_in', 3600) # Default to 3600 seconds if not provided
|
|
369
|
+
|
|
370
|
+
if not access_token:
|
|
371
|
+
raise ValueError("No access token returned by the server.")
|
|
372
|
+
|
|
373
|
+
# Store the access token and calculate the expiry time
|
|
374
|
+
token_cache['access_token'] = access_token
|
|
375
|
+
token_cache['expires_at'] = current_time + expires_in - 120 # Subtracting 120 seconds to provide buffer
|
|
376
|
+
|
|
377
|
+
return access_token
|
|
378
|
+
except requests.RequestException as e:
|
|
379
|
+
# Handle HTTP errors (e.g., network problems, invalid response)
|
|
380
|
+
error_msg = "Failed to retrieve access token: {0}. Response status: {1}".format(str(e), response.status_code if response else 'No response')
|
|
381
|
+
raise Exception(error_msg)
|
|
382
|
+
except ValueError as e:
|
|
383
|
+
# Handle specific errors like missing access token
|
|
384
|
+
raise Exception("Configuration or server response error: {0}".format(str(e)))
|
|
385
|
+
|
|
386
|
+
def fetch_payer_name_from_api(payer_id, config, endpoint):
|
|
387
|
+
|
|
388
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
389
|
+
# Currently configured for Availity
|
|
390
|
+
token = get_access_token(endpoint_config) # Get the access token using the function above
|
|
391
|
+
|
|
392
|
+
api_url = endpoint_config.get("api_url", "")
|
|
393
|
+
headers = {
|
|
394
|
+
'Authorization': 'Bearer {0}'.format(token),
|
|
395
|
+
'Accept': 'application/json'
|
|
396
|
+
}
|
|
397
|
+
params = {'payerId': payer_id}
|
|
398
|
+
# print(params) DEBUG
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
response = requests.get(api_url, headers=headers, params=params)
|
|
402
|
+
response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code
|
|
403
|
+
data = response.json()
|
|
404
|
+
# print(data) DEBUG
|
|
405
|
+
|
|
406
|
+
# Check if 'payers' key exists and has at least one item
|
|
407
|
+
if 'payers' in data and data['payers']:
|
|
408
|
+
payer = data['payers'][0]
|
|
409
|
+
payer_name = payer.get('displayName') or payer.get('name', 'No name available')
|
|
410
|
+
return payer_name
|
|
411
|
+
else:
|
|
412
|
+
raise ValueError("No payer found for ID: {0}".format(payer_id))
|
|
413
|
+
except requests.RequestException as e:
|
|
414
|
+
print("Error fetching payer name: {0}".format(e))
|
|
415
|
+
raise
|
|
416
|
+
|
|
417
|
+
# Test Case for API fetch
|
|
418
|
+
#payer_id = "11347"
|
|
419
|
+
#config = load_configuration()
|
|
420
|
+
#payer_name = fetch_payer_name_from_api(payer_id, config, endpoint='AVAILITY')
|
|
421
|
+
#print(payer_id, payer_name)
|
|
422
|
+
|
|
423
|
+
# Constructs the PRV segment for billing provider.
|
|
424
|
+
def create_billing_prv_segment(config, endpoint):
|
|
425
|
+
if endpoint.lower() == 'optumedi':
|
|
426
|
+
return "PRV*BI*PXC*{}~".format(config['billing_provider_taxonomy'])
|
|
427
|
+
return ""
|
|
428
|
+
|
|
429
|
+
# Constructs the HL segment for subscriber [hierarchical level (HL*2)]
|
|
430
|
+
def create_hl_subscriber_segment():
|
|
431
|
+
return ["HL*2*1*22*0~"]
|
|
432
|
+
|
|
433
|
+
# Constructs the SBR segment for subscriber based on parsed data and configuration.
|
|
434
|
+
def create_sbr_segment(config, parsed_data, endpoint):
|
|
435
|
+
# Determine the payer responsibility sequence number code based on the payer type
|
|
436
|
+
# If the payer is Medicare, use 'P' (Primary)
|
|
437
|
+
# If the payer is not Medicare and is primary insurance, use 'P' (Primary)
|
|
438
|
+
# If the payer is secondary insurance after Medicare, use 'S' (Secondary)
|
|
439
|
+
# For simplicity, we're going to assume everything is Primary for now.
|
|
440
|
+
responsibility_code = 'P'
|
|
441
|
+
|
|
442
|
+
# Determine the insurance type code based on the payer
|
|
443
|
+
# Map different payers to their respective insurance type codes
|
|
444
|
+
# For simplicity, 'CI' (Commercial Insurance) as a default
|
|
445
|
+
insurance_type_code = 'CI'
|
|
446
|
+
|
|
447
|
+
# Construct the SBR segment using the determined codes
|
|
448
|
+
sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
|
|
449
|
+
responsibility_code=responsibility_code,
|
|
450
|
+
insurance_type_code=insurance_type_code
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return sbr_segment
|
|
454
|
+
|
|
455
|
+
# Constructs the NM1 segment for subscriber based on parsed data and configuration.
|
|
456
|
+
def create_nm1_subscriber_segment(config, parsed_data, endpoint):
|
|
457
|
+
if endpoint.lower() == 'optumedi':
|
|
458
|
+
entity_identifier_code = config['endpoints']['OPTUMEDI'].get('subscriber_entity_code', 'IL')
|
|
459
|
+
else:
|
|
460
|
+
entity_identifier_code = 'IL' # Default value if endpoint is not 'optumedi'
|
|
461
|
+
|
|
462
|
+
return "NM1*{entity_identifier_code}*1*{last_name}*{first_name}*{middle_name}***MI*{policy_number}~".format(
|
|
463
|
+
entity_identifier_code=entity_identifier_code,
|
|
464
|
+
last_name=parsed_data['LAST'],
|
|
465
|
+
first_name=parsed_data['FIRST'],
|
|
466
|
+
middle_name=parsed_data['MIDDLE'],
|
|
467
|
+
policy_number=parsed_data['IPOLICY']
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Constructs the N3 and N4 segments for subscriber address based on parsed data.
|
|
471
|
+
def create_subscriber_address_segments(parsed_data):
|
|
472
|
+
return [
|
|
473
|
+
"N3*{}~".format(parsed_data['ISTREET']),
|
|
474
|
+
"N4*{}*{}*{}~".format(
|
|
475
|
+
parsed_data['ICITY'],
|
|
476
|
+
parsed_data['ISTATE'],
|
|
477
|
+
parsed_data['IZIP'][:5]
|
|
478
|
+
)
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
# Constructs the DMG segment for subscriber based on parsed data.
|
|
482
|
+
def create_dmg_segment(parsed_data):
|
|
483
|
+
return "DMG*D8*{}*{}~".format(
|
|
484
|
+
convert_date_format(parsed_data['BDAY']),
|
|
485
|
+
parsed_data['SEX'] # ensure it returns a string instead of a list if it only returns one segment
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def create_nm1_rendering_provider_segment(config, is_rendering_provider_different=False):
|
|
489
|
+
"""
|
|
490
|
+
#BUG This is getting placed incorrectly.
|
|
491
|
+
|
|
492
|
+
Constructs the NM1 segment for the rendering provider in the 2310B loop using configuration data.
|
|
493
|
+
|
|
494
|
+
Parameters:
|
|
495
|
+
- config: Configuration dictionary including rendering provider details.
|
|
496
|
+
- is_rendering_provider_different: Boolean indicating if rendering provider differs from billing provider.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
- List containing the NM1 segment for the rendering provider if required, otherwise an empty list.
|
|
500
|
+
"""
|
|
501
|
+
if is_rendering_provider_different:
|
|
502
|
+
segments = []
|
|
503
|
+
rp_npi = config.get('rendering_provider_npi', '')
|
|
504
|
+
rp_last_name = config.get('rendering_provider_last_name', '')
|
|
505
|
+
rp_first_name = config.get('rendering_provider_first_name', '')
|
|
506
|
+
|
|
507
|
+
# NM1 Segment for Rendering Provider
|
|
508
|
+
segments.append("NM1*82*1*{0}*{1}**XX*{2}~".format(
|
|
509
|
+
rp_last_name,
|
|
510
|
+
rp_first_name,
|
|
511
|
+
rp_npi
|
|
512
|
+
))
|
|
513
|
+
|
|
514
|
+
# PRV Segment for Rendering Provider Taxonomy
|
|
515
|
+
if config.get('rendering_provider_taxonomy'):
|
|
516
|
+
segments.append("PRV*PE*PXC*{}~".format(config['billing_provider_taxonomy']))
|
|
517
|
+
|
|
518
|
+
return segments
|
|
519
|
+
else:
|
|
520
|
+
return []
|
|
521
|
+
|
|
522
|
+
def format_claim_number(chart_number, date_of_service):
|
|
523
|
+
# Remove any non-alphanumeric characters from chart number and date
|
|
524
|
+
chart_number_alphanumeric = ''.join(filter(str.isalnum, chart_number))
|
|
525
|
+
date_of_service_alphanumeric = ''.join(filter(str.isalnum, date_of_service))
|
|
526
|
+
|
|
527
|
+
# Combine the alphanumeric components without spaces
|
|
528
|
+
formatted_claim_number = chart_number_alphanumeric + date_of_service_alphanumeric
|
|
529
|
+
|
|
530
|
+
return formatted_claim_number
|
|
531
|
+
|
|
532
|
+
# Constructs the CLM and related segments based on parsed data and configuration.
|
|
533
|
+
def create_clm_and_related_segments(parsed_data, config):
|
|
534
|
+
"""
|
|
535
|
+
Insert the claim information (2300 loop),
|
|
536
|
+
ensuring that details such as claim ID, total charge amount,and service date are included.
|
|
537
|
+
|
|
538
|
+
The HI segment for Health Care Diagnosis Codes should accurately reflect the diagnosis related to the service line.
|
|
539
|
+
|
|
540
|
+
Service Line Information (2400 Loop):
|
|
541
|
+
Verify that the service line number (LX), professional service (SV1), and service date (DTP) segments contain
|
|
542
|
+
accurate information and are formatted according to the claim's details.
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
segments = []
|
|
546
|
+
|
|
547
|
+
# Format the claim number
|
|
548
|
+
chart_number = parsed_data.get('CHART', '')
|
|
549
|
+
date_of_service = parsed_data.get('DATE', '')
|
|
550
|
+
formatted_claim_number = format_claim_number(chart_number, date_of_service)
|
|
551
|
+
|
|
552
|
+
# CLM - Claim Information
|
|
553
|
+
segments.append("CLM*{}*{}***{}:B:1*Y*A*Y*Y~".format(
|
|
554
|
+
formatted_claim_number,
|
|
555
|
+
parsed_data['AMOUNT'],
|
|
556
|
+
parsed_data['TOS']))
|
|
557
|
+
|
|
558
|
+
# HI - Health Care Diagnosis Code
|
|
559
|
+
# The code value should be preceded by the qualifier "ABK" or "BK" to specify the type of
|
|
560
|
+
# diagnosis code being used. "ABK" typically indicates ICD-10 codes, while "BK" indicates ICD-9 codes.
|
|
561
|
+
segments.append("HI*ABK:H{}~".format(''.join(char for char in parsed_data['DIAG'] if char.isalnum() or char.isdigit())))
|
|
562
|
+
|
|
563
|
+
# (2310C Loop) Service Facility Location NPI and Address Information
|
|
564
|
+
segments.extend(create_service_facility_location_npi_segment(config))
|
|
565
|
+
|
|
566
|
+
# For future reference, SBR - (Loop 2320: OI, NM1 (2330A), N3, N4, NM1 (2330B)) - Other Subscriber Information goes here.
|
|
567
|
+
|
|
568
|
+
# LX - Service Line Number
|
|
569
|
+
segments.append("LX*1~")
|
|
570
|
+
|
|
571
|
+
# SV1 - Professional Service
|
|
572
|
+
segments.append("SV1*HC:{}:{}*{}*MJ*{}***1~".format(
|
|
573
|
+
parsed_data['CODEA'],
|
|
574
|
+
parsed_data['POS'],
|
|
575
|
+
parsed_data['AMOUNT'],
|
|
576
|
+
parsed_data['MINTUES']))
|
|
577
|
+
|
|
578
|
+
# DTP - Date
|
|
579
|
+
segments.append("DTP*472*D8*{}~".format(convert_date_format(parsed_data['DATE'])))
|
|
580
|
+
|
|
581
|
+
# Is there REF - Line Item Control Number missing here?
|
|
582
|
+
|
|
583
|
+
return segments
|
|
584
|
+
|
|
585
|
+
# Generates the ISA and GS segments for the interchange header based on configuration and endpoint.
|
|
586
|
+
def create_interchange_header(config, endpoint):
|
|
587
|
+
"""
|
|
588
|
+
Generate ISA and GS segments for the interchange header, ensuring endpoint-specific requirements are met.
|
|
589
|
+
Includes support for Availity, Optum, and PNT_DATA endpoints, with streamlined configuration and default handling.
|
|
590
|
+
|
|
591
|
+
Parameters:
|
|
592
|
+
- config: Configuration dictionary with settings and identifiers.
|
|
593
|
+
- endpoint: String indicating the target endpoint ('Availity', 'Optum', 'PNT_DATA').
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
- Tuple containing the ISA and GS segment strings.
|
|
597
|
+
"""
|
|
598
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
599
|
+
|
|
600
|
+
# Set defaults for ISA segment values
|
|
601
|
+
isa02 = isa04 = " " # Default value for ISA02 and ISA04
|
|
602
|
+
isa05 = isa07 = 'ZZ' # Default qualifier
|
|
603
|
+
isa13 = '000000001' # Default Interchange Control Number. Not sure what to do with this. date? See also trailer
|
|
604
|
+
isa15 = 'P' # 'T' for Test, 'P' for Production
|
|
605
|
+
|
|
606
|
+
# Conditional values from config
|
|
607
|
+
isa_sender_id = endpoint_config.get('isa_06_value', config.get('submitterId', '')).rstrip()
|
|
608
|
+
isa07_value = endpoint_config.get('isa_07_value', isa07)
|
|
609
|
+
isa_receiver_id = endpoint_config.get('isa_08_value', config.get('receiverId', '')).rstrip()
|
|
610
|
+
isa13_value = endpoint_config.get('isa_13_value', isa13) # Needs to match IEA02
|
|
611
|
+
gs_sender_code = endpoint_config.get('gs_02_value', config.get('submitterEdi', ''))
|
|
612
|
+
gs_receiver_code = endpoint_config.get('gs_03_value', config.get('receiverEdi', ''))
|
|
613
|
+
isa15_value = endpoint_config.get('isa_15_value', isa15)
|
|
614
|
+
|
|
615
|
+
# ISA Segment
|
|
616
|
+
isa_segment = "ISA*00*{}*00*{}*{}*{}*{}*{}*{}*{}*^*00501*{}*0*{}*:~".format(
|
|
617
|
+
isa02, isa04, isa05, isa_sender_id.ljust(15), isa07_value, isa_receiver_id.ljust(15),
|
|
618
|
+
format_datetime(format_type='isa'), format_datetime(format_type='time'), isa13_value, isa15_value
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# GS Segment
|
|
622
|
+
# GS04 YYYYMMDD
|
|
623
|
+
# GS06 Group Control Number, Field Length 1/9, must match GE02
|
|
624
|
+
gs06 = '1' # Placeholder for now?
|
|
625
|
+
|
|
626
|
+
gs_segment = "GS*HC*{}*{}*{}*{}*{}*X*005010X222A1~".format(
|
|
627
|
+
gs_sender_code, gs_receiver_code, format_datetime(), format_datetime(format_type='time'), gs06
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
log("Created interchange header for endpoint: {}".format(endpoint), config, level="INFO")
|
|
631
|
+
|
|
632
|
+
return isa_segment, gs_segment
|
|
633
|
+
|
|
634
|
+
# Generates the GE and IEA segments for the interchange trailer based on the number of transactions and functional groups.
|
|
635
|
+
def create_interchange_trailer(config, endpoint, num_transactions, num_functional_groups=1):
|
|
636
|
+
"""
|
|
637
|
+
Generate GE and IEA segments for the interchange trailer.
|
|
638
|
+
|
|
639
|
+
Parameters:
|
|
640
|
+
- num_transactions: The number of transactions within the functional group.
|
|
641
|
+
- num_functional_groups: The number of functional groups within the interchange. Default is 1.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
- Tuple containing the GE and IEA segment strings.
|
|
645
|
+
"""
|
|
646
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
647
|
+
# GE Segment: Functional Group Trailer
|
|
648
|
+
# Indicates the end of a functional group and provides the count of the number of transactions within it.
|
|
649
|
+
# GE02 Group Control Number, Field Length 1/9, must match GS06 (Header)
|
|
650
|
+
ge02 = '1' #Placeholder for now?
|
|
651
|
+
ge_segment = "GE*{}*{}~".format(num_transactions, ge02)
|
|
652
|
+
|
|
653
|
+
# IEA Segment: Interchange Control Trailer
|
|
654
|
+
isa13 = '000000001' # Default Interchange Control Number. Not sure what to do with this. date? See also trailer
|
|
655
|
+
isa13_value = endpoint_config.get('isa_13_value', isa13) # Needs to match IEA02, can this be a date/time?
|
|
656
|
+
# Indicates the end of an interchange and provides the count of the number of functional groups within it.
|
|
657
|
+
# The Interchange Control Number needs to match isa13 and iea02
|
|
658
|
+
iea_segment = "IEA*{}*{}~".format(num_functional_groups, isa13_value)
|
|
659
|
+
|
|
660
|
+
log("Created interchange trailer", config, level="INFO")
|
|
661
|
+
|
|
662
|
+
return ge_segment, iea_segment
|
|
663
|
+
|
|
664
|
+
# Generates segment counts for the formatted 837P transaction and updates SE segment.
|
|
665
|
+
def generate_segment_counts(compiled_segments, transaction_set_control_number):
|
|
666
|
+
# Count the number of segments, not including the placeholder SE segment
|
|
667
|
+
segment_count = compiled_segments.count('~') # + 1 Including SE segment itself, but seems to be giving errors.
|
|
668
|
+
|
|
669
|
+
# Ensure transaction set control number is correctly formatted as a string
|
|
670
|
+
formatted_control_number = str(transaction_set_control_number).zfill(4) # Pad to ensure minimum 4 characters
|
|
671
|
+
|
|
672
|
+
# Construct the SE segment with the actual segment count and the formatted transaction set control_number
|
|
673
|
+
se_segment = "SE*{0}*{1}~".format(segment_count, formatted_control_number)
|
|
674
|
+
|
|
675
|
+
# Assuming the placeholder SE segment was the last segment added before compiling
|
|
676
|
+
# This time, we directly replace the placeholder with the correct SE segment
|
|
677
|
+
formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
|
|
678
|
+
|
|
679
|
+
return formatted_837p
|