medicafe 0.240613.0__tar.gz → 0.240809.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

Files changed (53) hide show
  1. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot.bat +37 -5
  2. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot.py +13 -2
  3. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_Crosswalk_Library.py +15 -8
  4. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_Preprocessor_lib.py +14 -2
  5. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_docx_decoder.py +13 -5
  6. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink.py +42 -77
  7. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_837p_encoder.py +64 -47
  8. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_837p_encoder_library.py +24 -35
  9. medicafe-0.240809.0/MediLink/MediLink_API_Generator.py +246 -0
  10. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_API_v2.py +2 -0
  11. medicafe-0.240809.0/MediLink/MediLink_API_v3.py +429 -0
  12. medicafe-0.240809.0/MediLink/MediLink_ClaimStatus.py +144 -0
  13. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_ConfigLoader.py +13 -7
  14. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_DataMgmt.py +4 -4
  15. medicafe-0.240809.0/MediLink/MediLink_Decoder.py +165 -0
  16. medicafe-0.240809.0/MediLink/MediLink_Deductible.py +210 -0
  17. medicafe-0.240809.0/MediLink/MediLink_Down.py +153 -0
  18. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_Parser.py +106 -24
  19. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_UI.py +12 -26
  20. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_Up.py +181 -111
  21. {medicafe-0.240613.0 → medicafe-0.240809.0}/PKG-INFO +1 -1
  22. {medicafe-0.240613.0 → medicafe-0.240809.0}/medicafe.egg-info/PKG-INFO +1 -1
  23. {medicafe-0.240613.0 → medicafe-0.240809.0}/medicafe.egg-info/SOURCES.txt +4 -1
  24. {medicafe-0.240613.0 → medicafe-0.240809.0}/medicafe.egg-info/requires.txt +1 -0
  25. {medicafe-0.240613.0 → medicafe-0.240809.0}/setup.py +3 -2
  26. medicafe-0.240613.0/MediLink/MediLink_Decoder.py +0 -63
  27. medicafe-0.240613.0/MediLink/MediLink_Down.py +0 -122
  28. medicafe-0.240613.0/MediLink/MediLink_StatusCheck.py +0 -0
  29. {medicafe-0.240613.0 → medicafe-0.240809.0}/LICENSE +0 -0
  30. {medicafe-0.240613.0 → medicafe-0.240809.0}/MANIFEST.in +0 -0
  31. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_Charges.py +0 -0
  32. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_Post.py +0 -0
  33. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_Preprocessor.py +0 -0
  34. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_UI.py +0 -0
  35. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/MediBot_dataformat_library.py +0 -0
  36. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/PDF_to_CSV_Cleaner.py +0 -0
  37. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/__init__.py +0 -0
  38. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/update_json.py +0 -0
  39. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediBot/update_medicafe.py +0 -0
  40. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_APIs.py +0 -0
  41. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_Gmail.py +0 -0
  42. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_Mailer.py +0 -0
  43. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_Scan.py +0 -0
  44. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_Scheduler.py +0 -0
  45. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/MediLink_batch.bat +0 -0
  46. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/Soumit_api.py +0 -0
  47. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/__init__.py +0 -0
  48. {medicafe-0.240613.0 → medicafe-0.240809.0}/MediLink/test.py +0 -0
  49. {medicafe-0.240613.0 → medicafe-0.240809.0}/README.md +0 -0
  50. {medicafe-0.240613.0 → medicafe-0.240809.0}/medicafe.egg-info/dependency_links.txt +0 -0
  51. {medicafe-0.240613.0 → medicafe-0.240809.0}/medicafe.egg-info/not-zip-safe +0 -0
  52. {medicafe-0.240613.0 → medicafe-0.240809.0}/medicafe.egg-info/top_level.txt +0 -0
  53. {medicafe-0.240613.0 → medicafe-0.240809.0}/setup.cfg +0 -0
@@ -11,6 +11,8 @@ set "medicafe_package=medicafe"
11
11
  set "upgrade_medicafe=F:\Medibot\update_medicafe.py"
12
12
  set "temp_file=F:\Medibot\last_update_timestamp.txt"
13
13
  set "firefox_path=C:\Program Files\Mozilla Firefox\firefox.exe"
14
+ set "claims_status_script=..\MediLink\MediLink_ClaimStatus.py"
15
+ set "deductible_script=..\MediLink\MediLink_Deductible.py"
14
16
  set "package_version="
15
17
  set PYTHONWARNINGS=ignore
16
18
 
