medicafe 0.240517.0__py3-none-any.whl → 0.240716.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (37) hide show
  1. MediBot/MediBot.bat +46 -6
  2. MediBot/MediBot.py +9 -36
  3. MediBot/MediBot_Charges.py +0 -28
  4. MediBot/MediBot_Crosswalk_Library.py +16 -8
  5. MediBot/MediBot_Post.py +0 -0
  6. MediBot/MediBot_Preprocessor.py +26 -63
  7. MediBot/MediBot_Preprocessor_lib.py +182 -43
  8. MediBot/MediBot_UI.py +2 -7
  9. MediBot/MediBot_dataformat_library.py +0 -9
  10. MediBot/MediBot_docx_decoder.py +283 -60
  11. MediLink/MediLink.py +80 -120
  12. MediLink/MediLink_837p_encoder.py +3 -28
  13. MediLink/MediLink_837p_encoder_library.py +19 -53
  14. MediLink/MediLink_API_Generator.py +246 -0
  15. MediLink/MediLink_API_v2.py +2 -0
  16. MediLink/MediLink_API_v3.py +325 -0
  17. MediLink/MediLink_APIs.py +2 -0
  18. MediLink/MediLink_ClaimStatus.py +144 -0
  19. MediLink/MediLink_ConfigLoader.py +13 -7
  20. MediLink/MediLink_DataMgmt.py +224 -68
  21. MediLink/MediLink_Decoder.py +165 -0
  22. MediLink/MediLink_Deductible.py +203 -0
  23. MediLink/MediLink_Down.py +122 -96
  24. MediLink/MediLink_Gmail.py +453 -74
  25. MediLink/MediLink_Mailer.py +0 -7
  26. MediLink/MediLink_Parser.py +193 -0
  27. MediLink/MediLink_Scan.py +0 -0
  28. MediLink/MediLink_Scheduler.py +2 -172
  29. MediLink/MediLink_StatusCheck.py +0 -4
  30. MediLink/MediLink_UI.py +54 -18
  31. MediLink/MediLink_Up.py +6 -15
  32. {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/METADATA +4 -1
  33. medicafe-0.240716.2.dist-info/RECORD +47 -0
  34. {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/WHEEL +1 -1
  35. medicafe-0.240517.0.dist-info/RECORD +0 -39
  36. {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/LICENSE +0 -0
  37. {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/top_level.txt +0 -0
@@ -1,80 +1,303 @@
1
- """
2
- Using docx-utils 0.1.3,
3
-
4
- This script parses a .docx file containing a table of patient information and extracts
5
- relevant data into a dictionary. Each row in the table corresponds to a new patient,
6
- and the data from each cell is parsed into specific variables. The resulting dictionary
7
- uses the 'Patient ID Number' as keys and lists containing 'Diagnosis Code',
8
- 'Left or Right Eye', and 'Femto yes or no' as values.
9
-
10
- Functions:
11
- parse_docx(filepath): Reads the .docx file and constructs the patient data dictionary.
12
- parse_patient_id(text): Extracts the Patient ID Number from the text.
13
- parse_diagnosis_code(text): Extracts the Diagnosis Code from the text.
14
- parse_left_or_right_eye(text): Extracts the eye information (Left or Right) from the text.
15
- parse_femto_yes_or_no(text): Extracts the Femto information (yes or no) from the text.
16
- """
17
-
18
1
  from docx import Document
2
+ import re
3
+ from lxml import etree
4
+ import zipfile
5
+ from datetime import datetime
6
+ import os
7
+ import sys
8
+ from collections import OrderedDict
9
+
10
+ # Add parent directory of the project to the Python path
11
+ project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
12
+ sys.path.append(project_dir)
13
+
14
+ try:
15
+ import MediLink_ConfigLoader
16
+ except ImportError:
17
+ from MediLink import MediLink_ConfigLoader
19
18
 
20
19
  def parse_docx(filepath):
21
- # Open the .docx file
22
- doc = Document(filepath)
20
+ try:
21
+ doc = Document(filepath) # Open the .docx file
22
+ except Exception as e:
23
+ MediLink_ConfigLoader.log("Error opening document: {}".format(e)) # Log error
24
+ return {}
25
+
26
+ patient_data = OrderedDict() # Initialize OrderedDict to store data
27
+ MediLink_ConfigLoader.log("Extracting Date of Service from {}".format(filepath), level="DEBUG")
28
+
29
+ date_of_service = extract_date_of_service(filepath) # Extract date of service
30
+ MediLink_ConfigLoader.log("Date of Service recorded as: {}".format(date_of_service), level="DEBUG")
31
+
32
+ for table in doc.tables: # Iterate over tables in the document
33
+ for row in table.rows:
34
+ cells = [cell.text.strip() for cell in row.cells]
35
+ if len(cells) > 4 and cells[3].startswith('#'):
36
+ try:
37
+ patient_id = parse_patient_id(cells[3])
38
+ diagnosis_code = parse_diagnosis_code(cells[4])
39
+ left_or_right_eye = parse_left_or_right_eye(cells[4])
40
+ femto_yes_or_no = parse_femto_yes_or_no(cells[4])
41
+
42
+ if patient_id not in patient_data:
43
+ patient_data[patient_id] = {}
44
+
45
+ if date_of_service in patient_data[patient_id]:
46
+ MediLink_ConfigLoader.log("Duplicate entry for patient ID {} on date {}. Skipping.".format(patient_id, date_of_service))
47
+ else:
48
+ patient_data[patient_id][date_of_service] = [diagnosis_code, left_or_right_eye, femto_yes_or_no]
49
+ except Exception as e:
50
+ MediLink_ConfigLoader.log("Error processing row: {}. Error: {}".format(cells, e))
51
+
52
+ # Validation steps
53
+ validate_unknown_entries(patient_data)
54
+ validate_diagnostic_code(patient_data)
23
55
 
24
- # Initialize the dictionary to store data
25
- patient_data = {}
56
+ return patient_data
57
+
58
+ def validate_unknown_entries(patient_data):
59
+ for patient_id, dates in list(patient_data.items()):
60
+ for date, details in list(dates.items()):
61
+ if 'Unknown' in details:
62
+ warning_message = "Warning: 'Unknown' entry found. Patient ID: {}, Date: {}, Details: {}".format(patient_id, date, details)
63
+ MediLink_ConfigLoader.log(warning_message, level="WARNING")
64
+ print(warning_message)
65
+ del patient_data[patient_id][date]
66
+ if not patient_data[patient_id]: # If no dates left for the patient, remove the patient
67
+ del patient_data[patient_id]
68
+
69
+ def validate_diagnostic_code(patient_data):
70
+ for patient_id, dates in patient_data.items():
71
+ for date, details in dates.items():
72
+ diagnostic_code, eye, _ = details
73
+ if diagnostic_code[-1].isdigit():
74
+ if eye == 'Left' and not diagnostic_code.endswith('2'):
75
+ log_and_warn(patient_id, date, diagnostic_code, eye)
76
+ elif eye == 'Right' and not diagnostic_code.endswith('1'):
77
+ log_and_warn(patient_id, date, diagnostic_code, eye)
78
+
79
+ def log_and_warn(patient_id, date, diagnostic_code, eye):
80
+ warning_message = (
81
+ "Warning: Mismatch found for Patient ID: {}, Date: {}, "
82
+ "Diagnostic Code: {}, Eye: {}".format(patient_id, date, diagnostic_code, eye)
83
+ )
84
+ MediLink_ConfigLoader.log(warning_message, level="WARNING")
85
+ print(warning_message)
86
+
87
+ # Extract and parse the date of service from the .docx file
88
+ def extract_date_of_service(docx_path):
89
+ extract_to = "extracted_docx"
90
+ try:
91
+ if not os.path.exists(extract_to):
92
+ os.makedirs(extract_to)
93
+ with zipfile.ZipFile(docx_path, 'r') as docx:
94
+ docx.extractall(extract_to)
95
+ MediLink_ConfigLoader.log("Extracted DOCX to: {}".format(extract_to), level="DEBUG")
96
+
97
+ file_path = find_text_in_xml(extract_to, "Surgery Schedule")
98
+ if file_path:
99
+ return extract_date_from_file(file_path)
100
+ else:
101
+ MediLink_ConfigLoader.log("Target text 'Surgery Schedule' not found in any XML files.", level="WARNING")
102
+ return None
103
+ finally:
104
+ # Clean up the extracted files
105
+ remove_directory(extract_to)
106
+ MediLink_ConfigLoader.log("Cleaned up extracted files in: {}".format(extract_to), level="DEBUG")
107
+
108
+ def remove_directory(path):
109
+ if os.path.exists(path):
110
+ for root, dirs, files in os.walk(path, topdown=False):
111
+ for name in files:
112
+ os.remove(os.path.join(root, name))
113
+ for name in dirs:
114
+ os.rmdir(os.path.join(root, name))
115
+ os.rmdir(path)
116
+
117
+ # Find the target text in the extracted XML files
118
+ def find_text_in_xml(directory, target_text):
119
+ for root_dir, dirs, files in os.walk(directory):
120
+ for file in files:
121
+ if file.endswith('.xml'):
122
+ file_path = os.path.join(root_dir, file)
123
+ try:
124
+ tree = etree.parse(file_path)
125
+ root = tree.getroot()
126
+ namespaces = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} # hardcoded for XP handling BUG
127
+ for elem in root.xpath('//w:t', namespaces=namespaces):
128
+ if elem.text and target_text in elem.text:
129
+ MediLink_ConfigLoader.log("Found target text in file: {}".format(file_path), level="DEBUG")
130
+ return file_path
131
+ except Exception as e:
132
+ MediLink_ConfigLoader.log("Error parsing XML file {}: {}".format(file_path, e))
133
+ print("Error parsing XML file {}: {}".format(file_path, e))
134
+ return None
135
+
136
+ # Normalize month and day abbreviations
137
+ def normalize_text(text):
138
+ month_map = {
139
+ 'JAN': 'JANUARY', 'FEB': 'FEBRUARY', 'MAR': 'MARCH', 'APR': 'APRIL',
140
+ 'MAY': 'MAY', 'JUN': 'JUNE', 'JUL': 'JULY', 'AUG': 'AUGUST',
141
+ 'SEP': 'SEPTEMBER', 'OCT': 'OCTOBER', 'NOV': 'NOVEMBER', 'DEC': 'DECEMBER'
142
+ }
143
+ day_map = {
144
+ 'MON': 'MONDAY', 'TUE': 'TUESDAY', 'WED': 'WEDNESDAY', 'THU': 'THURSDAY',
145
+ 'FRI': 'FRIDAY', 'SAT': 'SATURDAY', 'SUN': 'SUNDAY'
146
+ }
26
147
 
27
- # Assuming the first table contains the required data
28
- table = doc.tables[0]
148
+ for abbr, full in month_map.items():
149
+ text = re.sub(r'\b' + abbr + r'\b', full, text, flags=re.IGNORECASE)
150
+ for abbr, full in day_map.items():
151
+ text = re.sub(r'\b' + abbr + r'\b', full, text, flags=re.IGNORECASE)
29
152
 
30
- # Iterate over the rows in the table
31
- for row in table.rows[1:]: # Skip header row if it exists
32
- cells = row.cells
153
+ return text
154
+
155
+ def reassemble_year(text):
156
+ # First, handle the most common case where a 4-digit year is split as (3,1), (1,3), or (2,2)
157
+ text = re.sub(r'(\d{3}) (\d{1})', r'\1\2', text)
158
+ text = re.sub(r'(\d{1}) (\d{3})', r'\1\2', text)
159
+ text = re.sub(r'(\d{2}) (\d{2})', r'\1\2', text)
160
+
161
+ # Handle the less common cases where the year might be split as (1,1,2) or (2,1,1) or (1,2,1)
162
+ parts = re.findall(r'\b(\d{1,2})\b', text)
163
+ if len(parts) >= 4:
164
+ for i in range(len(parts) - 3):
165
+ candidate = ''.join(parts[i:i + 4])
166
+ if len(candidate) == 4 and candidate.isdigit():
167
+ combined_year = candidate
168
+ text = re.sub(r'\b' + r'\b \b'.join(parts[i:i + 4]) + r'\b', combined_year, text)
169
+ break
170
+
171
+ return text
172
+
173
+ # Extract and parse the date from the file
174
+ def extract_date_from_file(file_path):
175
+ try:
176
+ tree = etree.parse(file_path)
177
+ root = tree.getroot()
178
+ collected_text = []
179
+
180
+ namespaces = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} # hardcoded for XP handling BUG
181
+ for elem in root.xpath('//w:t', namespaces=namespaces):
182
+ if elem.text:
183
+ collected_text.append(elem.text.strip())
33
184
 
34
- # Extract and parse data from each cell
35
- patient_id = parse_patient_id(cells[0].text.strip())
36
- diagnosis_code = parse_diagnosis_code(cells[1].text.strip())
37
- left_or_right_eye = parse_left_or_right_eye(cells[2].text.strip())
38
- femto_yes_or_no = parse_femto_yes_or_no(cells[3].text.strip())
185
+ for elem in root.iter():
186
+ if elem.tag.endswith('t') and elem.text:
187
+ collected_text.append(elem.text.strip())
188
+
189
+ combined_text = ' '.join(collected_text)
190
+ combined_text = reassemble_year(combined_text) # Fix OCR splitting years
191
+ # combined_text = re.sub(r'(\d{3}) (\d{1})', r'\1\2', combined_text) # initial year regex.
192
+ combined_text = normalize_text(combined_text) # Normalize abbreviations
193
+ combined_text = re.sub(r',', '', combined_text) # Remove commas if they exist
194
+
195
+ # Log the combined text
196
+ MediLink_ConfigLoader.log("Combined text: {}".format(combined_text), level="DEBUG")
197
+ # print("DEBUG: Combined text: {}".format(combined_text))
198
+
199
+ day_week_pattern = r"(MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|SUNDAY)"
200
+ month_day_pattern = r"(JANUARY|FEBRUARY|MARCH|APRIL|MAY|JUNE|JULY|AUGUST|SEPTEMBER|OCTOBER|NOVEMBER|DECEMBER) \d{1,2}"
201
+ year_pattern = r"\d{4}"
202
+
203
+ day_of_week = re.search(day_week_pattern, combined_text, re.IGNORECASE)
204
+ month_day = re.search(month_day_pattern, combined_text, re.IGNORECASE)
205
+ year_match = re.search(year_pattern, combined_text, re.IGNORECASE)
39
206
 
40
- # Construct the dictionary entry
41
- patient_data[patient_id] = [diagnosis_code, left_or_right_eye, femto_yes_or_no]
207
+ # Log the results of the regex searches
208
+ MediLink_ConfigLoader.log("Day of week found: {}".format(day_of_week.group() if day_of_week else 'None'), level="DEBUG")
209
+ MediLink_ConfigLoader.log("Month and day found: {}".format(month_day.group() if month_day else 'None'), level="DEBUG")
210
+ MediLink_ConfigLoader.log("Year found: {}".format(year_match.group() if year_match else 'None'), level="DEBUG")
211
+
212
+ if day_of_week and month_day and year_match:
213
+ date_str = "{} {} {}".format(day_of_week.group(), month_day.group(), year_match.group())
214
+ try:
215
+ date_obj = datetime.strptime(date_str, '%A %B %d %Y')
216
+ return date_obj.strftime('%m-%d-%Y')
217
+ except ValueError as e:
218
+ MediLink_ConfigLoader.log("Error converting date: {}. Error: {}".format(date_str, e), level="ERROR")
219
+ else:
220
+ MediLink_ConfigLoader.log("Date components not found or incomplete in the text. Combined text: {}, Day of week: {}, Month and day: {}, Year: {}"
221
+ .format(combined_text,
222
+ day_of_week.group() if day_of_week else 'None',
223
+ month_day.group() if month_day else 'None',
224
+ year_match.group() if year_match else 'None'),
225
+ level="WARNING")
226
+ except Exception as e:
227
+ MediLink_ConfigLoader.log("Error extracting date from file: {}. Error: {}".format(file_path, e))
228
+ print("Error extracting date from file: {}. Error: {}".format(file_path, e))
42
229
 
43
- return patient_data
230
+ return None
44
231
 
45
232
  def parse_patient_id(text):
46
- # Implement parsing logic for Patient ID Number
47
- # Example: Assume the ID is the first part of the text, separated by a space or newline
48
- return text.split()[0]
233
+ try:
234
+ return text.split()[0].lstrip('#') # Extract patient ID number (removing the '#')
235
+ except Exception as e:
236
+ MediLink_ConfigLoader.log("Error parsing patient ID: {}. Error: {}".format(text, e))
237
+ return None
49
238
 
50
239
  def parse_diagnosis_code(text):
51
- # Implement parsing logic for Diagnosis Code
52
- # Example: Extract the code from a known pattern or location in the text
53
- return text.split(':')[1].strip() if ':' in text else text
240
+ try:
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
+
254
+ except Exception as e:
255
+ MediLink_ConfigLoader.log("Error parsing diagnosis code: {}. Error: {}".format(text, e))
256
+ return "Unknown"
54
257
 
55
258
  def parse_left_or_right_eye(text):
56
- # Implement parsing logic for Left or Right Eye
57
- # Example: Assume the text contains 'Left' or 'Right' and extract it
58
- if 'Left' in text:
59
- return 'Left'
60
- elif 'Right' in text:
61
- return 'Right'
62
- else:
259
+ try:
260
+ if 'LEFT EYE' in text.upper():
261
+ return 'Left'
262
+ elif 'RIGHT EYE' in text.upper():
263
+ return 'Right'
264
+ else:
265
+ return 'Unknown'
266
+ except Exception as e:
267
+ MediLink_ConfigLoader.log("Error parsing left or right eye: {}. Error: {}".format(text, e))
63
268
  return 'Unknown'
64
269
 
65
270
  def parse_femto_yes_or_no(text):
66
- # Implement parsing logic for Femto yes or no
67
- # Example: Check for presence of keywords 'yes' or 'no'
68
- if 'yes' in text.lower():
69
- return 'Yes'
70
- elif 'no' in text.lower():
71
- return 'No'
72
- else:
73
- return 'Unknown'
271
+ try:
272
+ if 'FEMTO' in text.upper():
273
+ return True
274
+ else:
275
+ return False
276
+ except Exception as e:
277
+ MediLink_ConfigLoader.log("Error parsing femto yes or no: {}. Error: {}".format(text, e))
278
+ return False
279
+
280
+ def rotate_docx_files(directory):
281
+ # List all files in the directory
282
+ files = os.listdir(directory)
283
+
284
+ # Filter files that contain "DR" and "SS" in the filename
285
+ filtered_files = [file for file in files if "DR" in file and "SS" in file]
286
+
287
+ # Iterate through filtered files
288
+ for filename in filtered_files:
289
+ filepath = os.path.join(directory, filename)
290
+ # Parse each document and print the resulting dictionary
291
+ patient_data_dict = parse_docx(filepath)
292
+ print("Data from file '{}':".format(filename))
293
+ import pprint
294
+ pprint.pprint(patient_data_dict)
295
+ print()
74
296
 
75
- # Placeholder function call (replace 'path_to_docx' with the actual file path)
76
- filepath = 'path_to_docx'
77
- patient_data_dict = parse_docx(filepath)
297
+ def main():
298
+ # Call the function with the directory containing your .docx files
299
+ directory = "C:\\Users\\danie\\Downloads\\"
300
+ rotate_docx_files(directory)
78
301
 
79
- # Print the resulting dictionary
80
- print(patient_data_dict)
302
+ if __name__ == "__main__":
303
+ main()
MediLink/MediLink.py CHANGED
@@ -2,7 +2,7 @@ import os
2
2
  import MediLink_Down
3
3
  import MediLink_Up
4
4
  import MediLink_ConfigLoader
5
- import MediLink_837p_encoder
5
+ import MediLink_DataMgmt
6
6
 
7
7
  # For UI Functions
8
8
  import os
@@ -18,93 +18,54 @@ from MediBot import MediBot_Preprocessor_lib
18
18
  load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
19
19
  from MediBot import MediBot_Crosswalk_Library
20
20
 
21
- """
22
- Development Tasks for Backend Enhancement in MediSoft Claims Submittal (MediLink) Script:
23
-
24
- Implement dynamic configurations for multiple endpoints (Availity, Optum, PNT Data) with environmental settings support.
25
- Enhance file detection with detailed logging and introduce integrity checks for pre-processing validation.
26
- Verify file transmissions via WinSCP log analysis for successful endpoint acknowledgments and secure data transfer.
27
- Automate response file handling from endpoints and integrate feedback into MediSoft with exception alerts.
28
- De-persisting Intermediate Files.
29
- When transmissions fail, there is some retaining of patient data in memory or something that seems to default
30
- any new endpoint changes to Optum. May need to "de-confirm" patients, but leave the suggested endpoints as the previously
31
- confirmed endpoints. This should be similar logic to if the user made a mistake and wants to go back and fix it.
32
- These tasks involve backend enhancements such as dynamic configurations, file detection improvements, file transmission verification, automation of response file handling, and management of intermediate files and transmission failures.
33
-
34
- TODO (Low) Availity has a response file that says "File was received at TIME. File was sent for processing." as a confirmation
35
- that sits in the SendFiles folder after a submittal.
36
-
37
- TODO (Crosswalk) When an endpoint is updated in the UI, the crosswalk should also be updated and saved for that payer ID because that payer ID
38
- would basically forever need to be going to that endpoint for any patient. the suggested_endpoint should eventually be always correct.
39
-
40
- BUG Suggested Endpoint when you say 'n' to proceed with transmission is not getting updated with the endpoint
41
- that was selected previously by the user. However, when we go back to the confirmation list, we do have a persist of the assignment.
42
- This can be confusing for the user.
43
-
44
- MediLink
45
- | - import MediLink_Down
46
- | - import MediLink_ERA_decoder
47
- | | - from MediLink_ConfigLoader import load_configuration
48
- | | | - None
49
- | | - from MediLink_DataMgmt import consolidate_csvs
50
- | | | - from MediLink import MediLink_ConfigLoader
51
- | | - None
52
- | - from MediLink_DataMgmt import operate_winscp
53
- | - from MediLink import MediLink_ConfigLoader
54
- | - None
55
- | - import MediLink_Up
56
- | - None
57
- | - import MediLink_ConfigLoader
58
- | - None
59
- | - import MediLink_837p_encoder
60
- | - import MediLink_ConfigLoader
61
- | | - None
62
- | - from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
63
- | - from MediLink import MediLink_ConfigLoader
64
- | - None
65
- | - import MediLink_837p_encoder_library
66
- | - from MediLink import MediLink_ConfigLoader
67
- | - None
68
- | - import MediLink_UI
69
- | - None
70
- """
71
-
72
- 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):
73
26
  """
74
- Detects new files in the specified directory and prepares detailed patient data for processing,
75
- including suggestions for endpoints based on insurance provider information found in the config.
27
+ Collects detailed patient data from the selected files.
76
28
 
77
- :param directory_path: Path to the directory containing files to be detected.
29
+ :param selected_files: List of selected file paths.
78
30
  :param config: Configuration settings loaded from a JSON file.
79
- :return: A tuple containing a list of new file paths and the detailed patient data.
31
+ :param crosswalk: Crosswalk data for mapping purposes.
32
+ :return: A list of detailed patient data.
80
33
  """
81
- new_files = detect_new_files(directory_path)
82
- if not new_files:
83
- print(" No new claims detected. Check Medisoft claims output.\n")
84
- return False, []
85
-
86
- detailed_patient_data = [] # Initialize list for detailed patient data
87
- for file_path in new_files:
34
+ detailed_patient_data = []
35
+ for file_path in selected_files:
88
36
  detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
89
37
  detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
38
+
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)
90
44
 
