medicafe 0.250725.18__py3-none-any.whl → 0.250728.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.

@@ -190,6 +190,31 @@ def ensure_dependencies():
190
190
  print("Warning: Failed to install {}.".format(package_name))
191
191
  time.sleep(2) # Pause for 2 seconds after failure message
192
192
 
193
+ def check_for_updates_only():
194
+ """
195
+ Check if a new version is available without performing the upgrade.
196
+ Returns a simple status message for batch script consumption.
197
+ """
198
+ if not check_internet_connection():
199
+ print("ERROR")
200
+ return
201
+
202
+ package = "medicafe"
203
+ current_version = get_installed_version(package)
204
+ if not current_version:
205
+ print("ERROR")
206
+ return
207
+
208
+ latest_version = get_latest_version(package)
209
+ if not latest_version:
210
+ print("ERROR")
211
+ return
212
+
213
+ if compare_versions(latest_version, current_version) > 0:
214
+ print("UPDATE_AVAILABLE:" + latest_version)
215
+ else:
216
+ print("UP_TO_DATE")
217
+
193
218
  def main():
194
219
  # Ensure internet connection before proceeding
195
220
  if not check_internet_connection():
@@ -243,4 +268,7 @@ def main():
243
268
  sys.exit(1)
244
269
 
245
270
  if __name__ == "__main__":
246
- main()
271
+ if len(sys.argv) > 1 and sys.argv[1] == "--check-only":
272
+ check_for_updates_only()
273
+ else:
274
+ main()
MediLink/MediLink.py CHANGED
@@ -58,6 +58,11 @@ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_map
58
58
  - Enriched detailed patient data with insurance type added.
59
59
 
60
60
  TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
61
+ This is going to be coming soon as an API feature from United Healthcare. We'll be able to get insurance types directly via their API.
62
+ So, while we won't be able to do it for all payerIDs, we'll be able to do it for the ones that are supported by UHC.
63
+ So, we'll need a way to augment the associated menu here so that the user is aware of which insurance types are already pulled from
64
+ UHC and which ones are not yet supported so they know which ones they need to edit. It is possible that we may want to isolate the
65
+ patient data that is already pulled from UHC so that the user can see which ones are already using the enriched data.
61
66
  """
62
67
  if patient_insurance_type_mapping is None:
63
68
  MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.", level="WARNING")
@@ -221,14 +226,12 @@ def process_endpoint(endpoint_key, endpoint_info, config):
221
226
  MediLink_ConfigLoader.log("Error processing endpoint {}: {}".format(endpoint_key, e), level="ERROR")
222
227
  return [], []
223
228
 
224
- def user_decision_on_suggestions(detailed_patient_data, config, insurance_edited):
229
+ def user_decision_on_suggestions(detailed_patient_data, config, insurance_edited, crosswalk):
225
230
  """
226
231
  Presents the user with all patient summaries and suggested endpoints,
227
232
  then asks for confirmation to proceed with all or specify adjustments manually.
228
233
 
229
- BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
230
- although the user decision is persisting. Possibly consider making the current/suggested/confirmed endpoint
231
- part of a class that the user can interact with via these menus? Probably better handling that way.
234
+ FIXED: Display now properly shows effective endpoints (user preferences over original suggestions)
232
235
  """
233
236
  if insurance_edited:
234
237
  # Display summaries only if insurance types were edited
@@ -247,18 +250,16 @@ def user_decision_on_suggestions(detailed_patient_data, config, insurance_edited
247
250
 
248
251
  # If the user agrees to proceed with all suggested endpoints, confirm them.
249
252
  if proceed:
250
- return MediLink_DataMgmt.confirm_all_suggested_endpoints(detailed_patient_data)
253
+ return MediLink_DataMgmt.confirm_all_suggested_endpoints(detailed_patient_data), crosswalk
251
254
  # Otherwise, allow the user to adjust the endpoints manually.
252
255
  else:
253
- return select_and_adjust_files(detailed_patient_data, config)
256
+ return select_and_adjust_files(detailed_patient_data, config, crosswalk)
254
257
 
255
- def select_and_adjust_files(detailed_patient_data, config):
258
+ def select_and_adjust_files(detailed_patient_data, config, crosswalk):
256
259
  """
257
260
  Allows users to select patients and adjust their endpoints by interfacing with UI functions.
258
261
 