@@ -99,14 +101,18 @@ if "!internet_available!"=="1" (
99
101
  echo 1. Check for MediCafe Package Updates
100
102
  echo 2. Download Email de Carol
101
103
  echo 3. MediLink Claims
104
+ echo 4. United Claims Status
105
+ echo 5. United Deductible
102
106
  )
103
- echo 4. Run MediBot
104
- echo 5. Exit
107
+ echo 6. Run MediBot
108
+ echo 7. Exit
105
109
  echo.
106
110
  set /p choice=Enter your choice:
107
111
 
108
- if "!choice!"=="5" goto end_script
109
- if "!choice!"=="4" goto medibot_flow
112
+ if "!choice!"=="7" goto end_script
113
+ if "!choice!"=="6" goto medibot_flow
114
+ if "!choice!"=="5" goto united_deductible
115
+ if "!choice!"=="4" goto united_claims_status
110
116
  if "!choice!"=="3" goto medilink_flow
111
117
  if "!choice!"=="2" goto download_emails
112
118
  if "!choice!"=="1" goto check_updates
@@ -168,6 +174,32 @@ if errorlevel 1 echo MediLink failed to execute.
168
174
  pause
169
175
  goto main_menu
170
176
 
177
+ :: United Claims Status
178
+ :united_claims_status
179
+ if "!internet_available!"=="0" (
180
+ echo No internet connection available.
181
+ goto main_menu
182
+ )
183
+ cls
184
+ echo Checking United Claims Status...
185
+ py "%claims_status_script%"
186
+ if errorlevel 1 echo Failed to check United Claims Status.
187
+ pause
188
+ goto main_menu
189
+
190
+ :: United Deductible
191
+ :united_deductible
192
+ if "!internet_available!"=="0" (
193
+ echo No internet connection available.
194
+ goto main_menu
195
+ )
196
+ cls
197
+ echo Checking United Deductible...
198
+ py "%deductible_script%"
199
+ if errorlevel 1 echo Failed to check United Deductible.
200
+ pause
201
+ goto main_menu
202
+
171
203
  :: Process CSV Files
172
204
  :process_csvs
173
205
  for /f "tokens=1-5 delims=/: " %%a in ('echo %time%') do (
@@ -203,4 +235,4 @@ goto :eof
203
235
  :end_script
204
236
  echo Exiting MediCafe.
205
237
  pause
206
- exit /b
238
+ exit /b
@@ -262,10 +262,21 @@ if __name__ == "__main__":
262
262
  print("\nNOTE: The following patient(s) already EXIST in the system and \n will be excluded from processing:")
263
263
  # TODO ... not excluded anymore, because charges may need to be added.
264
264
  # So at this point in the processing, we should have already processed the surgery schedules and enriched the data with Charges.
265
+ # Find the corresponding rows in csv_data to get the surgery dates and store them along with patient info
266
+ patient_info = []
265
267
  for patient_id, patient_name in existing_patients:
266
- print("(ID: {0}) {1}".format(patient_id, patient_name))
268
+ surgery_date = next((row.get('Surgery Date', 'Unknown Date') for row in csv_data if row.get(reverse_mapping['Patient ID #2']) == patient_id), 'Unknown Date')
269
+ patient_info.append((surgery_date, patient_name, patient_id))
270
+
271
+ # Sort by surgery date first and then by patient name
272
+ patient_info.sort(key=lambda x: (x[0], x[1]))
273
+
274
+ # Print the sorted patient info
275
+ for index, (surgery_date, patient_name, patient_id) in enumerate(patient_info):
276
+ print("{0:03d}: {3:%m-%d} (ID: {2}) {1} ".format(index + 1, patient_name, patient_id, surgery_date))
277
+
267
278
  # Update csv_data to exclude existing patients
268
- # TODO This now has to be updated to handle patients that exist but need new charges added.
279
+ # TODO This now has to be updated to handle patients that exist but need new charges added.
269
280
  csv_data = [row for row in csv_data if row[reverse_mapping['Patient ID #2']] in patients_to_process]
270
281
  else:
271
282
  print("\nSelected patient(s) are NEW patients and will be processed.")
@@ -14,10 +14,10 @@ except ImportError:
14
14
  from MediLink import MediLink_ConfigLoader
15
15
 
16
16
  try:
17
- from MediLink_API_v2 import fetch_payer_name_from_api
17
+ from MediLink_API_v3 import fetch_payer_name_from_api
18
18
  except ImportError:
19
- from MediLink import MediLink_API_v2
20
- fetch_payer_name_from_api = MediLink_API_v2.fetch_payer_name_from_api
19
+ from MediLink import MediLink_API_v3
20
+ fetch_payer_name_from_api = MediLink_API_v3.fetch_payer_name_from_api
21
21
 
22
22
  try:
23
23
  from MediBot import MediBot_Preprocessor_lib
@@ -127,7 +127,7 @@ def initialize_crosswalk_from_mapat():
127
127
  validate_and_correct_payer_ids(crosswalk, config)
128
128
 
129
129
  # Save the initial crosswalk
130
- if save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk):
130
+ if save_crosswalk(config, crosswalk):
131
131
  message = "Crosswalk initialized with mappings for {} payers.".format(len(crosswalk.get('payer_id', {})))
