medicafe 0.250725.17__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.
- MediBot/MediBot.bat +47 -1
- MediBot/MediBot.py +7 -2
- MediBot/MediBot_Crosswalk_Library.py +30 -10
- MediBot/MediBot_Preprocessor_lib.py +94 -6
- MediBot/MediBot_UI.py +348 -321
- MediBot/update_medicafe.py +29 -1
- MediLink/MediLink.py +161 -22
- MediLink/MediLink_837p_encoder.py +31 -11
- MediLink/MediLink_837p_encoder_library.py +5 -1
- MediLink/MediLink_API_v3.py +76 -18
- MediLink/MediLink_DataMgmt.py +4 -2
- MediLink/MediLink_UI.py +7 -2
- MediLink/MediLink_Up.py +32 -17
- {medicafe-0.250725.17.dist-info → medicafe-0.250728.0.dist-info}/METADATA +1 -1
- {medicafe-0.250725.17.dist-info → medicafe-0.250728.0.dist-info}/RECORD +18 -18
- {medicafe-0.250725.17.dist-info → medicafe-0.250728.0.dist-info}/LICENSE +0 -0
- {medicafe-0.250725.17.dist-info → medicafe-0.250728.0.dist-info}/WHEEL +0 -0
- {medicafe-0.250725.17.dist-info → medicafe-0.250728.0.dist-info}/top_level.txt +0 -0
MediBot/update_medicafe.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
#
|
|
288
|
-
|
|
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
|
|
295
|
+
print("Invalid selection. Keeping the current endpoint.")
|
|
296
|
+
data['confirmed_endpoint'] = current_effective_endpoint
|
|
291
297
|
else:
|
|
292
|
-
data['confirmed_endpoint'] =
|
|
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("
|
|
307
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
MediLink/MediLink_API_v3.py
CHANGED
|
@@ -73,7 +73,7 @@ class TokenCache:
|
|
|
73
73
|
return token_info['access_token']
|
|
74
74
|
|
|
75
75
|
# Log cache miss
|
|
76
|
-
#
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
#
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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:
|
MediLink/MediLink_DataMgmt.py
CHANGED
|
@@ -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
|
|
600
|
-
endpoint = data
|
|
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", "
|
|
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
|
-
|
|
154
|
+
effective_endpoint[:20])
|
|
150
155
|
)
|
|
151
156
|
|
|
152
157
|
def user_select_files(file_list):
|