91
- # Return just the list of new files and the enriched detailed patient data
92
- return new_files, detailed_patient_data
45
+ return detailed_patient_data
93
46
 
94
- def detect_new_files(directory_path, file_extension='.DAT'):
47
+ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
95
48
  """
96
- Scans the specified directory for new files with a given extension.
49
+ Enriches the detailed patient data with insurance type based on patient ID.
50
+
51
+ Parameters:
52
+ - detailed_patient_data: List of dictionaries containing detailed patient data.
53
+ - patient_insurance_mapping: Dictionary mapping patient IDs to their insurance types.
54
+
55
+ Returns:
56
+ - Enriched detailed patient data with insurance type added.
97
57
 
98
- :param directory_path: Path to the directory containing files to be detected.
99
- :param file_extension: Extension of the files to detect. Defaults to '.csv'.
100
- :return: A list of paths to new files detected in the directory.
58
+ TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
101
59
  """
102
- detected_file_paths = []
103
- for filename in os.listdir(directory_path):
104
- if filename.endswith(file_extension):
105
- file_path = os.path.join(directory_path, filename)
106
- detected_file_paths.append(file_path)
107
- return detected_file_paths
60
+ if patient_insurance_type_mapping is None:
61
+ MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.")
62
+ patient_insurance_type_mapping = {}
63
+
64
+ for data in detailed_patient_data:
65
+ patient_id = data.get('PATID') # I think this is the right name?
66
+ insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO)
67
+ data['insurance_type'] = insurance_type
68
+ return detailed_patient_data
108
69
 