132
132
  print(message)
133
133
  MediLink_ConfigLoader.log(message, config, level="INFO")
@@ -197,7 +197,7 @@ def crosswalk_update(config, crosswalk):
197
197
  crosswalk['payer_id'][payer_id]['medisoft_medicare_id'] = list(crosswalk['payer_id'][payer_id]['medisoft_medicare_id'])
198
198
 
199
199
  # Save the updated crosswalk to the specified file
200
- return save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk)
200
+ return save_crosswalk(config, crosswalk)
201
201
 
202
202
  def update_crosswalk_with_corrected_payer_id(old_payer_id, corrected_payer_id, config, crosswalk):
203
203
  """Updates the crosswalk with the corrected payer ID."""
@@ -216,7 +216,7 @@ def update_crosswalk_with_corrected_payer_id(old_payer_id, corrected_payer_id, c
216
216
  MediLink_ConfigLoader.log("Crosswalk csv_replacements updated: added {} -> {}".format(old_payer_id, corrected_payer_id), config, level="INFO")
217
217
 
218
218
  # Save the updated crosswalk
219
- return save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk)
219
+ return save_crosswalk(config, crosswalk)
220
220
 
221
221
  def update_crosswalk_with_new_payer_id(insurance_name, payer_id, config):
222
222
  """Updates the crosswalk with a new payer ID."""
@@ -229,14 +229,14 @@ def update_crosswalk_with_new_payer_id(insurance_name, payer_id, config):
229
229
  crosswalk['payer_id'][payer_id] = {"medisoft_id": [medisoft_id_str], "medisoft_medicare_id": []}
230
230
  else:
231
231
  crosswalk['payer_id'][payer_id]['medisoft_id'].append(medisoft_id_str)
232
- save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk)
232
+ save_crosswalk(config, crosswalk)
233
233
  MediLink_ConfigLoader.log("Updated crosswalk with new payer ID {} for insurance name {}".format(payer_id, insurance_name), config, level="INFO")
234
234
  else:
235
235
  message = "Failed to update crosswalk: Medisoft ID not found for insurance name {}".format(insurance_name)
236
236
  print(message)
237
237
  MediLink_ConfigLoader.log(message, config, level="ERROR")
238
238
 
239
- def save_crosswalk(crosswalk_path, crosswalk):
239
+ def save_crosswalk(config, crosswalk):
240
240
  """
241
241
  Saves the updated crosswalk to a JSON file.
242
242
  Args:
@@ -245,6 +245,13 @@ def save_crosswalk(crosswalk_path, crosswalk):
245
245
  Returns:
246
246
  bool: True if the file was successfully saved, False otherwise.
247
247
  """
248
+ # Attempt to fetch crosswalkPath from MediLink_Config
249
+ try:
250
+ crosswalk_path = config['MediLink_Config']['crosswalkPath']
251
+ except KeyError:
252
+ # If KeyError occurs, fall back to fetching crosswalkPath directly
253
+ crosswalk_path = config.get('crosswalkPath', None) # Replace None with a default value if needed
254
+
248
255
  try:
249
256
  # Initialize 'payer_id' key if not present
250
257
  if 'payer_id' not in crosswalk:
@@ -124,7 +124,8 @@ def sort_and_deduplicate(csv_data):
124
124
  if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
125
125
  unique_patients[patient_id] = row
126
126
  csv_data[:] = list(unique_patients.values())
127
- # TODO Sorting, now that we're going to have the Surgery Schedules available, should be ordered as the patients show up on the schedule.
127
+ # TODO Sorting, now that we're going to have the Surgery Schedules available, should (or shouldn't??
128
+ # maybe we should build in the option as liek a 'setting' in the config) be ordered as the patients show up on the schedule.
128
129
  # If we don't have that surgery schedule yet for some reason, we should default to the current ordering strategy.
129
130
  csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
130
131
 
@@ -161,7 +162,16 @@ def update_insurance_ids(csv_data, crosswalk):
161
162
  medisoft_ids = crosswalk['payer_id'][ins1_payer_id].get('medisoft_id', [])
162
163
  if medisoft_ids:
163
164
  medisoft_ids = [int(id) for id in medisoft_ids]
