medicafe 0.240419.2__py3-none-any.whl → 0.240517.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 +166 -38
- MediBot/MediBot.py +74 -44
- MediBot/MediBot_Crosswalk_Library.py +280 -0
- MediBot/MediBot_Preprocessor.py +155 -191
- MediBot/MediBot_Preprocessor_lib.py +357 -0
- MediBot/MediBot_UI.py +80 -30
- MediBot/MediBot_dataformat_library.py +88 -35
- MediBot/MediBot_docx_decoder.py +80 -0
- MediBot/update_medicafe.py +46 -8
- MediLink/MediLink.py +138 -34
- MediLink/MediLink_837p_encoder.py +319 -209
- MediLink/MediLink_837p_encoder_library.py +453 -242
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +137 -0
- MediLink/MediLink_ConfigLoader.py +44 -32
- MediLink/MediLink_DataMgmt.py +85 -33
- MediLink/MediLink_Down.py +12 -35
- MediLink/MediLink_ERA_decoder.py +4 -4
- MediLink/MediLink_Gmail.py +99 -3
- MediLink/MediLink_Mailer.py +7 -0
- MediLink/MediLink_Scheduler.py +41 -0
- MediLink/MediLink_UI.py +19 -17
- MediLink/MediLink_Up.py +297 -31
- MediLink/MediLink_batch.bat +1 -1
- MediLink/test.py +74 -0
- medicafe-0.240517.0.dist-info/METADATA +53 -0
- medicafe-0.240517.0.dist-info/RECORD +39 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +5 -5
- medicafe-0.240419.2.dist-info/METADATA +0 -19
- medicafe-0.240419.2.dist-info/RECORD +0 -32
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/top_level.txt +0 -0
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
import json
|
|
3
|
-
import requests
|
|
4
|
-
import time
|
|
5
2
|
import sys
|
|
6
|
-
import MediLink_ConfigLoader
|
|
3
|
+
from MediLink import MediLink_ConfigLoader
|
|
4
|
+
|
|
5
|
+
# Add parent directory of the project to the Python path
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
9
|
+
sys.path.append(project_dir)
|
|
10
|
+
|
|
11
|
+
from MediBot import MediBot_Preprocessor_lib
|
|
12
|
+
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
13
|
+
from MediBot import MediBot_Crosswalk_Library
|
|
14
|
+
from MediLink_API_v2 import fetch_payer_name_from_api
|
|
7
15
|
|
|
8
16
|
"""
|
|
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".
|
|
17
|
+
- [ ] 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.
|
|
18
|
+
- [ ] 2. Endpoint Support: Extend support within segment creation for additional endpoints with attention to their unique claim submission requirements.
|
|
19
|
+
- [ ] 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.
|
|
20
|
+
- [ ] 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.
|
|
21
|
+
- [ ] 5. De-persisting Intermediate Files.
|
|
22
|
+
- [ ] 6. Get an API for Optum "Entered As".
|
|
23
|
+
- [ ] 7. (MED) Add Authorization Number
|
|
24
|
+
- [X] 8. (HIGH) Interchange number should be 000HHMMSS instead of fixed constant.
|
|
25
|
+
- [ ] 9. Upgrade Interchange formation to consolidate the batch processor and also de-risk batch
|
|
26
|
+
interchange number matching for multiple .DAT.
|
|
15
27
|
"""
|
|
16
28
|
|
|
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
29
|
# Converts date format from one format to another.
|
|
29
30
|
def convert_date_format(date_str):
|
|
30
31
|
# Parse the input date string into a datetime object using the input format
|
|
@@ -137,15 +138,34 @@ def create_service_facility_location_npi_segment(config):
|
|
|
137
138
|
return [nm1_segment, n3_segment, n4_segment]
|
|
138
139
|
|
|
139
140
|
# Constructs the NM1 segment for submitter name and includes PER segment for contact information.
|
|
140
|
-
def create_1000A_submitter_name_segment(config, endpoint):
|
|
141
|
+
def create_1000A_submitter_name_segment(patient_data, config, endpoint):
|
|
142
|
+
"""
|
|
143
|
+
Creates the 1000A submitter name segment, including the PER segment for contact information.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
patient_data (dict): Enriched patient data containing payer information.
|
|
147
|
+
config (dict): Configuration settings.
|
|
148
|
+
endpoint (str): Intended Endpoint for resolving submitter information.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
list: A list containing the NM1 segment for the submitter name and the PER segment for contact information.
|
|
152
|
+
"""
|
|
141
153
|
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
142
154
|
submitter_id_qualifier = endpoint_config.get('submitter_id_qualifier', '46') # '46' for ETIN or 'XX' for NPI
|
|
143
155
|
submitter_name = endpoint_config.get('nm_103_value', 'DEFAULT NAME') # Default name if not in config
|
|
144
|
-
|
|
156
|
+
|
|
157
|
+
# Extract payer_id from patient_data
|
|
158
|
+
payer_id = patient_data.get('payer_id', '')
|
|
159
|
+
|
|
160
|
+
# Check if payer_id is Florida Blue (00590 or BCBSF) and assign submitter_id accordingly
|
|
161
|
+
if payer_id in ['00590', 'BCBSF']:
|
|
162
|
+
submitter_id = endpoint_config.get('nm_109_bcbsf', 'DEFAULT BCBSF ID')
|
|
163
|
+
else:
|
|
164
|
+
submitter_id = endpoint_config.get('nm_109_value', 'DEFAULT ID') # Default ID if not specified in endpoint
|
|
145
165
|
|
|
146
166
|
# Submitter contact details
|
|
147
|
-
contact_name = config.get('
|
|
148
|
-
contact_telephone_number = config.get('
|
|
167
|
+
contact_name = config.get('submitter_name', 'NONE')
|
|
168
|
+
contact_telephone_number = config.get('submitter_tel', 'NONE')
|
|
149
169
|
|
|
150
170
|
# Construct NM1 segment for the submitter
|
|
151
171
|
nm1_segment = "NM1*41*2*{}*****{}*{}~".format(submitter_name, submitter_id_qualifier, submitter_id)
|
|
@@ -174,123 +194,240 @@ def create_1000B_receiver_name_segment(config, endpoint):
|
|
|
174
194
|
receiver_edi=receiver_edi
|
|
175
195
|
)
|
|
176
196
|
|
|
177
|
-
def
|
|
197
|
+
def payer_id_to_payer_name(parsed_data, config, endpoint):
|
|
178
198
|
"""
|
|
179
|
-
|
|
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'.
|
|
199
|
+
Preprocesses payer information from parsed data and enriches parsed_data with the payer name and ID.
|
|
182
200
|
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
Args:
|
|
202
|
+
parsed_data (dict): Parsed data containing Z-dat information.
|
|
203
|
+
config (dict): Configuration settings.
|
|
204
|
+
endpoint (str): Intended Endpoint for resolving payer information.
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
206
|
+
Returns:
|
|
207
|
+
dict: Enriched parsed data with payer name and payer ID.
|
|
208
|
+
"""
|
|
209
|
+
# Step 1: Extract insurance name from parsed data
|
|
210
|
+
insurance_name = parsed_data.get('INAME', '')
|
|
189
211
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
212
|
+
# Step 2: Map insurance name to payer ID
|
|
213
|
+
payer_id = map_insurance_name_to_payer_id(insurance_name, config)
|
|
214
|
+
|
|
215
|
+
# Step 3: Validate payer_id
|
|
216
|
+
if payer_id is None:
|
|
217
|
+
error_message = "Payer ID for '{}' cannot be None.".format(insurance_name)
|
|
218
|
+
MediLink_ConfigLoader.log(error_message, level="WARNING")
|
|
219
|
+
raise ValueError(error_message)
|
|
220
|
+
|
|
221
|
+
# Step 4: Resolve payer name using payer ID
|
|
222
|
+
payer_name = resolve_payer_name(payer_id, config, endpoint, insurance_name, parsed_data)
|
|
223
|
+
|
|
224
|
+
# Enrich parsed_data with payer name and payer ID
|
|
225
|
+
parsed_data['payer_name'] = payer_name
|
|
226
|
+
parsed_data['payer_id'] = payer_id
|
|
227
|
+
|
|
228
|
+
return parsed_data
|
|
229
|
+
|
|
230
|
+
# Then you can use the enriched parsed_data in your main function
|
|
231
|
+
def create_2010BB_payer_information_segment(parsed_data):
|
|
195
232
|
"""
|
|
196
|
-
|
|
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")
|
|
233
|
+
Creates the 2010BB payer information segment.
|
|
205
234
|
|
|
206
|
-
|
|
235
|
+
Args:
|
|
236
|
+
parsed_data (dict): Parsed data containing enriched payer information.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
str: The 2010BB payer information segment.
|
|
207
240
|
"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
# Extract enriched payer name and payer ID
|
|
242
|
+
payer_name = parsed_data.get('payer_name')
|
|
243
|
+
payer_id = parsed_data.get('payer_id')
|
|
244
|
+
|
|
245
|
+
# Validate payer_name and payer_id
|
|
246
|
+
if not payer_name or not payer_id:
|
|
247
|
+
error_message = "Payer name and payer ID must be provided."
|
|
248
|
+
raise ValueError(error_message)
|
|
249
|
+
|
|
250
|
+
# Build NM1 segment using provided payer name and payer ID
|
|
251
|
+
return build_nm1_segment(payer_name, payer_id)
|
|
252
|
+
|
|
253
|
+
def resolve_payer_name(payer_id, config, primary_endpoint, insurance_name, parsed_data):
|
|
254
|
+
"""
|
|
255
|
+
Resolves the payer name using the provided payer ID.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
payer_id (str): The ID of the payer.
|
|
259
|
+
config (dict): Configuration settings.
|
|
260
|
+
primary_endpoint (str): The primary endpoint for resolving payer information.
|
|
261
|
+
insurance_name (str): The name of the insurance.
|
|
262
|
+
parsed_data (dict): Parsed data containing patient information.
|
|
263
|
+
|
|
214
264
|
Returns:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
Raises:
|
|
218
|
-
- ValueError if the payer ID is not found in the crosswalk mapping.
|
|
265
|
+
str: The resolved payer name.
|
|
219
266
|
"""
|
|
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
267
|
|
|
224
|
-
|
|
268
|
+
# Step 1: Attempt to fetch payer name from API using primary endpoint
|
|
269
|
+
MediLink_ConfigLoader.log("Attempting to resolve Payer ID {} via API.".format(payer_id), level="INFO")
|
|
225
270
|
try:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
return fetch_payer_name_from_api(payer_id, config, primary_endpoint)
|
|
272
|
+
except Exception as api_error:
|
|
273
|
+
# Step 2: Log API resolution failure and initiate user intervention
|
|
274
|
+
MediLink_ConfigLoader.log("API resolution failed for {}: {}. Initiating user intervention.".format(payer_id, str(api_error)), config, level="WARNING")
|
|
275
|
+
|
|
276
|
+
# Step 3: Print warning message for user intervention
|
|
277
|
+
print("\n\nWARNING: Unable to verify Payer ID '{}' for patient '{}'!".format(payer_id, parsed_data.get('CHART', 'unknown')))
|
|
278
|
+
print(" Claims for '{}' may be incorrectly\nrouted or fail without intervention.".format(insurance_name))
|
|
279
|
+
print("\nACTION REQUIRED: Please verify the internet connection and the Payer ID by searching for it")
|
|
280
|
+
print("at the expected endpoint's website or using Google.")
|
|
281
|
+
print("\nNote: If the Payer ID '{}' is incorrect for '{}', \nit may need to be manually corrected.".format(payer_id, insurance_name))
|
|
282
|
+
print("If the Payer ID appears correct, you may skip the correction and force-continue with this one.")
|
|
283
|
+
print("\nPlease check the Payer ID in the Crosswalk and the initial \ndata source (e.g., Carol's CSV) as needed.")
|
|
284
|
+
print("If unsure, llamar a Dani for guidance on manual corrections.")
|
|
285
|
+
|
|
286
|
+
# Step 4: Integrate user input logic
|
|
287
|
+
user_decision = input("\nType 'FORCE' to force-continue with the Medisoft name, \nor press Enter to pause processing and make corrections: ")
|
|
288
|
+
user_decision = user_decision.strip().lower() # Convert to lowercase and remove leading/trailing whitespace
|
|
289
|
+
if user_decision == 'force':
|
|
290
|
+
# Step 5: Fallback to truncated insurance name
|
|
291
|
+
truncated_name = insurance_name[:10] # Temporary fallback
|
|
292
|
+
MediLink_ConfigLoader.log("Using truncated insurance name '{}' as a fallback for {}".format(truncated_name, payer_id), config, level="WARNING")
|
|
293
|
+
return truncated_name
|
|
294
|
+
elif not user_decision: # Check if user pressed Enter
|
|
295
|
+
corrected_payer_id = prompt_user_for_payer_id(insurance_name)
|
|
296
|
+
if corrected_payer_id:
|
|
297
|
+
try:
|
|
298
|
+
resolved_name = fetch_payer_name_from_api(corrected_payer_id, config, primary_endpoint)
|
|
299
|
+
print("API resolved to insurance name: {}".format(resolved_name))
|
|
300
|
+
MediLink_ConfigLoader.log("API Resolved to standard insurance name: {} for corrected payer ID: {}".format(resolved_name, corrected_payer_id), config, level="INFO")
|
|
301
|
+
|
|
302
|
+
confirmation = input("Is the standard insurance name '{}' correct? (yes/no): ".format(resolved_name)).strip().lower()
|
|
303
|
+
|
|
304
|
+
if confirmation in ['yes', 'y']:
|
|
305
|
+
if MediBot_Crosswalk_Library.update_crosswalk_with_corrected_payer_id(payer_id, corrected_payer_id):
|
|
306
|
+
return resolved_name
|
|
307
|
+
else:
|
|
308
|
+
print("Failed to update crosswalk with the corrected Payer ID.")
|
|
309
|
+
exit(1) # probably needs a different failure direction here.
|
|
310
|
+
else:
|
|
311
|
+
print("User did not confirm the standard insurance name. Manual intervention is required.")
|
|
312
|
+
MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
|
|
313
|
+
exit(1) # probably needs a different failure direction here.
|
|
314
|
+
except Exception as e:
|
|
315
|
+
print("Failed to resolve corrected payer ID to standard insurance name: {}".format(e))
|
|
316
|
+
MediLink_ConfigLoader.log("Failed to resolve corrected payer ID to standard insurance name: {}".format(e), config, level="ERROR")
|
|
317
|
+
exit(1) # probably needs a different failure direction here.
|
|
318
|
+
else:
|
|
319
|
+
print("Exiting script. Please make the necessary corrections and retry.")
|
|
320
|
+
exit(1) # probably needs a different failure direction here.
|
|
321
|
+
|
|
322
|
+
def prompt_user_for_payer_id(insurance_name):
|
|
323
|
+
"""
|
|
324
|
+
Prompts the user to input the payer ID manually.
|
|
325
|
+
"""
|
|
326
|
+
print("Manual intervention required: No payer ID found for insurance name '{}'.".format(insurance_name))
|
|
327
|
+
payer_id = input("Please enter the payer ID manually: ").strip()
|
|
328
|
+
return payer_id
|
|
234
329
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
except json.JSONDecodeError as e:
|
|
239
|
-
raise ValueError("Error decoding the crosswalk JSON file: {}".format(e))
|
|
330
|
+
def build_nm1_segment(payer_name, payer_id):
|
|
331
|
+
# Step 1: Build NM1 segment using payer name and ID
|
|
332
|
+
return "NM1*PR*2*{}*****PI*{}~".format(payer_name, payer_id)
|
|
240
333
|
|
|
241
|
-
def
|
|
334
|
+
def map_insurance_name_to_payer_id(insurance_name, config):
|
|
242
335
|
"""
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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'.
|
|
336
|
+
Maps insurance name to payer ID using the crosswalk configuration.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
insurance_name (str): Name of the insurance.
|
|
340
|
+
config (dict): Configuration settings.
|
|
252
341
|
|
|
253
342
|
Returns:
|
|
254
|
-
|
|
343
|
+
str: The payer ID corresponding to the insurance name.
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
# Ensure crosswalk is initialized and 'payer_id' key is available
|
|
347
|
+
MediBot_Crosswalk_Library.check_and_initialize_crosswalk(config)
|
|
348
|
+
|
|
349
|
+
# Load crosswalk configuration
|
|
350
|
+
_, crosswalk = MediLink_ConfigLoader.load_configuration(None, config.get('crosswalkPath', 'crosswalk.json'))
|
|
351
|
+
|
|
352
|
+
# Load insurance data from MAINS to get insurance ID
|
|
353
|
+
insurance_to_id = load_insurance_data_from_mains(config)
|
|
354
|
+
|
|
355
|
+
# Get medisoft ID corresponding to the insurance name
|
|
356
|
+
medisoft_id = insurance_to_id.get(insurance_name)
|
|
357
|
+
if medisoft_id is None:
|
|
358
|
+
error_message = "No Medisoft ID found for insurance name: {}. Consider checking MAINS directly.".format(insurance_name)
|
|
359
|
+
MediLink_ConfigLoader.log(error_message, config, level="ERROR")
|
|
360
|
+
raise ValueError(error_message)
|
|
361
|
+
|
|
362
|
+
# Convert medisoft_id to string to match the JSON data type
|
|
363
|
+
medisoft_id_str = str(medisoft_id)
|
|
364
|
+
|
|
365
|
+
# Get payer ID corresponding to the medisoft ID
|
|
366
|
+
payer_id = None
|
|
367
|
+
for payer, payer_info in crosswalk['payer_id'].items():
|
|
368
|
+
if medisoft_id_str in payer_info['medisoft_id']:
|
|
369
|
+
payer_id = payer
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
# Handle the case where no payer ID is found
|
|
373
|
+
if payer_id is None:
|
|
374
|
+
error_message = "No payer ID found for Medisoft ID: {}".format(medisoft_id)
|
|
375
|
+
MediLink_ConfigLoader.log(error_message, config, level="ERROR")
|
|
376
|
+
print(error_message)
|
|
377
|
+
payer_id = handle_missing_payer_id(insurance_name, config)
|
|
255
378
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
379
|
+
return payer_id
|
|
380
|
+
|
|
381
|
+
except ValueError as e:
|
|
382
|
+
if "JSON" in str(e) and "decode" in str(e):
|
|
383
|
+
error_message = "Error decoding the crosswalk JSON file in map_insurance_name_to_payer_id"
|
|
384
|
+
MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
|
|
385
|
+
raise ValueError(error_message)
|
|
386
|
+
else:
|
|
387
|
+
error_message = "Unexpected error in map_insurance_name_to_payer_id: {}".format(e)
|
|
388
|
+
MediLink_ConfigLoader.log(error_message, config, level="ERROR")
|
|
389
|
+
raise e
|
|
390
|
+
|
|
391
|
+
def handle_missing_payer_id(insurance_name, config):
|
|
266
392
|
"""
|
|
267
|
-
|
|
268
|
-
insurance_name = parsed_data.get('INAME', '')
|
|
269
|
-
payer_id = map_insurance_name_to_payer_id(insurance_name, config)
|
|
393
|
+
Handles cases where the payer ID is not found for a given Medisoft ID.
|
|
270
394
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
395
|
+
"""
|
|
396
|
+
print("Missing Payer ID for insurance name: {}".format(insurance_name))
|
|
397
|
+
MediLink_ConfigLoader.log("Missing Payer ID for insurance name: {}".format(insurance_name), config, level="WARNING")
|
|
398
|
+
|
|
399
|
+
# Prompt the user for manual intervention to input the payer ID
|
|
400
|
+
payer_id = prompt_user_for_payer_id(insurance_name)
|
|
401
|
+
|
|
402
|
+
if not payer_id:
|
|
403
|
+
message = "Unable to resolve missing Payer ID. Manual intervention is required."
|
|
404
|
+
MediLink_ConfigLoader.log(message, config, level="CRITICAL")
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
# Resolve the payer ID to a standard insurance name via API
|
|
408
|
+
try:
|
|
409
|
+
# primary_endpoint=None should kick to the default in the api function.
|
|
410
|
+
standard_insurance_name = resolve_payer_name(payer_id, config, primary_endpoint=None, insurance_name=insurance_name, parsed_data={})
|
|
411
|
+
message = "Resolved to standard insurance name: {} for payer ID: {}".format(standard_insurance_name, payer_id)
|
|
412
|
+
print(message)
|
|
413
|
+
MediLink_ConfigLoader.log(message, config, level="INFO")
|
|
414
|
+
except Exception as e:
|
|
415
|
+
message = "Failed to resolve payer ID to standard insurance name: {}".format(e)
|
|
416
|
+
print(message)
|
|
417
|
+
MediLink_ConfigLoader.log(message, config, level="ERROR")
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
# Ask for user confirmation
|
|
421
|
+
confirmation = input("Is the standard insurance name '{}' correct? (yes/no): ".format(standard_insurance_name)).strip().lower() or 'yes'
|
|
422
|
+
|
|
423
|
+
if confirmation in ['yes', 'y']:
|
|
424
|
+
# Update the crosswalk with the new payer ID and insurance name mapping
|
|
425
|
+
MediBot_Crosswalk_Library.update_crosswalk_with_new_payer_id(insurance_name, payer_id, config)
|
|
426
|
+
return payer_id
|
|
427
|
+
else:
|
|
428
|
+
print("User did not confirm the standard insurance name. Manual intervention is required.")
|
|
429
|
+
MediLink_ConfigLoader.log("User did not confirm the standard insurance name. Manual intervention is required.", config, level="CRITICAL")
|
|
430
|
+
return None
|
|
294
431
|
|
|
295
432
|
def create_nm1_payto_address_segments(config):
|
|
296
433
|
"""
|
|
@@ -324,102 +461,6 @@ def create_payer_address_segments(config):
|
|
|
324
461
|
|
|
325
462
|
return [n3_segment, n4_segment]
|
|
326
463
|
|
|
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
464
|
# Constructs the PRV segment for billing provider.
|
|
424
465
|
def create_billing_prv_segment(config, endpoint):
|
|
425
466
|
if endpoint.lower() == 'optumedi':
|
|
@@ -430,28 +471,118 @@ def create_billing_prv_segment(config, endpoint):
|
|
|
430
471
|
def create_hl_subscriber_segment():
|
|
431
472
|
return ["HL*2*1*22*0~"]
|
|
432
473
|
|
|
433
|
-
# Constructs the SBR segment for subscriber based on parsed data and configuration.
|
|
474
|
+
# (2000B Loop) Constructs the SBR segment for subscriber based on parsed data and configuration.
|
|
434
475
|
def create_sbr_segment(config, parsed_data, endpoint):
|
|
435
476
|
# Determine the payer responsibility sequence number code based on the payer type
|
|
436
477
|
# If the payer is Medicare, use 'P' (Primary)
|
|
437
478
|
# If the payer is not Medicare and is primary insurance, use 'P' (Primary)
|
|
438
479
|
# If the payer is secondary insurance after Medicare, use 'S' (Secondary)
|
|
439
|
-
#
|
|
480
|
+
# Assume everything is Primary for now.
|
|
440
481
|
responsibility_code = 'P'
|
|
441
482
|
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
# For simplicity, 'CI' (Commercial Insurance) as a default
|
|
445
|
-
insurance_type_code = 'CI'
|
|
483
|
+
# Insurance Type Code
|
|
484
|
+
insurance_type_code = insurance_type_selection(parsed_data)
|
|
446
485
|
|
|
447
486
|
# Construct the SBR segment using the determined codes
|
|
448
487
|
sbr_segment = "SBR*{responsibility_code}*18*******{insurance_type_code}~".format(
|
|
449
488
|
responsibility_code=responsibility_code,
|
|
450
489
|
insurance_type_code=insurance_type_code
|
|
451
490
|
)
|
|
452
|
-
|
|
453
491
|
return sbr_segment
|
|
454
492
|
|
|
493
|
+
def insurance_type_selection(parsed_data):
|
|
494
|
+
"""
|
|
495
|
+
Quick Fix: This funtion selects the insurance type for a patient based on predefined codes.
|
|
496
|
+
|
|
497
|
+
TODO (HIGH SBR09) Finish making this function.
|
|
498
|
+
This should eventually integrate into a menu upstream. This menu flow probably needs to be alongside the suggested endpoint flow.
|
|
499
|
+
For now let's make a menu here and then figure out the automated/API method to getting this per patient as there are no
|
|
500
|
+
useful historical patterns other than to say that the default should be PPO (12), then CI, then FI as most common options,
|
|
501
|
+
followed by the rest.
|
|
502
|
+
|
|
503
|
+
Present a user-selectable menu of insurance types based on predefined codes.
|
|
504
|
+
User needs to input the desired 2 character code for that patient or default to PPO.
|
|
505
|
+
|
|
506
|
+
Currently implements a simple text-based selection menu to choose
|
|
507
|
+
an insurance type for a patient. The default selection is '12' for PPO, but the user
|
|
508
|
+
can input other codes as per the options listed. This initial implementation uses
|
|
509
|
+
simple input/output functions for selection and can be replaced in the future by a
|
|
510
|
+
more dynamic interface or API-driven selection method.
|
|
511
|
+
|
|
512
|
+
Future Enhancements:
|
|
513
|
+
- Automate selection via an API that fetches patient data directly and auto-assigns the insurance type.
|
|
514
|
+
- Error handling to manage incorrect inputs and retry mechanisms.
|
|
515
|
+
|
|
516
|
+
Parameters:
|
|
517
|
+
- parsed_data (dict): Contains patient's last name under key 'LAST'.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
- str: The insurance type code selected by the user.
|
|
521
|
+
|
|
522
|
+
"""
|
|
523
|
+
print("\nHorrible Temporary Menu: Select the insurance type for patient {}: ".format(parsed_data['LAST']))
|
|
524
|
+
|
|
525
|
+
# Define insurance options with codes and descriptions
|
|
526
|
+
insurance_options = {
|
|
527
|
+
"11": "Other Non-Federal Programs",
|
|
528
|
+
"12": "Preferred Provider Organization (PPO)",
|
|
529
|
+
"13": "Point of Service (POS)",
|
|
530
|
+
"14": "Exclusive Provider Organization (EPO)",
|
|
531
|
+
"15": "Indemnity Insurance",
|
|
532
|
+
"16": "Health Maintenance Organization (HMO) Medicare Risk",
|
|
533
|
+
"17": "Dental Maintenance Organization",
|
|
534
|
+
"AM": "Automobile Medical",
|
|
535
|
+
"BL": "Blue Cross/Blue Shield",
|
|
536
|
+
"CH": "Champus",
|
|
537
|
+
"CI": "Commercial Insurance Co.",
|
|
538
|
+
"DS": "Disability",
|
|
539
|
+
"FI": "Federal Employees Program",
|
|
540
|
+
"HM": "Health Maintenance Organization",
|
|
541
|
+
"LM": "Liability Medical",
|
|
542
|
+
"MA": "Medicare Part A",
|
|
543
|
+
"MB": "Medicare Part B",
|
|
544
|
+
"MC": "Medicaid",
|
|
545
|
+
"OF": "Other Federal Program",
|
|
546
|
+
"TV": "Title V",
|
|
547
|
+
"VA": "Veterans Affairs Plan",
|
|
548
|
+
"WC": "Workers Compensation Health Claim",
|
|
549
|
+
"ZZ": "Mutually Defined"
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
# Function to display full list of insurance options
|
|
553
|
+
def display_insurance_options(options):
|
|
554
|
+
print("Insurance Type Options:")
|
|
555
|
+
# Sorting the dictionary keys to ensure consistent order
|
|
556
|
+
sorted_keys = sorted(options.keys())
|
|
557
|
+
for code in sorted_keys:
|
|
558
|
+
description = options[code]
|
|
559
|
+
print("{} - {}".format(code, description))
|
|
560
|
+
|
|
561
|
+
def prompt_display_insurance_options():
|
|
562
|
+
# Prompt to display full list
|
|
563
|
+
display_full_list = input("Do you want to see the full list of insurance options? (yes/no): ").strip().lower()
|
|
564
|
+
|
|
565
|
+
# Display full list if user confirms
|
|
566
|
+
if display_full_list in ['yes', 'y']:
|
|
567
|
+
display_insurance_options(insurance_options)
|
|
568
|
+
|
|
569
|
+
# Horrible menu
|
|
570
|
+
prompt_display_insurance_options()
|
|
571
|
+
|
|
572
|
+
# Default selection
|
|
573
|
+
insurance_type_code = '12'
|
|
574
|
+
|
|
575
|
+
# User input for insurance type
|
|
576
|
+
user_input = input("Enter the 2-character code for the insurance type (or press Enter to default to '12' for PPO): ").strip().upper()
|
|
577
|
+
|
|
578
|
+
# Validate input and set the insurance type code
|
|
579
|
+
if user_input in insurance_options:
|
|
580
|
+
insurance_type_code = user_input
|
|
581
|
+
else:
|
|
582
|
+
print("Skipped or Input not recognized. Defaulting to Preferred Provider Organization (PPO)\n")
|
|
583
|
+
|
|
584
|
+
return insurance_type_code
|
|
585
|
+
|
|
455
586
|
# Constructs the NM1 segment for subscriber based on parsed data and configuration.
|
|
456
587
|
def create_nm1_subscriber_segment(config, parsed_data, endpoint):
|
|
457
588
|
if endpoint.lower() == 'optumedi':
|
|
@@ -487,7 +618,7 @@ def create_dmg_segment(parsed_data):
|
|
|
487
618
|
|
|
488
619
|
def create_nm1_rendering_provider_segment(config, is_rendering_provider_different=False):
|
|
489
620
|
"""
|
|
490
|
-
#BUG This is getting placed incorrectly.
|
|
621
|
+
# BUG This is getting placed incorrectly. Has this been fixed??
|
|
491
622
|
|
|
492
623
|
Constructs the NM1 segment for the rendering provider in the 2310B loop using configuration data.
|
|
493
624
|
|
|
@@ -582,8 +713,44 @@ def create_clm_and_related_segments(parsed_data, config):
|
|
|
582
713
|
|
|
583
714
|
return segments
|
|
584
715
|
|
|
716
|
+
def get_endpoint_config(config, endpoint):
|
|
717
|
+
endpoint_config = config['endpoints'].get(endpoint.upper(), {})
|
|
718
|
+
if not endpoint_config:
|
|
719
|
+
print("Endpoint configuration for {} not found.".format(endpoint))
|
|
720
|
+
return None
|
|
721
|
+
return endpoint_config
|
|
722
|
+
|
|
723
|
+
def create_interchange_elements(config, endpoint, transaction_set_control_number):
|
|
724
|
+
"""
|
|
725
|
+
Create interchange headers and trailers for an 837P document.
|
|
726
|
+
|
|
727
|
+
Parameters:
|
|
728
|
+
- config: Configuration settings loaded from a JSON file.
|
|
729
|
+
- endpoint: The endpoint for which the data is being processed.
|
|
730
|
+
- transaction_set_control_number: The starting transaction set control number.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
- Tuple containing (ISA header, GS header, GE trailer, IEA trailer).
|
|
734
|
+
"""
|
|
735
|
+
endpoint_config = get_endpoint_config(config, endpoint)
|
|
736
|
+
|
|
737
|
+
# Get the current system time and format it as 'HHMMSS' in 24-hour clock.
|
|
738
|
+
current_time = datetime.now().strftime('%H%M%S')
|
|
739
|
+
isa13 = '000' + current_time # Format ISA13 with '000HHMMSS'.
|
|
740
|
+
|
|
741
|
+
# Check if isa13 could not be generated from the current time
|
|
742
|
+
if len(isa13) != 9:
|
|
743
|
+
# If isa13 cannot be generated from the current time, use the configured value.
|
|
744
|
+
isa13 = endpoint_config.get('isa_13_value', '000000001')
|
|
745
|
+
|
|
746
|
+
# Create interchange header and trailer using provided library functions.
|
|
747
|
+
isa_header, gs_header = create_interchange_header(config, endpoint, isa13)
|
|
748
|
+
ge_trailer, iea_trailer = create_interchange_trailer(config, transaction_set_control_number, isa13)
|
|
749
|
+
|
|
750
|
+
return isa_header, gs_header, ge_trailer, iea_trailer
|
|
751
|
+
|
|
585
752
|
# Generates the ISA and GS segments for the interchange header based on configuration and endpoint.
|
|
586
|
-
def create_interchange_header(config, endpoint):
|
|
753
|
+
def create_interchange_header(config, endpoint, isa13):
|
|
587
754
|
"""
|
|
588
755
|
Generate ISA and GS segments for the interchange header, ensuring endpoint-specific requirements are met.
|
|
589
756
|
Includes support for Availity, Optum, and PNT_DATA endpoints, with streamlined configuration and default handling.
|
|
@@ -591,6 +758,7 @@ def create_interchange_header(config, endpoint):
|
|
|
591
758
|
Parameters:
|
|
592
759
|
- config: Configuration dictionary with settings and identifiers.
|
|
593
760
|
- endpoint: String indicating the target endpoint ('Availity', 'Optum', 'PNT_DATA').
|
|
761
|
+
- isa13: The ISA13 field value representing the current system time.
|
|
594
762
|
|
|
595
763
|
Returns:
|
|
596
764
|
- Tuple containing the ISA and GS segment strings.
|
|
@@ -600,14 +768,12 @@ def create_interchange_header(config, endpoint):
|
|
|
600
768
|
# Set defaults for ISA segment values
|
|
601
769
|
isa02 = isa04 = " " # Default value for ISA02 and ISA04
|
|
602
770
|
isa05 = isa07 = 'ZZ' # Default qualifier
|
|
603
|
-
isa13 = '000000001' # Default Interchange Control Number. Not sure what to do with this. date? See also trailer
|
|
604
771
|
isa15 = 'P' # 'T' for Test, 'P' for Production
|
|
605
772
|
|
|
606
773
|
# Conditional values from config
|
|
607
774
|
isa_sender_id = endpoint_config.get('isa_06_value', config.get('submitterId', '')).rstrip()
|
|
608
775
|
isa07_value = endpoint_config.get('isa_07_value', isa07)
|
|
609
776
|
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
777
|
gs_sender_code = endpoint_config.get('gs_02_value', config.get('submitterEdi', ''))
|
|
612
778
|
gs_receiver_code = endpoint_config.get('gs_03_value', config.get('receiverEdi', ''))
|
|
613
779
|
isa15_value = endpoint_config.get('isa_15_value', isa15)
|
|
@@ -615,49 +781,50 @@ def create_interchange_header(config, endpoint):
|
|
|
615
781
|
# ISA Segment
|
|
616
782
|
isa_segment = "ISA*00*{}*00*{}*{}*{}*{}*{}*{}*{}*^*00501*{}*0*{}*:~".format(
|
|
617
783
|
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'),
|
|
784
|
+
format_datetime(format_type='isa'), format_datetime(format_type='time'), isa13, isa15_value
|
|
619
785
|
)
|
|
620
786
|
|
|
621
787
|
# GS Segment
|
|
622
788
|
# GS04 YYYYMMDD
|
|
623
789
|
# GS06 Group Control Number, Field Length 1/9, must match GE02
|
|
790
|
+
# BUG probably move up a function.
|
|
624
791
|
gs06 = '1' # Placeholder for now?
|
|
625
792
|
|
|
626
793
|
gs_segment = "GS*HC*{}*{}*{}*{}*{}*X*005010X222A1~".format(
|
|
627
794
|
gs_sender_code, gs_receiver_code, format_datetime(), format_datetime(format_type='time'), gs06
|
|
628
795
|
)
|
|
629
796
|
|
|
630
|
-
log("Created interchange header for endpoint: {}".format(endpoint), config, level="INFO")
|
|
797
|
+
MediLink_ConfigLoader.log("Created interchange header for endpoint: {}".format(endpoint), config, level="INFO")
|
|
631
798
|
|
|
632
799
|
return isa_segment, gs_segment
|
|
633
800
|
|
|
634
801
|
# 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,
|
|
802
|
+
def create_interchange_trailer(config, num_transactions, isa13, num_functional_groups=1):
|
|
636
803
|
"""
|
|
637
804
|
Generate GE and IEA segments for the interchange trailer.
|
|
638
805
|
|
|
639
806
|
Parameters:
|
|
807
|
+
- config: Configuration dictionary with settings and identifiers.
|
|
640
808
|
- num_transactions: The number of transactions within the functional group.
|
|
809
|
+
- isa13: The ISA13 field value representing the current system time.
|
|
641
810
|
- num_functional_groups: The number of functional groups within the interchange. Default is 1.
|
|
642
811
|
|
|
643
812
|
Returns:
|
|
644
813
|
- Tuple containing the GE and IEA segment strings.
|
|
645
814
|
"""
|
|
646
|
-
|
|
815
|
+
|
|
647
816
|
# GE Segment: Functional Group Trailer
|
|
648
817
|
# Indicates the end of a functional group and provides the count of the number of transactions within it.
|
|
818
|
+
|
|
649
819
|
# GE02 Group Control Number, Field Length 1/9, must match GS06 (Header)
|
|
820
|
+
# TODO This gs/ge matching should probably move up a function like isa13.
|
|
650
821
|
ge02 = '1' #Placeholder for now?
|
|
651
822
|
ge_segment = "GE*{}*{}~".format(num_transactions, ge02)
|
|
652
823
|
|
|
653
|
-
# IEA Segment: Interchange Control Trailer
|
|
654
|
-
|
|
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)
|
|
824
|
+
# IEA Segment: Interchange Control Trailer (Note: IEA02 needs to equal ISA13)
|
|
825
|
+
iea_segment = "IEA*{}*{}~".format(num_functional_groups, isa13)
|
|
659
826
|
|
|
660
|
-
log("Created interchange trailer", config, level="INFO")
|
|
827
|
+
MediLink_ConfigLoader.log("Created interchange trailer", config, level="INFO")
|
|
661
828
|
|
|
662
829
|
return ge_segment, iea_segment
|
|
663
830
|
|
|
@@ -676,4 +843,48 @@ def generate_segment_counts(compiled_segments, transaction_set_control_number):
|
|
|
676
843
|
# This time, we directly replace the placeholder with the correct SE segment
|
|
677
844
|
formatted_837p = compiled_segments.rsplit('SE**', 1)[0] + se_segment
|
|
678
845
|
|
|
679
|
-
return formatted_837p
|
|
846
|
+
return formatted_837p
|
|
847
|
+
|
|
848
|
+
def handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
849
|
+
for error in validation_errors:
|
|
850
|
+
MediLink_ConfigLoader.log("Validation error for transaction set {}: {}".format(transaction_set_control_number, error), config, level="WARNING")
|
|
851
|
+
|
|
852
|
+
print("Validation errors encountered for transaction set {}. Errors: {}".format(transaction_set_control_number, validation_errors))
|
|
853
|
+
user_input = input("Skip this patient and continue without incrementing transaction set number? (yes/no): ")
|
|
854
|
+
if user_input.lower() == 'yes':
|
|
855
|
+
print("Skipping patient...")
|
|
856
|
+
MediLink_ConfigLoader.log("Skipped processing of transaction set {} due to user decision.".format(transaction_set_control_number), config, level="INFO")
|
|
857
|
+
return True # Skip the current patient
|
|
858
|
+
else:
|
|
859
|
+
print("Processing halted due to validation errors.")
|
|
860
|
+
MediLink_ConfigLoader.log("HALT: Processing halted at transaction set {} due to unresolved validation errors.".format(transaction_set_control_number), config, level="ERROR")
|
|
861
|
+
sys.exit() # Optionally halt further processing
|
|
862
|
+
|
|
863
|
+
def winscp_validate_output_directory(output_directory):
|
|
864
|
+
"""
|
|
865
|
+
Validates the output directory path to ensure it has no spaces.
|
|
866
|
+
If spaces are found, prompts the user to input a new path.
|
|
867
|
+
If the directory doesn't exist, creates it.
|
|
868
|
+
"""
|
|
869
|
+
while ' ' in output_directory:
|
|
870
|
+
print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
|
|
871
|
+
print(" Current proposed path: {}".format(output_directory))
|
|
872
|
+
new_path = input("Please enter a new path for the output directory: ")
|
|
873
|
+
output_directory = new_path.strip() # Remove leading/trailing spaces
|
|
874
|
+
|
|
875
|
+
# Check if the directory exists, if not, create it
|
|
876
|
+
if not os.path.exists(output_directory):
|
|
877
|
+
os.makedirs(output_directory)
|
|
878
|
+
print("INFO: Created output directory: {}".format(output_directory))
|
|
879
|
+
|
|
880
|
+
return output_directory
|
|
881
|
+
|
|
882
|
+
def get_output_directory(config):
|
|
883
|
+
# Retrieve desired default output file path from config
|
|
884
|
+
output_directory = config.get('outputFilePath', '')
|
|
885
|
+
# BUG (Low SFTP) Add WinSCP validation because of the mishandling of spaces in paths. (This shouldn't need to exist.)
|
|
886
|
+
output_directory = winscp_validate_output_directory(output_directory)
|
|
887
|
+
if not os.path.isdir(output_directory):
|
|
888
|
+
print("Output directory does not exist. Please check the configuration.")
|
|
889
|
+
return None
|
|
890
|
+
return output_directory
|