109
70
  def extract_and_suggest_endpoint(file_path, config, crosswalk):
110
71
  """
@@ -127,8 +88,8 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
127
88
  insurance_to_id = load_insurance_data_from_mains(config)
128
89
  MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
129
90
 
130
- for personal_info, insurance_info, service_info in MediLink_837p_encoder.read_fixed_width_data(file_path):
131
- parsed_data = MediLink_837p_encoder.parse_fixed_width_data(personal_info, insurance_info, service_info, config.get('MediLink_Config', config))
91
+ for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
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))
132
93
 
133
94
  primary_insurance = parsed_data.get('INAME')
134
95
 
@@ -174,25 +135,6 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
174
135
  # Return only the enriched detailed patient data, eliminating the need for a separate summary list
175
136
  return detailed_patient_data
176
137
 
177
- def organize_patient_data_by_endpoint(detailed_patient_data):
178
- """
179
- Organizes detailed patient data by their confirmed endpoints.
180
- This simplifies processing and conversion per endpoint basis, ensuring that claims are generated and submitted
181
- according to the endpoint-specific requirements.
182
-
183
- :param detailed_patient_data: A list of dictionaries, each containing detailed patient data including confirmed endpoint.
184
- :return: A dictionary with endpoints as keys and lists of detailed patient data as values for processing.
185
- """
186
- organized = {}
187
- for data in detailed_patient_data:
188
- # Retrieve confirmed endpoint from each patient's data
189
- endpoint = data['confirmed_endpoint'] if 'confirmed_endpoint' in data else data['suggested_endpoint']
190
- # Initialize a list for the endpoint if it doesn't exist
191
- if endpoint not in organized:
192
- organized[endpoint] = []
193
- organized[endpoint].append(data)
194
- return organized
195
-
196
138
  def check_for_new_remittances(config):
197
139
  print("\nChecking for new files across all endpoints...")
198
140
  endpoints = config['MediLink_Config']['endpoints']
@@ -239,7 +181,7 @@ def user_decision_on_suggestions(detailed_patient_data, config):
239
181
 
240
182
  BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
241
183
  although the user decision is persisting. Possibly consider making the current/suggested/confirmed endpoint
242
- part of a class that the user can interact with via these menus. Probably better handling that way.
184
+ part of a class that the user can interact with via these menus? Probably better handling that way.
243
185
  """