259
- BUG (Med suggested_endpoint) After the user is done making their selection (probably via a class?),
260
- Then suggested_endpoint should update to persist the user selection as priority over its original suggestion.
261
- Which means the crosswalk should persist the change in the endpoint as well.
262
+ FIXED: Now properly updates suggested_endpoint and persists user preferences to crosswalk.
262
263
  """
263
264
  # Display options for patients
264
265
  MediLink_UI.display_patient_options(detailed_patient_data)
@@ -268,11 +269,12 @@ def select_and_adjust_files(detailed_patient_data, config):
268
269
 
269
270
  # Get an ordered list of endpoint keys
270
271
  endpoint_keys = list(config['MediLink_Config']['endpoints'].keys())
271
-
272
+
272
273
  # Iterate over each selected index and process endpoint changes
273
274
  for i in selected_indices:
274
275
  data = detailed_patient_data[i]
275
- MediLink_UI.display_patient_for_adjustment(data['patient_name'], data.get('suggested_endpoint', 'N/A'))
276
+ current_effective_endpoint = get_effective_endpoint(data)
277
+ MediLink_UI.display_patient_for_adjustment(data['patient_name'], current_effective_endpoint)
276
278
 
277
279
  endpoint_change = MediLink_UI.get_endpoint_decision()
278
280
  if endpoint_change == 'y':
@@ -281,17 +283,132 @@ def select_and_adjust_files(detailed_patient_data, config):
281
283
 
282
284
  if 0 <= endpoint_index < len(endpoint_keys):
283
285
  selected_endpoint_key = endpoint_keys[endpoint_index]
284
- data['confirmed_endpoint'] = selected_endpoint_key
285
286
  print("Endpoint changed to {0} for patient {1}.".format(config['MediLink_Config']['endpoints'][selected_endpoint_key]['name'], data['patient_name']))
286
- # BUG (Med, Crosswalk & suggested_endpoint) Probably update crosswalk and suggested endpoint here??? Not quite, because the user could be changing multiple patients
287
- # at the same time and that would interrupt the flow.
288
- # TODO This needs to make its way to save_crosswalk but with a user confirmation about updating for all the future patients from that insurance company. (or maybe that flow needs to be within save_crosswalk?)
287
+
288
+ # Use the new endpoint management system
289
+ updated_crosswalk = update_suggested_endpoint_with_user_preference(
290
+ detailed_patient_data, i, selected_endpoint_key, config, crosswalk
291
+ )
292
+ if updated_crosswalk:
293
+ crosswalk = updated_crosswalk
289
294
  else:
290
- print("Invalid selection. Keeping the suggested endpoint.")
295
+ print("Invalid selection. Keeping the current endpoint.")
296
+ data['confirmed_endpoint'] = current_effective_endpoint
291
297
  else:
292
- data['confirmed_endpoint'] = data.get('suggested_endpoint', 'N/A')
298
+ data['confirmed_endpoint'] = current_effective_endpoint
299
+
300
+ return detailed_patient_data, crosswalk
301
+
302
+ def update_suggested_endpoint_with_user_preference(detailed_patient_data, patient_index, new_endpoint, config, crosswalk):
303
+ """
304
+ Updates the suggested endpoint for a patient and optionally updates the crosswalk
305
+ for future patients with the same insurance.
306
+
307
+ :param detailed_patient_data: List of patient data dictionaries
308
+ :param patient_index: Index of the patient being updated
309
+ :param new_endpoint: The new endpoint selected by the user
310
+ :param config: Configuration settings
311
+ :param crosswalk: Crosswalk data for in-memory updates
312
+ :return: Updated crosswalk if changes were made, None otherwise
313
+ """
314
+ data = detailed_patient_data[patient_index]
315
+ original_suggested = data.get('suggested_endpoint')
316
+
317
+ # Update the patient's endpoint preference
318
+ data['user_preferred_endpoint'] = new_endpoint
319
+ data['confirmed_endpoint'] = new_endpoint
320
+
321
+ # If user changed from the original suggestion, offer to update crosswalk
322
+ if original_suggested != new_endpoint:
323
+ primary_insurance = data.get('primary_insurance')
324
+ patient_name = data.get('patient_name')
325
+
326
+ print("\nYou changed the endpoint for {} from {} to {}.".format(patient_name, original_suggested, new_endpoint))
327
+ update_future = input("Would you like to use {} as the default endpoint for future patients with {}? (Y/N): ".format(new_endpoint, primary_insurance)).strip().lower()
328
+
329
+ if update_future in ['y', 'yes']:
330
+ # Find the payer ID associated with this insurance
331
+ insurance_to_id = load_insurance_data_from_mains(config)
332
+ insurance_id = insurance_to_id.get(primary_insurance)
333
+
334
+ if insurance_id:
335
+ # Find the payer ID in crosswalk and update it
336
+ updated = False
337
+ for payer_id, payer_data in crosswalk.get('payer_id', {}).items():
338
+ medisoft_ids = [str(id) for id in payer_data.get('medisoft_id', [])]
339
+ if str(insurance_id) in medisoft_ids:
340
+ # Update the crosswalk in memory
341
+ crosswalk['payer_id'][payer_id]['endpoint'] = new_endpoint
342
+ MediLink_ConfigLoader.log("Updated crosswalk in memory: Payer ID {} ({}) now defaults to {}".format(payer_id, primary_insurance, new_endpoint), level="INFO")
343
+
344
+ # Update suggested_endpoint for other patients with same insurance in current batch
345
+ for other_data in detailed_patient_data:
346
+ if (other_data.get('primary_insurance') == primary_insurance and
347
+ 'user_preferred_endpoint' not in other_data):
348
+ other_data['suggested_endpoint'] = new_endpoint
349
+
350
+ updated = True
351
+ break
352
+
353
+ if updated:
354
+ # Save the updated crosswalk to disk immediately using API bypass mode
355
+ if save_crosswalk_immediately(config, crosswalk):
356
+ print("Updated default endpoint for {} to {}".format(primary_insurance, new_endpoint))
357
+ else:
358
+ print("Updated endpoint preference (will be saved during next crosswalk update)")
359
+ return crosswalk
360
+ else:
361
+ MediLink_ConfigLoader.log("Could not find payer ID in crosswalk for insurance {}".format(primary_insurance), level="WARNING")
362
+ else:
363
+ MediLink_ConfigLoader.log("Could not find insurance ID for {} to update crosswalk".format(primary_insurance), level="WARNING")
364
+
365
+ return None
366
+
367
+ def save_crosswalk_immediately(config, crosswalk):
368
+ """
369
+ Saves the crosswalk to disk immediately using API bypass mode.
370
+
371
+ :param config: Configuration settings
372
+ :param crosswalk: Crosswalk data to save
373
+ :return: True if saved successfully, False otherwise
374
+ """
375
+ try:
376
+ # Import the crosswalk library
377
+ from MediBot import MediBot_Crosswalk_Library
378
+
379
+ # Save using API bypass mode (no client needed, skip API operations)
380
+ success = MediBot_Crosswalk_Library.save_crosswalk(None, config, crosswalk, skip_api_operations=True)
381
+
382
+ if success:
383
+ MediLink_ConfigLoader.log("Successfully saved crosswalk with updated endpoint preferences", level="INFO")
384
+ else:
385
+ MediLink_ConfigLoader.log("Failed to save crosswalk - preferences will be saved during next crosswalk update", level="WARNING")
386
+
387
+ return success
388
+
389
+ except ImportError:
390
+ MediLink_ConfigLoader.log("Could not import MediBot_Crosswalk_Library for saving crosswalk", level="ERROR")
391
+ return False
392
+ except Exception as e:
393
+ MediLink_ConfigLoader.log("Error saving crosswalk: {}".format(e), level="ERROR")
394
+ return False
395
+
396
+ def get_effective_endpoint(patient_data):
397
+ """
398
+ Returns the most appropriate endpoint for a patient based on the hierarchy:
399
+ 1. Confirmed endpoint (final decision)
400
+ 2. User preferred endpoint (if user made a change)
401
+ 3. Original suggested endpoint
402
+ 4. Default (AVAILITY)
403
+
404
+ :param patient_data: Individual patient data dictionary
405
+ :return: The effective endpoint to use for this patient
406
+ """
407
+ return (patient_data.get('confirmed_endpoint') or
408
+ patient_data.get('user_preferred_endpoint') or
409
+ patient_data.get('suggested_endpoint', 'AVAILITY'))
410
+
293
411
 
294
- return detailed_patient_data
295
412
 
296
413
  def main_menu():
297
414
  """
