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

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

Potentially problematic release.


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

@@ -1,80 +1,295 @@
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)
55
+
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
+ }
23
147
 
24
- # Initialize the dictionary to store data
25
- patient_data = {}
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)
26
152
 
27
- # Assuming the first table contains the required data
28
- table = doc.tables[0]
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)
29
160
 
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
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 = []
33
179
 
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())
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())
39
184
 
40
- # Construct the dictionary entry
41
- patient_data[patient_id] = [diagnosis_code, left_or_right_eye, femto_yes_or_no]
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)
206
+
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
+ 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]
245
+ except Exception as e:
246
+ MediLink_ConfigLoader.log("Error parsing diagnosis code: {}. Error: {}".format(text, e))
247
+ return "Unknown"
54
248
 
55
249
  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:
250
+ try:
251
+ if 'LEFT EYE' in text.upper():
252
+ return 'Left'
253
+ elif 'RIGHT EYE' in text.upper():
254
+ return 'Right'
255
+ else:
256
+ return 'Unknown'
257
+ except Exception as e:
258
+ MediLink_ConfigLoader.log("Error parsing left or right eye: {}. Error: {}".format(text, e))
63
259
  return 'Unknown'
64
260
 
65
261
  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'
262
+ try:
263
+ if 'FEMTO' in text.upper():
264
+ return True
265
+ else:
266
+ return False
267
+ except Exception as e:
268
+ MediLink_ConfigLoader.log("Error parsing femto yes or no: {}. Error: {}".format(text, e))
269
+ return False
270
+
271
+
272
+ def rotate_docx_files(directory):
273
+ # List all files in the directory
274
+ files = os.listdir(directory)
275
+
276
+ # Filter files that contain "DR" and "SS" in the filename
277
+ filtered_files = [file for file in files if "DR" in file and "SS" in file]
278
+
279
+ # Iterate through filtered files
280
+ for filename in filtered_files:
281
+ filepath = os.path.join(directory, filename)
282
+ # Parse each document and print the resulting dictionary
283
+ patient_data_dict = parse_docx(filepath)
284
+ print("Data from file '{}':".format(filename))
285
+ import pprint
286
+ pprint.pprint(patient_data_dict)
287
+ print()
74
288
 
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)
289
+ def main():
290
+ # Call the function with the directory containing your .docx files
291
+ directory = "C:\\Users\\danie\\Downloads\\"
292
+ rotate_docx_files(directory)
78
293
 
79
- # Print the resulting dictionary
80
- print(patient_data_dict)
294
+ if __name__ == "__main__":
295
+ main()
MediLink/MediLink.py CHANGED
@@ -3,6 +3,7 @@ import MediLink_Down
3
3
  import MediLink_Up
4
4
  import MediLink_ConfigLoader
5
5
  import MediLink_837p_encoder
6
+ import MediLink_DataMgmt
6
7
 
7
8
  # For UI Functions
8
9
  import os
@@ -18,56 +19,33 @@ from MediBot import MediBot_Preprocessor_lib
18
19
  load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
19
20
  from MediBot import MediBot_Crosswalk_Library
20
21
 
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
- """
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
+ }
71
49
 
72
50
  def detect_and_display_file_summaries(directory_path, config, crosswalk):
73
51
  """
@@ -76,35 +54,61 @@ def detect_and_display_file_summaries(directory_path, config, crosswalk):
76
54
 
77
55
  :param directory_path: Path to the directory containing files to be detected.
78
56
  :param config: Configuration settings loaded from a JSON file.
57
+ :param crosswalk: Crosswalk data for mapping purposes.
79
58
  :return: A tuple containing a list of new file paths and the detailed patient data.
80
59
  """
81
- new_files = detect_new_files(directory_path)
82
- if not new_files:
83
- print(" No new claims detected. Check Medisoft claims output.\n")
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)
80
+
81
+ # Display summaries and provide an option for bulk edit
82
+ MediLink_UI.display_patient_summaries(detailed_patient_data)
83
+
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))
84
88
  return False, []
85
89
 
86
- detailed_patient_data = [] # Initialize list for detailed patient data
87
- for file_path in new_files:
88
- detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
89
- detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
90
+ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
91
+ """
92
+ Enriches the detailed patient data with insurance type based on patient ID.
90
93
 
91
- # Return just the list of new files and the enriched detailed patient data
92
- return new_files, detailed_patient_data
94
+ Parameters:
95
+ - detailed_patient_data: List of dictionaries containing detailed patient data.
96
+ - patient_insurance_mapping: Dictionary mapping patient IDs to their insurance types.
93
97
 