244
186
  # Display summaries of patient details and endpoints.
245
187
  MediLink_UI.display_patient_summaries(detailed_patient_data)
@@ -249,20 +191,11 @@ def user_decision_on_suggestions(detailed_patient_data, config):
249
191
 
250
192
  # If the user agrees to proceed with all suggested endpoints, confirm them.
251
193
  if proceed:
252
- return confirm_all_suggested_endpoints(detailed_patient_data)
194
+ return MediLink_DataMgmt.confirm_all_suggested_endpoints(detailed_patient_data)
253
195
  # Otherwise, allow the user to adjust the endpoints manually.
254
196
  else:
255
197
  return select_and_adjust_files(detailed_patient_data, config)
256
-
257
- def confirm_all_suggested_endpoints(detailed_patient_data):
258
- """
259
- Confirms all suggested endpoints for each patient's detailed data.
260
- """
261
- for data in detailed_patient_data:
262
- if 'confirmed_endpoint' not in data:
263
- data['confirmed_endpoint'] = data['suggested_endpoint']
264
- return detailed_patient_data
265
-
198
+
266
199
  def select_and_adjust_files(detailed_patient_data, config):
267
200
  """
268
201
  Allows users to select patients and adjust their endpoints by interfacing with UI functions.
@@ -323,14 +256,14 @@ def main_menu():
323
256
  # Normalize the directory path for file operations.
324
257
  directory_path = os.path.normpath(config['MediLink_Config']['inputFilePath'])
325
258
 
326
- # Detect new files and collect detailed patient data if available.
327
- new_files, detailed_patient_data = detect_and_display_file_summaries(directory_path, config, crosswalk)
259
+ # Detect files and determine if a new file is flagged.
260
+ all_files, file_flagged = MediLink_DataMgmt.detect_new_files(directory_path)
328
261
 
329
262
  while True:
330
263
  # Define the menu options. Base options include checking remittances and exiting the program.
331
264
  options = ["Check for new remittances", "Exit"]
332
- # If new files are detected, add the option to submit claims.
333
- if new_files:
265
+ # If any files are detected, add the option to submit claims.
266
+ if all_files:
334
267
  options.insert(1, "Submit claims")
335
268
 
336
269
  # Display the dynamically adjusted menu options.
@@ -341,11 +274,22 @@ def main_menu():
341
274
  if choice == '1':
342
275
  # Handle remittance checking.
343
276
  check_for_new_remittances(config)
344
- elif choice == '2' and new_files:
345
- # Handle the claims submission flow if 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.
346
290
  handle_submission(detailed_patient_data, config)
347
- elif choice == '3' or (choice == '2' and not new_files):
348
- # Exit the program if the user chooses to exit or if no new files are present.
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.
349
293
  MediLink_UI.display_exit_message()
350
294
  break
351
295
  else:
@@ -357,13 +301,29 @@ def handle_submission(detailed_patient_data, config):
357
301
  Handles the submission process for claims based on detailed patient data.
358
302
  This function orchestrates the flow from user decision on endpoint suggestions to the actual submission of claims.
359
303
  """