@@ -303,8 +420,26 @@ def main_menu():
303
420
 
304
421
  # Check to make sure payer_id key is available in crosswalk, otherwise, go through that crosswalk initialization flow
305
422
  if 'payer_id' not in crosswalk:
306
- print("Crosswalk 'payer_id' not found. Please run MediBot_Preprocessor.py with the --update-crosswalk argument.")
307
- exit() # BUG Halting the script execution here for now, should be handled in the preprocessor script.
423
+ print("\n" + "="*60)
424
+ print("SETUP REQUIRED: Payer Information Database Missing")
425
+ print("="*60)
426
+ print("\nThe system needs to build a database of insurance company information")
427
+ print("before it can process claims. This is a one-time setup requirement.")
428
+ print("\nThis typically happens when:")
429
+ print("• You're running MediLink for the first time")
430
+ print("• The payer database was accidentally deleted or corrupted")
431
+ print("• You're using a new installation of the system")
432
+ print("\nTO FIX THIS:")
433
+ print("1. Open a command prompt/terminal")
434
+ print("2. Navigate to the MediCafe directory")
435
+ print("3. Run: python MediBot/MediBot_Preprocessor.py --update-crosswalk")
436
+ print("4. Wait for the process to complete (this may take a few minutes)")
437
+ print("5. Return here and restart MediLink")
438
+ print("\nThis will download and build the insurance company database.")
439
+ print("="*60)
440
+ print("\nPress Enter to exit...")
441
+ input()
442
+ return # Graceful exit instead of abrupt halt
308
443
 