164
- # TODO Try to match OpenPM's Insurance Name to get a better match
165
+ # TODO Try to match OpenPM's Insurance Name to get a better match.
166
+ # Potential approach:
167
+ # 1. Retrieve the insurance name from the current row
168
+ # insurance_name = row.get('Primary Insurnace', '').strip()
169
+ # 2. Check if the insurance name exists in the subset of MAINS names associated with
170
+ # crosswalk medisoft ID values for the given payer ID.
171
+ # 3. If an approximate match is found above a certain confidence, use the corresponding medisoft_id.
172
+ # else: 4. If no match is found, default to the first medisoft_id
173
+ # row['Ins1 Insurance ID'] = medisoft_ids[0]
174
+
165
175
  row['Ins1 Insurance ID'] = medisoft_ids[0]
166
176
  # MediLink_ConfigLoader.log("Ins1 Insurance ID '{}' used for Payer ID {} in crosswalk.".format(row.get('Ins1 Insurance ID', ''), ins1_payer_id))
167
177
  else:
@@ -367,6 +377,8 @@ def load_insurance_data_from_mains(config):
367
377
  # Retrieve MAINS path and slicing information from the configuration
368
378
  # TODO (Low) For secondary insurance, this needs to be pulling from the correct MAINS (there are 2)
369
379
  # TODO (Low) Performance: There probably needs to be a dictionary proxy for MAINS that gets updated.
380
+ # Meh, this just has to be part of the new architecture plan where we make Medisoft a downstream
381
+ # recipient from the db.
370
382
  mains_path = config['MAINS_MED_PATH']
371
383
  mains_slices = crosswalk['mains_mapping']['slices']
372
384
 
@@ -238,10 +238,19 @@ def parse_patient_id(text):
238
238
 
239
239
  def parse_diagnosis_code(text):
240
240
  try:
241
- if '(' in text and ')' in text: # Extract the diagnosis code before the '/'
242
- full_code = text[text.index('(')+1:text.index(')')]
243
- return full_code.split('/')[0]
244
- return text.split('/')[0]
241
+ # Regular expression to find all ICD-10 codes starting with 'H' and containing a period
242
+ pattern = re.compile(r'H\d{2}\.\d+')
243
+ matches = pattern.findall(text)
244
+
245
+ if matches:
246
+ return matches[0] # Return the first match
247
+ else:
248
+ # Fallback to original method if no match is found
249
+ if '(' in text and ')' in text: # Extract the diagnosis code before the '/'
250
+ full_code = text[text.index('(')+1:text.index(')')]
251
+ return full_code.split('/')[0]
252
+ return text.split('/')[0]
253
+
245
254
  except Exception as e:
246
255
  MediLink_ConfigLoader.log("Error parsing diagnosis code: {}. Error: {}".format(text, e))
247
256
  return "Unknown"
@@ -268,7 +277,6 @@ def parse_femto_yes_or_no(text):
268
277
  MediLink_ConfigLoader.log("Error parsing femto yes or no: {}. Error: {}".format(text, e))
269
278
  return False
270
279
 
271
-
272
280
  def rotate_docx_files(directory):
273
281
  # List all files in the directory
274
282
  files = os.listdir(directory)
@@ -2,7 +2,6 @@ import os
2
2
  import MediLink_Down
3
3
  import MediLink_Up
4
4
  import MediLink_ConfigLoader
5
- import MediLink_837p_encoder
6
5
  import MediLink_DataMgmt
7
6
 
8
7
  # For UI Functions
@@ -19,73 +18,31 @@ from MediBot import MediBot_Preprocessor_lib
19
18
  load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
20
19
  from MediBot import MediBot_Crosswalk_Library
21
20
 