304
+ # TODO If we get here via a user decline we end up not displaying the patient summary data, but this doesn't happen in the first round. Can be de-tangled later.
305
+
306
+ # Ask the user if they want to edit insurance types
307
+ edit_insurance = input("Do you want to edit insurance types? (y/n): ").strip().lower()
308
+ if edit_insurance in ['y', 'yes', '']:
309
+ while True:
310
+ # Bulk edit insurance types
311
+ MediLink_DataMgmt.bulk_edit_insurance_types(detailed_patient_data, insurance_options)
312
+
313
+ # Review and confirm changes
314
+ if MediLink_DataMgmt.review_and_confirm_changes(detailed_patient_data, insurance_options):
315
+ break # Exit the loop if changes are confirmed
316
+ else:
317
+ print("Returning to bulk edit insurance types.")
318
+
360
319
  # Initiate user interaction to confirm or adjust suggested endpoints.
361
320
  adjusted_data = user_decision_on_suggestions(detailed_patient_data, config)
321
+
362
322
  # Confirm all remaining suggested endpoints.
363
- confirmed_data = confirm_all_suggested_endpoints(adjusted_data)
323
+ confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
364
324
  if confirmed_data: # Proceed if there are confirmed data entries.
365
325
  # Organize data by confirmed endpoints for submission.
366
- organized_data = organize_patient_data_by_endpoint(confirmed_data)
326
+ organized_data = MediLink_DataMgmt.organize_patient_data_by_endpoint(confirmed_data)
367
327
  # Confirm transmission with the user and check for internet connectivity.
368
328
  if MediLink_Up.confirm_transmission(organized_data):
369
329
  if MediLink_Up.check_internet_connection():