309
444
  # Check if the application is in test mode
310
445
  if config.get("MediLink_Config", {}).get("TestMode", False):
@@ -378,7 +513,11 @@ def handle_submission(detailed_patient_data, config, crosswalk):
378
513
  print("Returning to bulk edit insurance types.")
379
514
 
380
515
  # Initiate user interaction to confirm or adjust suggested endpoints.
381
- adjusted_data = user_decision_on_suggestions(detailed_patient_data, config, insurance_edited)
516
+ adjusted_data, updated_crosswalk = user_decision_on_suggestions(detailed_patient_data, config, insurance_edited, crosswalk)
517
+
518
+ # Update crosswalk reference if it was modified
519
+ if updated_crosswalk:
520
+ crosswalk = updated_crosswalk
382
521
 
383
522
  # Confirm all remaining suggested endpoints.
384
523
  confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
@@ -28,6 +28,16 @@ except (ImportError, SystemError):
28
28
  import MediLink_837p_encoder_library
29
29
  except ImportError:
30
30
  from MediLink import MediLink_837p_encoder_library
31
+
32
+ # Safe import for API client - works in multiple contexts
33
+ try:
34
+ from . import MediLink_API_v3
35
+ except (ImportError, SystemError):
36
+ try:
37
+ import MediLink_API_v3
38
+ except ImportError:
39
+ from MediLink import MediLink_API_v3
40
+
31
41
  # TODO (COB ENHANCEMENT): Import COB library when implementing Medicare and secondary claim support
32
42
  # import MediLink_837p_cob_library
33
43
  #from tqdm import tqdm