22
- # Define insurance options with codes and descriptions.
23
- # TODO This needs to move to the config file
24
- insurance_options = {
25
- "11": "Other Non-Federal Programs",
26
- "12": "Preferred Provider Organization (PPO)",
27
- "13": "Point of Service (POS)",
28
- "14": "Exclusive Provider Organization (EPO)",
29
- "15": "Indemnity Insurance",
30
- "16": "Health Maintenance Organization (HMO) Medicare Risk",
31
- "17": "Dental Maintenance Organization",
32
- "AM": "Automobile Medical",
33
- "BL": "Blue Cross/Blue Shield",
34
- "CH": "Champus",
35
- "CI": "Commercial Insurance Co.",
36
- "DS": "Disability",
37
- "FI": "Federal Employees Program",
38
- "HM": "Health Maintenance Organization",
39
- "LM": "Liability Medical",
40
- "MA": "Medicare Part A",
41
- "MB": "Medicare Part B",
42
- "MC": "Medicaid",
43
- "OF": "Other Federal Program",
44
- "TV": "Title V",
45
- "VA": "Veterans Affairs Plan",
46
- "WC": "Workers Compensation Health Claim",
47
- "ZZ": "Mutually Defined"
48
- }
49
-
50
- def detect_and_display_file_summaries(directory_path, config, crosswalk):
21
+ # Retrieve insurance options with codes and descriptions
22
+ config, _ = MediLink_ConfigLoader.load_configuration()
23
+ insurance_options = config['MediLink_Config'].get('insurance_options')
24
+
25
+ def collect_detailed_patient_data(selected_files, config, crosswalk):
51
26
  """
52
- Detects new files in the specified directory and prepares detailed patient data for processing,
53
- including suggestions for endpoints based on insurance provider information found in the config.
27
+ Collects detailed patient data from the selected files.
54
28
 
55
- :param directory_path: Path to the directory containing files to be detected.
29
+ :param selected_files: List of selected file paths.
56
30
  :param config: Configuration settings loaded from a JSON file.
57
31
  :param crosswalk: Crosswalk data for mapping purposes.
58
- :return: A tuple containing a list of new file paths and the detailed patient data.
32
+ :return: A list of detailed patient data.
59
33
  """
60
- try:
61
- new_files, file_flagged = MediLink_DataMgmt.detect_new_files(directory_path)
62
- if not new_files:
63
- print("No new claims detected. Check Medisoft claims output.")
64
- MediLink_ConfigLoader.log("No new claims detected. Check Medisoft claims output.")
65
- return False, []
66
-
67
- if not file_flagged:
68
- selected_files = MediLink_UI.user_select_files(new_files)
69
- else:
70
- # Extract the newest single latest file from the list
71
- selected_files = [max(new_files, key=os.path.getctime)]
72
-
73
- detailed_patient_data = [] # Initialize list for detailed patient data
74
- for file_path in selected_files:
75
- detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
76
- detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
77
-
78
- # Enrich the detailed patient data with insurance type
79
- detailed_patient_data = enrich_with_insurance_type(detailed_patient_data, insurance_options)
34
+ detailed_patient_data = []
35
+ for file_path in selected_files:
36
+ detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
37
+ detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
80
38
 
81
- # Display summaries and provide an option for bulk edit
82
- MediLink_UI.display_patient_summaries(detailed_patient_data)
39
+ # Enrich the detailed patient data with insurance type
40
+ detailed_patient_data = enrich_with_insurance_type(detailed_patient_data, insurance_options)
41
+
42
+ # Display summaries and provide an option for bulk edit
43
+ MediLink_UI.display_patient_summaries(detailed_patient_data)
83
44
 
84
- # Return the list of new files and the enriched detailed patient data
85
- return selected_files, detailed_patient_data
86
- except Exception as e:
87
- MediLink_ConfigLoader.log("Error in detect_and_display_file_summaries: {}".format(e))
88
- return False, []
45
+ return detailed_patient_data
89
46
 
90
47
  def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
91
48
  """
@@ -101,7 +58,7 @@ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_map
101
58
  TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
102
59
  """
103
60
  if patient_insurance_type_mapping is None:
104
- MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.")
61
+ MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.", level="WARNING")
105
62
  patient_insurance_type_mapping = {}
106
63
 
107
64
  for data in detailed_patient_data:
@@ -129,7 +86,7 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
129
86
 
130
87
  # Load insurance data from MAINS to create a mapping from insurance names to their respective IDs
131
88
  insurance_to_id = load_insurance_data_from_mains(config)
132
- MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
89
+ MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)), level="INFO")
133
90
 
134
91
  for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
135
92
  parsed_data = MediLink_DataMgmt.parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2, service_info_3, config.get('MediLink_Config', config))
@@ -191,13 +148,13 @@ def check_for_new_remittances(config):
191
148
  try:
192
149
  ERA_path = MediLink_Down.main(desired_endpoint=endpoint_key)
193
150
  processed_endpoints.append((endpoint_info['name'], ERA_path))
194
- MediLink_ConfigLoader.log("Results for {} saved to: {}".format(endpoint_info['name'], ERA_path))
151
+ MediLink_ConfigLoader.log("Results for {} saved to: {}".format(endpoint_info['name'], ERA_path), level="DEBUG")
195
152
  # TODO (Low SFTP - Download side) This needs to check to see if this actually worked maybe winscplog before saying it completed successfully
196
153
  # Check if there is commonality with the upload side so we can use the same validation function.
197
154
  except Exception as e:
198
155
  print("An error occurred while checking remittances for {}: {}".format(endpoint_info['name'], e))
199
156
  else:
200
- MediLink_ConfigLoader.log("Skipping endpoint '{}' as it does not have 'remote_directory_down' configured.".format(endpoint_info['name']))
157
+ MediLink_ConfigLoader.log("Skipping endpoint '{}' as it does not have 'remote_directory_down' configured.".format(endpoint_info['name']), level="WARNING")
201
158
  else:
202
159
  print("Error: Endpoint config is not a 'dictionary' as expected.")
203
160
  # Check if all ERA paths are the same
@@ -299,17 +256,14 @@ def main_menu():
299
256
  # Normalize the directory path for file operations.
300
257
  directory_path = os.path.normpath(config['MediLink_Config']['inputFilePath'])
301
258
 
302
- # Detect new files and collect detailed patient data if available.
303
- new_files, detailed_patient_data = detect_and_display_file_summaries(directory_path, config, crosswalk)
304
-
305
- if new_files:
306
- handle_submission(detailed_patient_data, config)
259
+ # Detect files and determine if a new file is flagged.
260
+ all_files, file_flagged = MediLink_DataMgmt.detect_new_files(directory_path)
307
261
 
308
262
  while True:
309
263
  # Define the menu options. Base options include checking remittances and exiting the program.
310
264
  options = ["Check for new remittances", "Exit"]
311
- # If new files are detected, add the option to submit claims.
312
- if new_files:
265
+ # If any files are detected, add the option to submit claims.
266
+ if all_files:
313
267
  options.insert(1, "Submit claims")
314
268
 
315
269
  # Display the dynamically adjusted menu options.
@@ -320,11 +274,22 @@ def main_menu():
320
274
  if choice == '1':
321
275
  # Handle remittance checking.
322
276
  check_for_new_remittances(config)
323
- elif choice == '2' and new_files:
324
- # Handle the claims submission flow if new files are present.
325
- handle_submission(detailed_patient_data, config) # Since we have the bulk edit, we should actually go there first
326
- elif choice == '3' or (choice == '2' and not new_files):
327
- # Exit the program if the user chooses to exit or if no new files are present.
277
+ elif choice == '2' and all_files:
278
+ # Handle the claims submission flow if any files are present.
279
+ if file_flagged:
280
+ # Extract the newest single latest file from the list if a new file is flagged.
281
+ selected_files = [max(all_files, key=os.path.getctime)]
282
+ else:
283
+ # Prompt the user to select files if no new file is flagged.
284
+ selected_files = MediLink_UI.user_select_files(all_files)
285
+
286
+ # Collect detailed patient data for selected files.
287
+ detailed_patient_data = collect_detailed_patient_data(selected_files, config, crosswalk)
288
+
289
+ # Process the claims submission.
290
+ handle_submission(detailed_patient_data, config)
291
+ elif choice == '3' or (choice == '2' and not all_files):
292
+ # Exit the program if the user chooses to exit or if no files are present.
328
293
  MediLink_UI.display_exit_message()
329
294
  break
330
295
  else:
@@ -77,33 +77,27 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
77
77
 
78
78
  return formatted_837p
79
79
 
80
- def write_output_file(document_segments, output_directory, endpoint_key, input_file_path, config):
80
+ def write_output_file(document_segments, output_directory, endpoint_key, input_file_path, config, suffix=""):
81
81
  """
82
82
  Writes formatted 837P document segments to an output file with a dynamically generated name.
83
-
84
- Development Roadmap:
85
- - Ensure input `document_segments` is a non-empty list to avoid creating empty files.
86
- - Verify `output_directory` exists and is writable before proceeding. Create the directory if it does not exist.
87
- - Consider parameterizing the file naming convention or providing options for customization to accommodate different organizational needs.
88
- - Implement error handling to gracefully manage file writing failures, potentially returning a status or error message alongside the file path.
89
- - Incorporate logging directly within the function, accepting an optional `config` or `logger` parameter to facilitate tracking of the file writing process and outcomes.
90
- - Update the return value to include both the path to the output file and any relevant status information (e.g., success flag, error message) to enhance downstream error handling and user feedback.
91
-
83
+
92
84
  Parameters:
93
85
  - document_segments: List of strings, where each string is a segment of the 837P document to be written.
94
86
  - output_directory: String specifying the directory where the output file will be saved.
95
87
  - endpoint_key: String specifying the endpoint for which the claim is processed, used in naming the output file.
96
88
  - input_file_path: String specifying the path to the input file being processed, used in naming the output file.
97
-
89
+ - config: Configuration settings for logging and other purposes.
90
+ - suffix: Optional string to differentiate filenames, useful for single-patient processing.
91
+
98
92
  Returns:
99
- - String specifying the path to the successfully created output file. Consider returning a tuple (path, status) for enhanced error handling.
93
+ - String specifying the path to the successfully created output file, or None if an error occurred.
100
94
  """