94
- def detect_new_files(directory_path, file_extension='.DAT'):
95
- """
96
- Scans the specified directory for new files with a given extension.
98
+ Returns:
99
+ - Enriched detailed patient data with insurance type added.
97
100
 
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.
101
+ TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
101
102
  """
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
103
+ if patient_insurance_type_mapping is None:
104
+ MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.")
105
+ patient_insurance_type_mapping = {}
106
+
107
+ for data in detailed_patient_data:
108
+ patient_id = data.get('PATID') # I think this is the right name?
109
+ insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO)
110
+ data['insurance_type'] = insurance_type
111
+ return detailed_patient_data
108
112
 
109
113
  def extract_and_suggest_endpoint(file_path, config, crosswalk):
110
114
  """
@@ -127,8 +131,8 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
127
131
  insurance_to_id = load_insurance_data_from_mains(config)
128
132
  MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
129
133
 
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))
134
+ for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
135
+ 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
136
 
133
137
  primary_insurance = parsed_data.get('INAME')
134
138
 
@@ -174,25 +178,6 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
174
178
  # Return only the enriched detailed patient data, eliminating the need for a separate summary list
175
179
  return detailed_patient_data
176
180
 
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
181
  def check_for_new_remittances(config):
197
182
  print("\nChecking for new files across all endpoints...")
198
183
  endpoints = config['MediLink_Config']['endpoints']
@@ -239,7 +224,7 @@ def user_decision_on_suggestions(detailed_patient_data, config):
239
224
 
240
225
  BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
241
226
  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.
227
+ part of a class that the user can interact with via these menus? Probably better handling that way.
243
228
  """
244
229
  # Display summaries of patient details and endpoints.
245
230
  MediLink_UI.display_patient_summaries(detailed_patient_data)
@@ -249,20 +234,11 @@ def user_decision_on_suggestions(detailed_patient_data, config):
249
234
 
250
235
  # If the user agrees to proceed with all suggested endpoints, confirm them.
251
236
  if proceed:
252
- return confirm_all_suggested_endpoints(detailed_patient_data)
237
+ return MediLink_DataMgmt.confirm_all_suggested_endpoints(detailed_patient_data)
253
238
  # Otherwise, allow the user to adjust the endpoints manually.
254
239
  else:
255
240
  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
-
241
+
266
242
  def select_and_adjust_files(detailed_patient_data, config):
267
243
  """
268
244
  Allows users to select patients and adjust their endpoints by interfacing with UI functions.
@@ -325,6 +301,9 @@ def main_menu():
325
301
 
326
302
  # Detect new files and collect detailed patient data if available.
327
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)
328
307
 
329
308
  while True:
330
309
  # Define the menu options. Base options include checking remittances and exiting the program.
@@ -343,7 +322,7 @@ def main_menu():
343
322
  check_for_new_remittances(config)
344
323
  elif choice == '2' and new_files:
345
324
  # Handle the claims submission flow if new files are present.
346
- handle_submission(detailed_patient_data, config)
325
+ handle_submission(detailed_patient_data, config) # Since we have the bulk edit, we should actually go there first
347
326
  elif choice == '3' or (choice == '2' and not new_files):
348
327
  # Exit the program if the user chooses to exit or if no new files are present.
349
328
  MediLink_UI.display_exit_message()
@@ -357,13 +336,29 @@ def handle_submission(detailed_patient_data, config):
357
336
  Handles the submission process for claims based on detailed patient data.
358
337
  This function orchestrates the flow from user decision on endpoint suggestions to the actual submission of claims.
359
338
  """
339
+ # 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.
340
+
341
+ # Ask the user if they want to edit insurance types
342
+ edit_insurance = input("Do you want to edit insurance types? (y/n): ").strip().lower()
343
+ if edit_insurance in ['y', 'yes', '']:
344
+ while True:
345
+ # Bulk edit insurance types
346
+ MediLink_DataMgmt.bulk_edit_insurance_types(detailed_patient_data, insurance_options)
347
+
348
+ # Review and confirm changes
349
+ if MediLink_DataMgmt.review_and_confirm_changes(detailed_patient_data, insurance_options):
350
+ break # Exit the loop if changes are confirmed
351
+ else:
352
+ print("Returning to bulk edit insurance types.")
353
+
360
354
  # Initiate user interaction to confirm or adjust suggested endpoints.
361
355
  adjusted_data = user_decision_on_suggestions(detailed_patient_data, config)
356
+
362
357
  # Confirm all remaining suggested endpoints.
363
- confirmed_data = confirm_all_suggested_endpoints(adjusted_data)
358
+ confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
364
359
  if confirmed_data: # Proceed if there are confirmed data entries.
365
360
  # Organize data by confirmed endpoints for submission.
366
- organized_data = organize_patient_data_by_endpoint(confirmed_data)
361
+ organized_data = MediLink_DataMgmt.organize_patient_data_by_endpoint(confirmed_data)
367
362
  # Confirm transmission with the user and check for internet connectivity.
368
363
  if MediLink_Up.confirm_transmission(organized_data):
369
364
  if MediLink_Up.check_internet_connection():