@@ -192,7 +202,7 @@ def write_output_file(document_segments, output_directory, endpoint_key, input_f
192
202
  MediLink_ConfigLoader.log("Error: Failed to write output file. {}".format(e), config, level="ERROR")
193
203
  return None
194
204
 
195
- def process_single_file(file_path, config, endpoint_key, transaction_set_control_number, crosswalk): # BUG Duplicate function name??
205
+ def process_single_file(file_path, config, endpoint_key, transaction_set_control_number, crosswalk, client):
196
206
  """
197
207
  Process the claim data from a file into the 837P format.
198
208
 
@@ -201,6 +211,8 @@ def process_single_file(file_path, config, endpoint_key, transaction_set_control
201
211
  config (dict): Configuration settings loaded from a JSON file.
202
212
  endpoint_key (str): The key representing the endpoint for which the claim is being processed.
203
213
  transaction_set_control_number (int): The starting transaction set control number for 837P segments.
214
+ crosswalk (dict): Crosswalk data for payer information.
215
+ client: API client for payer name resolution.
204
216
 
205
217
  Returns:
206
218
  tuple: A tuple containing the formatted claim segments and the next transaction set control number.
@@ -213,7 +225,7 @@ def process_single_file(file_path, config, endpoint_key, transaction_set_control
213
225
  return None, transaction_set_control_number # Halt processing if the user chooses
214
226
 
215
227
  # Process each valid claim
216
- formatted_claims, transaction_set_control_number = format_claims(valid_claims, config, endpoint_key, transaction_set_control_number, crosswalk)
228
+ formatted_claims, transaction_set_control_number = format_claims(valid_claims, config, endpoint_key, transaction_set_control_number, crosswalk, client)
217
229
 
218
230
  formatted_claims_str = '\n'.join(formatted_claims) # Join formatted claims into a single string
219
231
  return formatted_claims_str, transaction_set_control_number
@@ -247,7 +259,7 @@ def read_and_validate_claims(file_path, config):
247
259
 
248
260
  return valid_claims, validation_errors
249
261
 
250
- def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_control_number, crosswalk):
262
+ def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_control_number, crosswalk, client):
251
263
  """
252
264
  Formats a list of parsed claim data into 837P segments.
253
265
 
@@ -256,6 +268,8 @@ def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_c
256
268
  - config: Configuration settings loaded from a JSON file.
257
269
  - endpoint: The endpoint key representing the specific endpoint.
258
270
  - starting_transaction_set_control_number: Starting transaction set control number for 837P segments.
271
+ - crosswalk: Crosswalk data for payer information.
272
+ - client: API client for payer name resolution.
259
273
 
260
274
  Returns:
261
275
  - A list of formatted 837P claims and the next transaction set control number.
@@ -264,7 +278,7 @@ def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_c
264
278
  transaction_set_control_number = starting_transaction_set_control_number
265
279
 
266
280
  for parsed_data in parsed_data_list:
267
- formatted_claim = format_single_claim(parsed_data, config, endpoint, transaction_set_control_number, crosswalk)
281
+ formatted_claim = format_single_claim(parsed_data, config, endpoint, transaction_set_control_number, crosswalk, client)
268
282
  formatted_claims.append(formatted_claim)
269
283
  transaction_set_control_number += 1 # Increment for each successfully processed claim
270
284
 
@@ -342,7 +356,7 @@ def validate_claim_data(parsed_data, config, required_fields=[]):
342
356
 
343
357
  return True, []
344
358
 
345
- def process_and_write_file(file_path, config, endpoint, crosswalk, starting_tscn=1):
359
+ def process_and_write_file(file_path, config, endpoint, crosswalk, client, starting_tscn=1):
346
360
  """
347
361
  Process a single file, create complete 837P document with headers and trailers, and write to output file.
348
362
 
@@ -350,11 +364,13 @@ def process_and_write_file(file_path, config, endpoint, crosswalk, starting_tscn
350
364
  - file_path: Path to the .DAT file to be processed.
351
365
  - config: Configuration settings.
352
366
  - endpoint: Endpoint key.
367
+ - crosswalk: Crosswalk data for payer information.
368
+ - client: API client for payer name resolution.
353
369
  - starting_tscn: Starting Transaction Set Control Number.
354
370
  """
355
371
  print("Processing: {}".format(file_path))
356
372
  MediLink_ConfigLoader.log("Processing: {}".format(file_path))
357
- formatted_data, transaction_set_control_number = process_single_file(file_path, config, endpoint, starting_tscn, crosswalk)
373
+ formatted_data, transaction_set_control_number = process_single_file(file_path, config, endpoint, starting_tscn, crosswalk, client)
358
374
  isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
359
375
 
360
376
  # Combine everything into a single document
@@ -371,7 +387,6 @@ def process_and_write_file(file_path, config, endpoint, crosswalk, starting_tscn
371
387
  print("File processed. Output saved to: {}".format(output_file_path))
372
388
 
373
389
  def main():
374
- # BUG (MAJOR) THIS NEEDS THE API CLIENT TO BE PASSED INTO THE FUNCTION FOR SINGLE FILE PROCESSING.
375
390
  """
376
391
  Converts fixed-width files to 837P format for health claim submissions.
377
392
 
@@ -418,10 +433,13 @@ def main():
418
433
  config, crosswalk = MediLink_ConfigLoader.load_configuration()
419
434
  config = config.get('MediLink_Config', config)
420
435
 
421
- process_dat_files(args.path, config, args.endpoint, args.is_directory, crosswalk)
436
+ # Create API client for payer name resolution
437
+ client = MediLink_API_v3.APIClient()
438
+
439
+ process_dat_files(args.path, config, args.endpoint, args.is_directory, crosswalk, client)
422
440
  print("Conversion complete.")
423
441
 
424
- def process_dat_files(path, config, endpoint, is_directory, crosswalk):
442
+ def process_dat_files(path, config, endpoint, is_directory, crosswalk, client):
425
443
  """
426
444
  Processes either a single file or all files within a directory.
427
445
 
@@ -430,6 +448,8 @@ def process_dat_files(path, config, endpoint, is_directory, crosswalk):
430
448
  - config: Configuration settings loaded from a JSON file.
431
449
  - endpoint: The endpoint for which the conversion is intended.
432
450
  - is_directory: Boolean flag indicating if the path is a directory.
451
+ - crosswalk: Crosswalk data for payer information.
452
+ - client: API client for payer name resolution.
433
453
 
434
454
  Returns:
435
455
  - None
@@ -439,10 +459,10 @@ def process_dat_files(path, config, endpoint, is_directory, crosswalk):
439
459
  for file_name in os.listdir(path):
440
460
  if file_name.endswith(".DAT"):
441
461
  file_path = os.path.join(path, file_name)
442
- process_and_write_file(file_path, config, endpoint, crosswalk)
462
+ process_and_write_file(file_path, config, endpoint, crosswalk, client)
443
463
  else:
444
464
  MediLink_ConfigLoader.log("Processing the single file: {}".format(path))
445
- process_and_write_file(path, config, endpoint, crosswalk)
465
+ process_and_write_file(path, config, endpoint, crosswalk, client)
446
466
 
447
467
  if __name__ == "__main__":
448
468
  main()
@@ -188,7 +188,11 @@ def create_1000A_submitter_name_segment(patient_data, config, endpoint):
188
188
  entity_type_qualifier = '1' if submitter_first_name else '2' # Make sure that this is correct. Original default was 2.
189
189
 
190
190
  # Construct NM1 segment for the submitter
191
- nm1_segment = "NM1*41*{}*{}*****{}*{}~".format(entity_type_qualifier, submitter_name, submitter_id_qualifier, submitter_id) # BUG - need to check submitter_name because this is written as fixed ****** which implies a single entry and not a first and last name. This is weird.
191
+ # EDI NM1 Segment Format: NM1*41*{entity_type}*{org_name}*{first}*{middle}*{prefix}*{suffix}*{id_qualifier}*{id}~
192
+ # For organizational submitters (entity_type=2), we use the organization name in NM1-03 and leave individual name fields blank
193
+ # TODO: For individual submitters (entity_type=1), we would need to parse submitter_name into first/last components
194
+ # Current implementation works in production - claims are being paid successfully
195
+ nm1_segment = "NM1*41*{}*{}*****{}*{}~".format(entity_type_qualifier, submitter_name, submitter_id_qualifier, submitter_id)
192
196
 
193
197
  # Construct PER segment for the submitter's contact information
194
198
  per_segment = "PER*IC*{}*TE*{}~".format(contact_name, contact_telephone_number)
@@ -73,7 +73,7 @@ class TokenCache:
73
73
  return token_info['access_token']
74
74
 
75
75
  # Log cache miss
76
- # BUG In the future, check the token refresh flow here to make sure we're not picking up unnecessary tokens.
76
+ # Token refresh flow validation has been implemented in get_access_token() to prevent unnecessary token pickup
77
77
  log_message = "No valid token found for {}".format(endpoint_name)
78
78
  MediLink_ConfigLoader.log(log_message, level="INFO")
79
79
 
@@ -123,8 +123,29 @@ class APIClient(BaseAPIClient):
123
123
  MediLink_ConfigLoader.log("Cached token expires at {}".format(expires_at), level="DEBUG")
124
124
  return cached_token
125
125
 
126
+ # Validate that we actually need a token before fetching
127
+ # Check if the endpoint configuration exists and is valid
128
+ try:
129
+ endpoint_config = self.config['MediLink_Config']['endpoints'][endpoint_name]
130
+ if not endpoint_config:
131
+ MediLink_ConfigLoader.log("No configuration found for endpoint: {}".format(endpoint_name), level="ERROR")
132
+ return None
133
+
134
+ # Validate required configuration fields
135
+ required_fields = ['token_url', 'client_id', 'client_secret']
136
+ missing_fields = [field for field in required_fields if field not in endpoint_config]
137
+ if missing_fields:
138
+ MediLink_ConfigLoader.log("Missing required configuration fields for {}: {}".format(endpoint_name, missing_fields), level="ERROR")
139
+ return None
140
+
141
+ except KeyError:
142
+ MediLink_ConfigLoader.log("Endpoint {} not found in configuration".format(endpoint_name), level="ERROR")
143
+ return None
144
+ except Exception as e:
145
+ MediLink_ConfigLoader.log("Error validating endpoint configuration for {}: {}".format(endpoint_name, str(e)), level="ERROR")
146
+ return None
147
+
126
148
  # If no valid token, fetch a new one
127
- endpoint_config = self.config['MediLink_Config']['endpoints'][endpoint_name]
128
149
  token_url = endpoint_config['token_url']
129
150
  data = {
130
151
  'grant_type': 'client_credentials',
@@ -138,15 +159,22 @@ class APIClient(BaseAPIClient):
138
159
 
139
160
  headers = {'Content-Type': 'application/x-www-form-urlencoded'}
140
161
 
141
- response = requests.post(token_url, headers=headers, data=data)
142
- response.raise_for_status()
143
- token_data = response.json()
144
- access_token = token_data['access_token']
145
- expires_in = token_data.get('expires_in', 3600)
146
-
147
- self.token_cache.set(endpoint_name, access_token, expires_in, current_time)
148
- MediLink_ConfigLoader.log("Obtained NEW token for endpoint: {}".format(endpoint_name), level="INFO")
149
- return access_token
162
+ try:
163
+ response = requests.post(token_url, headers=headers, data=data)
164
+ response.raise_for_status()
165
+ token_data = response.json()
166
+ access_token = token_data['access_token']
167
+ expires_in = token_data.get('expires_in', 3600)
168
+
169
+ self.token_cache.set(endpoint_name, access_token, expires_in, current_time)
170
+ MediLink_ConfigLoader.log("Obtained NEW token for endpoint: {}".format(endpoint_name), level="INFO")
171
+ return access_token
172
+ except requests.exceptions.RequestException as e:
173
+ MediLink_ConfigLoader.log("Failed to obtain token for {}: {}".format(endpoint_name, str(e)), level="ERROR")
174
+ return None
175
+ except (KeyError, ValueError) as e:
176
+ MediLink_ConfigLoader.log("Invalid token response for {}: {}".format(endpoint_name, str(e)), level="ERROR")
177
+ return None
150
178
 
151
179
  def make_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None, headers=None):
152
180
  token = self.get_access_token(endpoint_name)
@@ -334,9 +362,13 @@ def fetch_payer_name_from_api(client, payer_id, config, primary_endpoint='AVAILI
334
362
  MediLink_ConfigLoader.log(error_message, level="ERROR")
335
363
  exit(1) # Exit the script
336
364
 
337
- # BUG Force AVAILITY until I can get a payer-list api from any of the other endpoints.
338
- MediLink_ConfigLoader.log("[Fetch payer name from API] Overriding {} with AVAILITY.".format(primary_endpoint), level="DEBUG")
339
- primary_endpoint = 'AVAILITY'
365
+ # TODO: FUTURE IMPLEMENTATION - Remove AVAILITY default when other endpoints have payer-list APIs
366
+ # Currently defaulting to AVAILITY as it's the only endpoint with confirmed payer-list functionality
367
+ # In the future, this should be removed and the system should dynamically detect which endpoints
368
+ # have payer-list capabilities and use them accordingly.
369
+ if primary_endpoint != 'AVAILITY':
370
+ MediLink_ConfigLoader.log("[Fetch payer name from API] Overriding {} with AVAILITY (default until multi-endpoint payer-list support is implemented).".format(primary_endpoint), level="DEBUG")
371
+ primary_endpoint = 'AVAILITY'
340
372
 
341
373
  try:
342
374
  endpoints = config['MediLink_Config']['endpoints']
@@ -360,10 +392,36 @@ def fetch_payer_name_from_api(client, payer_id, config, primary_endpoint='AVAILI
360
392
  MediLink_ConfigLoader.log(error_message, level="ERROR")
361
393
  print(error_message)
362
394
 
363
- # Define endpoint rotation logic
364
- endpoint_order = ([primary_endpoint] +
365
- [endpoint for endpoint in endpoints if endpoint != primary_endpoint]
366
- if primary_endpoint in endpoints else list(endpoints.keys()))
395
+ # FUTURE IMPLEMENTATION: Dynamic endpoint selection based on payer-list availability
396
+ # This will replace the hardcoded AVAILITY default when other endpoints have payer-list APIs
397
+ # The logic should:
398
+ # 1. Check all endpoints for 'payer_list_endpoint' configuration
399
+ # 2. Prioritize endpoints that have confirmed payer-list functionality
400
+ # 3. Fall back to endpoints with basic payer lookup if available
401
+ # 4. Use AVAILITY as final fallback
402
+
403
+ # Define endpoint rotation logic with payer-list capability detection
404
+ available_endpoints = []
405
+
406
+ # Check which endpoints have payer-list functionality configured
407
+ for endpoint_name, endpoint_config in endpoints.items():
408
+ if 'payer_list_endpoint' in endpoint_config:
409
+ available_endpoints.append(endpoint_name)
410
+ MediLink_ConfigLoader.log("Found payer-list endpoint for {}: {}".format(endpoint_name, endpoint_config['payer_list_endpoint']), level="DEBUG")
411
+
412
+ # If no endpoints have payer-list configured, fall back to AVAILITY
413
+ if not available_endpoints:
414
+ MediLink_ConfigLoader.log("No endpoints with payer-list configuration found, using AVAILITY as fallback", level="INFO")
415
+ available_endpoints = ['AVAILITY']
416
+
417
+ # Prioritize the primary endpoint if it has payer-list capability
418
+ if primary_endpoint in available_endpoints:
419
+ endpoint_order = [primary_endpoint] + [ep for ep in available_endpoints if ep != primary_endpoint]
420
+ else:
421
+ # If primary endpoint doesn't have payer-list, use available endpoints in order
422
+ endpoint_order = available_endpoints
423
+
424
+ MediLink_ConfigLoader.log("Endpoint order for payer lookup: {}".format(endpoint_order), level="DEBUG")
367
425
 
368
426
  for endpoint_name in endpoint_order:
369
427
  try:
@@ -596,8 +596,10 @@ def organize_patient_data_by_endpoint(detailed_patient_data):
596
596
  """
597
597
  organized = {}
598
598
  for data in detailed_patient_data:
599
- # Retrieve confirmed endpoint from each patient's data
600
- endpoint = data['confirmed_endpoint'] if 'confirmed_endpoint' in data else data['suggested_endpoint']
599
+ # Retrieve endpoint in priority order: confirmed -> user_preferred -> suggested
600
+ endpoint = (data.get('confirmed_endpoint') or
601
+ data.get('user_preferred_endpoint') or
602
+ data.get('suggested_endpoint', 'AVAILITY'))
601
603
  # Initialize a list for the endpoint if it doesn't exist
602
604
  if endpoint not in organized:
603
605
  organized[endpoint] = []
MediLink/MediLink_UI.py CHANGED
@@ -131,12 +131,17 @@ def display_file_summary(index, summary):
131
131
  # Add header row if it's the first index
132
132
  if index == 1:
133
133
  print("{:<3} {:5} {:<10} {:20} {:15} {:2} {:20}".format(
134
- "No.", "Date", "ID", "Name", "Primary Ins.", "IT", "Suggested Endpoint"
134
+ "No.", "Date", "ID", "Name", "Primary Ins.", "IT", "Current Endpoint"
135
135
  ))
136
136
  print("-"*80)
137
137
 
138
138
  # Check if insurance_type is available; if not, set a default placeholder (this should already be '12' at this point)
139
139
  insurance_type = summary.get('insurance_type', '--')
140
+
141
+ # Get the effective endpoint (confirmed > user preference > suggestion > default)
142
+ effective_endpoint = (summary.get('confirmed_endpoint') or
143
+ summary.get('user_preferred_endpoint') or
144
+ summary.get('suggested_endpoint', 'AVAILITY'))
140
145
 
141
146
  # Displays the summary of a file.
142
147
  print("{:02d}. {:5} ({:<8}) {:20} {:15} {:2} {:20}".format(
@@ -146,7 +151,7 @@ def display_file_summary(index, summary):
146
151
  summary['patient_name'][:20],
147
152
  summary['primary_insurance'][:15],
148
153
  insurance_type[:2],
149
- summary['suggested_endpoint'][:20])
154
+ effective_endpoint[:20])
150
155
  )
151
156
 
152
157
  def user_select_files(file_list):