101
- # Check if document segments are empty
95
+ # Ensure the document segments are not empty
102
96
  if not document_segments:
103
97
  MediLink_ConfigLoader.log("Error: Empty document segments provided. No output file created.", config, level="ERROR")
104
98
  return None
105
99
 
106
- # Check if the output directory exists and is writable
100
+ # Verify the output directory exists and is writable, create if necessary
107
101
  if not os.path.exists(output_directory):
108
102
  try:
109
103
  os.makedirs(output_directory)
@@ -114,13 +108,13 @@ def write_output_file(document_segments, output_directory, endpoint_key, input_f
114
108
  MediLink_ConfigLoader.log("Error: Output directory is not writable.", config, level="ERROR")
115
109
  return None
116
110
 
117
- # Generate new output file path
111
+ # Generate the new output file path
118
112
  base_name = os.path.splitext(os.path.basename(input_file_path))[0]
119
113
  timestamp = datetime.now().strftime("%m%d%H%M")
120
- new_output_file_name = "{}_{}_{}.txt".format(base_name, endpoint_key.lower(), timestamp)
114
+ new_output_file_name = "{}_{}_{}{}.txt".format(base_name, endpoint_key.lower(), timestamp, suffix)
121
115
  new_output_file_path = os.path.join(output_directory, new_output_file_name)
122
116
 
123
- # Write formatted 837P document to the output file
117
+ # Write the document to the output file
124
118
  try:
125
119
  with open(new_output_file_path, 'w') as output_file:
126
120
  output_file.write('\n'.join(document_segments))
@@ -335,7 +329,7 @@ def main():
335
329
  parser.add_argument(
336
330
  "-e", "--endpoint",
337
331
  required=True,
338
- choices=["AVAILITY", "OPTUMEDI", "PNT_DATA"],
332
+ choices=["AVAILITY", "OPTUMEDI", "PNT_DATA", "UHCAPI"],
339
333
  help="Specify the endpoint for which the conversion is intended."
340
334
  )
341
335
  parser.add_argument(
@@ -388,9 +382,9 @@ if __name__ == "__main__":
388
382
  #######################################################################################
389
383
 
390
384
  def convert_files_for_submission(detailed_patient_data, config):
391
- """
385
+ """
392
386
  Processes detailed patient data for submission based on their confirmed endpoints,
393
- generating a separate 837P file for each endpoint.
387
+ generating separate 837P files for each endpoint according to the configured submission type.
394
388
 
395
389
  Parameters:
396
390
  - detailed_patient_data: A list containing detailed patient data with endpoint information.
@@ -398,73 +392,93 @@ def convert_files_for_submission(detailed_patient_data, config):
398
392
 
399
393
  Returns:
400
394
  - A list of paths to the converted files ready for submission.
395
+
396
+ Note:
397
+ - This function currently supports batch and single-patient submissions based on the configuration.
398
+ - Future implementation may include progress tracking using tools like `tqdm`.
401
399
  """
402
400
 
403
401
  # Initialize a dictionary to hold patient data segregated by confirmed endpoints
404
402
  data_by_endpoint = {}
405
-
406
- # Populate the dictionary with patient data
403
+
404
+ # Group patient data by endpoint
407
405
  for data in detailed_patient_data:
408
- endpoint = data['confirmed_endpoint']
409
- if endpoint not in data_by_endpoint:
410
- data_by_endpoint[endpoint] = []
411
- data_by_endpoint[endpoint].append(data)
406
+ endpoint = data.get('confirmed_endpoint')
407
+ if endpoint:
408
+ if endpoint not in data_by_endpoint:
409
+ data_by_endpoint[endpoint] = []
410
+ data_by_endpoint[endpoint].append(data)
412
411
 
413
412
  # List to store paths of converted files for each endpoint
414
413
  converted_files_paths = []
415
414
 
416
- # Determine the total number of unique endpoints for progress bar
417
- # total_endpoints = len(data_by_endpoint)
418
-
419
415
  # Iterate over each endpoint and process its corresponding patient data
420
416
  for endpoint, patient_data_list in data_by_endpoint.items():
421
- # tqdm(data_by_endpoint.items(), desc="Creating 837p(s)", total=total_endpoints, ascii=True)
422
- # Each endpoint might have multiple patients' data to be processed into a single 837P file
423
- if patient_data_list:
424
- converted_path = process_claim(config['MediLink_Config'], endpoint, patient_data_list)
417
+ # Retrieve submission type from config; default to "batch" if not specified
418
+ submission_type = config.get('MediLink_Config', {}).get('endpoints', {}).get(endpoint, {}).get('submission_type', 'batch')
419
+
420
+ if submission_type == 'single':
421
+ # Process each patient's data individually for single-patient submissions
422
+ for patient_data in patient_data_list:
423
+ # Generate a unique suffix for each patient, e.g., using a truncated chart number
424
+ chart_number = patient_data.get('CHART', 'UNKNOWN')#[:5] truncation might cause collisions.
425
+ suffix = "_{}".format(chart_number)
426
+ # Process and convert each patient's data to a separate file
427
+ converted_path = process_claim(config, endpoint, [patient_data], suffix)
428
+ if converted_path:
429
+ converted_files_paths.append(converted_path)
430
+ else:
431
+ # Process all patient data together for batch submissions
432
+ converted_path = process_claim(config, endpoint, patient_data_list)
425
433
  if converted_path:
426
434
  converted_files_paths.append(converted_path)
427
-
435
+
428
436
  return converted_files_paths
429
437
 
430
- def process_claim(config, endpoint, patient_data_list):
438
+ def process_claim(config, endpoint, patient_data_list, suffix=""):
431
439
  """
432
440
  Processes patient data for a specified endpoint, converting it into the 837P format.
433
- Generates a separate 837P file for each endpoint based on detailed patient data.
441
+ Can handle both batch and single-patient submissions.
434
442
 
435
443
  Parameters:
436
444
  - config: Configuration settings loaded from a JSON file.
437
- - endpoint_key: The key representing the endpoint for which the data is being processed.
445
+ - endpoint: The key representing the endpoint for which the data is being processed.
438
446
  - patient_data_list: A list of dictionaries, each containing detailed patient data.
447
+ - suffix: An optional suffix to differentiate filenames for single-patient processing.
439
448
 
440
449
  Returns:
441
450
  - Path to the converted file, or None if an error occurs.
442
-
443
- TODO (LOW) Why are there duplicated interchange flows? Because the arg if we're doing a .dat directory or not.
444
- Although, that shouldn't be making duplicates of these interchange headers. That's still confusing and could end up making
445
- duplicate interchange headers because processing .dat in batch might be fast enough to be a problem.
446
451
  """
452
+ # Ensure we're accessing the correct configuration key
453
+ config = config.get('MediLink_Config', config)
454
+
455
+ # Retrieve the output directory from the configuration
447
456
  output_directory = MediLink_837p_encoder_library.get_output_directory(config)
448
457
  if not output_directory:
449
458
  return None
450
459
 
451
- # Initialize the transaction set control number and document segments
452
460
  transaction_set_control_number = 1
453
461
  document_segments = []
454
462
 
455
- # Process each patient's data in the list
456
463
  for patient_data in patient_data_list:
457
- # Validate each patient's data
464
+ # Validate each patient's data before processing
458
465
  is_valid, validation_errors = validate_claim_data(patient_data, config)
459
466
  if is_valid:
460
- # Format the claim if data is valid
467
+ # Format the claim into 837P segments
461
468
  formatted_claim = format_single_claim(patient_data, config, endpoint, transaction_set_control_number)
462
469
  document_segments.append(formatted_claim)
463
470
  transaction_set_control_number += 1
464
471
  else:
472
+ # Log any validation errors encountered
473
+ MediLink_ConfigLoader.log("Validation errors for patient data: {}".format(validation_errors), config, level="ERROR")
465
474
  if MediLink_837p_encoder_library.handle_validation_errors(transaction_set_control_number, validation_errors, config):
466
475
  continue # Skip the current patient
467
476
 
477
+ if not document_segments:
478
+ # If no valid segments were created, log the issue and return None
479
+ MediLink_ConfigLoader.log("No valid document segments created.", config, level="ERROR")
480
+ return None
481
+
468
482
  # Create interchange elements with the final transaction set control number
469
483
  isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
470
484
 
@@ -473,5 +487,8 @@ def process_claim(config, endpoint, patient_data_list):
473
487
  document_segments.insert(0, isa_header)
474
488
  document_segments.extend([ge_trailer, iea_trailer])
475
489
 
476
- # Write the complete 837P document to an output file and return its path
477
- return write_output_file(document_segments, output_directory, endpoint, patient_data_list[0]['file_path'], config)
490
+ # Use the first patient's file path as a reference for output file naming
491
+ input_file_path = patient_data_list[0].get('file_path', 'UNKNOWN')
492
+ # Write the complete 837P document to an output file
493
+ converted_file_path = write_output_file(document_segments, output_directory, endpoint, input_file_path, config, suffix)
494
+ return converted_file_path