medicafe 0.240419.2__py3-none-any.whl → 0.240613.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.

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