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.

@@ -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
- submitter_id = endpoint_config.get('nm_109_value', 'DEFAULT ID') # Default ID if not specified in endpoint
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('submitter_contact_name', 'RAFAEL OLIVERVIDAUD')
148
- contact_telephone_number = config.get('submitter_contact_tel', '9543821782')
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 map_insurance_name_to_payer_id(insurance_name, config):
197
+ def payer_id_to_payer_name(parsed_data, config, endpoint):
178
198
  """
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'.
199
+ Preprocesses payer information from parsed data and enriches parsed_data with the payer name and ID.
182
200
 
183
- Inputs:
184
- - insurance_name: The name of the insurance as provided by Medisoft.
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
- 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.
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
- 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.
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
- _, 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")
233
+ Creates the 2010BB payer information segment.
205
234
 
206
- def fetch_payer_name_from_crosswalk(payer_id, config, endpoint):
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
- 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
-
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
- - The payer name corresponding to the payer ID.
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
- crosswalk_path = config.get('crosswalkPath')
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
- 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))
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
- 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))
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 create_2010BB_payer_information_segment(parsed_data, config, endpoint):
334
+ def map_insurance_name_to_payer_id(insurance_name, config):
242
335
  """
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'.
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
- - A formatted NM1*PR segment string for the 2010BB loop, correctly identifying the payer with its name and ID.
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
- 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.
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
- # 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)
393
+ Handles cases where the payer ID is not found for a given Medisoft ID.
270
394
 
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)
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
- # For simplicity, we're going to assume everything is Primary for now.
480
+ # Assume everything is Primary for now.
440
481
  responsibility_code = 'P'
441
482
 
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'
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'), isa13_value, isa15_value
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, endpoint, num_transactions, num_functional_groups=1):
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
- endpoint_config = config['endpoints'].get(endpoint.upper(), {})
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
- 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)
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