medicafe 0.240809.0__py3-none-any.whl → 0.241015.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,20 +1,18 @@
1
+ #MediBot_Preprocessor_lib.py
1
2
  from collections import OrderedDict, defaultdict
2
- from datetime import datetime
3
- import os
4
- import csv
5
- import sys
3
+ from datetime import datetime, timedelta
4
+ import os, csv, sys
6
5
 
7
- # Add parent directory of the project to the Python path
8
- project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
9
- sys.path.append(project_dir)
6
+ # Add the parent directory of the project to the Python path
7
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
8
 
9
+ # Attempt to import necessary modules, falling back if they are not found
11
10
  try:
12
11
  import MediLink_ConfigLoader
13
12
  import MediLink_DataMgmt
14
13
  except ImportError:
15
- from MediLink import MediLink_ConfigLoader
16
- from MediLink import MediLink_DataMgmt
17
-
14
+ from MediLink import MediLink_ConfigLoader, MediLink_DataMgmt
15
+
18
16
  try:
19
17
  from MediBot_UI import app_control
20
18
  from MediBot_docx_decoder import parse_docx
@@ -32,26 +30,18 @@ class InitializationError(Exception):
32
30
  def initialize(config):
33
31
  global AHK_EXECUTABLE, CSV_FILE_PATH, field_mapping, page_end_markers
34
32
 
35
- try:
36
- AHK_EXECUTABLE = config.get('AHK_EXECUTABLE', "")
37
- except AttributeError:
38
- raise InitializationError("Error: 'AHK_EXECUTABLE' not found in config.")
39
-
40
- try:
41
- CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
42
- except AttributeError:
43
- raise InitializationError("Error: 'CSV_FILE_PATH' not found in config.")
44
-
45
- try:
46
- field_mapping = OrderedDict(config.get('field_mapping', {}))
47
- except AttributeError:
48
- raise InitializationError("Error: 'field_mapping' not found in config.")
33
+ required_keys = {
34
+ 'AHK_EXECUTABLE': "",
35
+ 'CSV_FILE_PATH': "",
36
+ 'field_mapping': {},
37
+ 'page_end_markers': []
38
+ }
49
39
 
50
- try:
51
- page_end_markers = config.get('page_end_markers', [])
52
- except AttributeError:
53
- raise InitializationError("Error: 'page_end_markers' not found in config.")
54
-
40
+ for key, default in required_keys.items():
41
+ try:
42
+ globals()[key] = config.get(key, default) if key != 'field_mapping' else OrderedDict(config.get(key, default))
43
+ except AttributeError:
44
+ raise InitializationError("Error: '{}' not found in config.".format(key))
55
45
 
56
46
  def open_csv_for_editing(csv_file_path):
57
47
  try:
@@ -103,117 +93,237 @@ def add_columns(csv_data, column_headers):
103
93
 
104
94
  # Extracting the list to a variable for future refactoring:
105
95
  def filter_rows(csv_data):
106
- # TODO This should go to the crosswalk.
107
- excluded_insurance = ['AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO']
108
- csv_data[:] = [row for row in csv_data if row.get('Patient ID', '').strip()]
109
- csv_data[:] = [row for row in csv_data if row.get('Primary Insurance', '').strip() not in excluded_insurance]
96
+ # TODO: This should be handled in the crosswalk.
97
+ excluded_insurance = {'AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO'}
98
+ csv_data[:] = [row for row in csv_data if row.get('Patient ID') and row.get('Primary Insurance') not in excluded_insurance]
110
99
 
111
100
  def convert_surgery_date(csv_data):
112
101
  for row in csv_data:
113
- try:
114
- row['Surgery Date'] = datetime.strptime(row.get('Surgery Date', ''), '%m/%d/%Y')
115
- except ValueError:
116
- row['Surgery Date'] = datetime.min # Assign a minimum datetime value for sorting purposes
102
+ surgery_date_str = row.get('Surgery Date', '')
103
+ row['Surgery Date'] = (datetime.strptime(surgery_date_str, '%m/%d/%Y')
104
+ if surgery_date_str else datetime.min) # Assign a minimum datetime value if empty
117
105
 
118
106
  def sort_and_deduplicate(csv_data):
119
- # TODO we need to figure out a new logic here for doing second-eye charges. I don't know what the flow should be yet.
120
- csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
107
+ # Create a dictionary to hold unique patients based on Patient ID
121
108
  unique_patients = {}
109
+
110
+ # Iterate through the CSV data and populate the unique_patients dictionary
122
111
  for row in csv_data:
123
112
  patient_id = row.get('Patient ID')
124
- if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
113
+ if patient_id not in unique_patients:
125
114
  unique_patients[patient_id] = row
126
- csv_data[:] = list(unique_patients.values())
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.
129
- # If we don't have that surgery schedule yet for some reason, we should default to the current ordering strategy.
130
- csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
115
+ else:
116
+ # If the patient ID already exists, compare surgery dates
117
+ existing_row = unique_patients[patient_id]
118
+ if row['Surgery Date'] < existing_row['Surgery Date']:
119
+ unique_patients[patient_id] = row
120
+
121
+ # Convert the unique_patients dictionary back to a list and sort it
122
+ csv_data[:] = sorted(unique_patients.values(), key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip())) # TODO Does this need to be sorted twice? once before and once after?
123
+
124
+ # TODO: Consider adding an option in the config to sort based on Surgery Schedules when available.
125
+ # If no schedule is available, the current sorting strategy will be used.
131
126
 
132
127
  def combine_fields(csv_data):
133
128
  for row in csv_data:
134
- row['Surgery Date'] = row['Surgery Date'].strftime('%m/%d/%Y')
129
+ # Safely handle the 'Surgery Date' conversion
130
+ surgery_date = row.get('Surgery Date')
131
+ row['Surgery Date'] = surgery_date.strftime('%m/%d/%Y') if surgery_date else ''
132
+
135
133
  first_name = row.get('Patient First', '').strip()
136
134
  middle_name = row.get('Patient Middle', '').strip()
137
- if len(middle_name) > 1:
138
- middle_name = middle_name[0] # Take only the first character
135
+ middle_name = middle_name[0] if len(middle_name) > 1 else '' # Take only the first character or empty
139
136
  last_name = row.get('Patient Last', '').strip()
140
- row['Patient Name'] = "{}, {} {}".format(last_name, first_name, middle_name).strip()
137
+ row['Patient Name'] = ', '.join(filter(None, [last_name, first_name, middle_name])) # Join non-empty parts
138
+
141
139
  address1 = row.get('Patient Address1', '').strip()
142
140
  address2 = row.get('Patient Address2', '').strip()
143
- row['Patient Street'] = "{} {}".format(address1, address2).strip()
141
+ row['Patient Street'] = ' '.join(filter(None, [address1, address2])) # Join non-empty addresses
144
142
 
145
143
  def apply_replacements(csv_data, crosswalk):
146
144
  replacements = crosswalk.get('csv_replacements', {})
147
145
  for row in csv_data:
148
146
  for old_value, new_value in replacements.items():
149
- if row.get('Patient SSN', '') == old_value:
150
- row['Patient SSN'] = new_value
151
- elif row.get('Primary Insurance', '') == old_value:
152
- row['Primary Insurance'] = new_value
153
- elif row.get('Ins1 Payer ID') == old_value:
154
- row['Ins1 Payer ID'] = new_value
155
-
156
- def update_insurance_ids(csv_data, crosswalk):
147
+ for key in ['Patient SSN', 'Primary Insurance', 'Ins1 Payer ID']:
148
+ if row.get(key) == old_value:
149
+ row[key] = new_value
150
+ break # Exit the loop once a replacement is made
151
+
152
+ import difflib
153
+ from collections import defaultdict
154
+
155
+ def find_best_medisoft_id(insurance_name, medisoft_ids, medisoft_to_mains_names):
156
+ """
157
+ Finds the best matching Medisoft ID for a given insurance name using fuzzy matching.
158
+
159
+ Parameters:
160
+ - insurance_name (str): The insurance name from the CSV row.
161
+ - medisoft_ids (list): List of Medisoft IDs associated with the Payer ID.
162
+ - medisoft_to_mains_names (dict): Mapping from Medisoft ID to list of MAINS names.
163
+
164
+ Returns:
165
+ - int or None: The best matching Medisoft ID or None if no match is found.
166
+ """
167
+ best_match_ratio = 0
168
+ best_medisoft_id = None
169
+
170
+ for medisoft_id in medisoft_ids:
171
+ mains_names = medisoft_to_mains_names.get(medisoft_id, [])
172
+ for mains_name in mains_names:
173
+ # Preprocess names by extracting non-numeric characters and converting to uppercase
174
+ processed_mains = ''.join(filter(lambda x: not x.isdigit(), mains_name)).upper()
175
+ processed_insurance = ''.join(filter(lambda x: not x.isdigit(), insurance_name)).upper()
176
+
177
+ # Log the processed names before computing the match ratio
178
+ MediLink_ConfigLoader.log("Processing Medisoft ID '{}': Comparing processed insurance '{}' with processed mains '{}'.".format(medisoft_id, processed_insurance, processed_mains), level="DEBUG")
179
+
180
+ # Compute the similarity ratio
181
+ match_ratio = difflib.SequenceMatcher(None, processed_insurance, processed_mains).ratio()
182
+
183
+ # Log the match ratio
184
+ MediLink_ConfigLoader.log("Match ratio for Medisoft ID '{}': {:.2f}".format(medisoft_id, match_ratio), level="DEBUG")
185
+
186
+ if match_ratio > best_match_ratio:
187
+ best_match_ratio = match_ratio
188
+ best_medisoft_id = medisoft_id
189
+ # Log the current best match
190
+ MediLink_ConfigLoader.log("New best match found: Medisoft ID '{}' with match ratio {:.2f}".format(best_medisoft_id, best_match_ratio), level="DEBUG")
191
+
192
+ # Log the final best match ratio and ID
193
+ MediLink_ConfigLoader.log("Final best match ratio: {:.2f} for Medisoft ID '{}'".format(best_match_ratio, best_medisoft_id), level="DEBUG")
194
+
195
+ # No threshold applied, return the best match found
196
+ return best_medisoft_id
197
+
198
+ def NEW_update_insurance_ids(csv_data, config, crosswalk):
199
+ """
200
+ Updates the 'Ins1 Insurance ID' field in each row of csv_data based on the crosswalk and MAINS data.
201
+
202
+ Parameters:
203
+ - csv_data (list of dict): The CSV data where each row is represented as a dictionary.
204
+ - config (dict): Configuration object containing necessary paths and parameters.
205
+ - crosswalk (dict): Crosswalk data containing mappings between Payer IDs and Medisoft IDs.
206
+
207
+ Returns:
208
+ - None: The function modifies the csv_data in place.
209
+ """
210
+ processed_payer_ids = set() # Track processed Payer IDs
211
+ MediLink_ConfigLoader.log("Starting update of insurance IDs.", level="INFO")
212
+
213
+ # Load MAINS data to get mapping from Medisoft ID to MAINS names
214
+ insurance_to_id = load_insurance_data_from_mains(config) # Assuming it returns a dict mapping insurance names to IDs
215
+ MediLink_ConfigLoader.log("Loaded MAINS data for insurance to ID mapping.", level="DEBUG")
216
+
217
+ # Invert the mapping to get Medisoft ID to MAINS names
218
+ medisoft_to_mains_names = defaultdict(list)
219
+ for insurance_name, medisoft_id in insurance_to_id.items():
220
+ medisoft_to_mains_names[medisoft_id].append(insurance_name)
221
+
157
222
  for row in csv_data:
158
223
  ins1_payer_id = row.get('Ins1 Payer ID', '').strip()
159
- # MediLink_ConfigLoader.log("Ins1 Payer ID '{}' associated with Patient ID {}.".format(ins1_payer_id, row.get('Patient ID', "None")))
224
+ MediLink_ConfigLoader.log("Processing row with Ins1 Payer ID: '{}'.".format(ins1_payer_id), level="DEBUG")
225
+
160
226
  if ins1_payer_id:
161
- if ins1_payer_id in crosswalk.get('payer_id', {}):
162
- medisoft_ids = crosswalk['payer_id'][ins1_payer_id].get('medisoft_id', [])
163
- if medisoft_ids:
164
- medisoft_ids = [int(id) for id in medisoft_ids]
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
-
175
- row['Ins1 Insurance ID'] = medisoft_ids[0]
176
- # MediLink_ConfigLoader.log("Ins1 Insurance ID '{}' used for Payer ID {} in crosswalk.".format(row.get('Ins1 Insurance ID', ''), ins1_payer_id))
177
- else:
178
- MediLink_ConfigLoader.log("Ins1 Payer ID '{}' not found in the crosswalk.".format(ins1_payer_id))
179
- # Create a placeholder entry in the crosswalk, need to consider the medisoft_medicare_id handling later.
180
- if 'payer_id' not in crosswalk:
181
- crosswalk['payer_id'] = {}
182
- crosswalk['payer_id'][ins1_payer_id] = {
183
- 'medisoft_id': [],
184
- 'medisoft_medicare_id': [],
185
- 'endpoint': 'OPTUMEDI' # Default probably should be a flag for the crosswalk update function to deal with. BUG HARDCODE THERE ARE 3 of these defaults
186
- }
187
-
188
- def update_procedure_codes(csv_data):
189
- # Define the Medisoft shorthand to diagnostic codes dictionary
190
- # TODO The reverse of this will be in the crosswalk. We'll need to reverse it here for lookup.
191
- medisoft_to_diagnosis = {
192
- "25811": "H25.811",
193
- "25812": "H25.812",
194
- "2512": "H25.12",
195
- "2511": "H25.11",
196
- "529XA": "T85.29XA",
197
- "4301": "H43.01",
198
- "4302": "H43.02",
199
- "011X2": "H40.11X2",
200
- "051X3": "H40.51X3",
201
- "5398A": "T85.398A"
202
- }
227
+ # Mark this Payer ID as processed
228
+ if ins1_payer_id not in processed_payer_ids:
229
+ processed_payer_ids.add(ins1_payer_id) # Add to set
230
+ MediLink_ConfigLoader.log("Marked Payer ID '{}' as processed.".format(ins1_payer_id), level="DEBUG")
231
+
232
+ # Retrieve Medisoft IDs for the current Payer ID
233
+ medisoft_ids = crosswalk.get('payer_id', {}).get(ins1_payer_id, {}).get('medisoft_id', [])
234
+ MediLink_ConfigLoader.log("Retrieved Medisoft IDs for Payer ID '{}': {}".format(ins1_payer_id, medisoft_ids), level="DEBUG")
235
+
236
+ if not medisoft_ids:
237
+ MediLink_ConfigLoader.log("No Medisoft IDs available for Payer ID '{}', creating placeholder entry.".format(ins1_payer_id), level="WARNING")
238
+ # Create a placeholder entry in the crosswalk
239
+ if 'payer_id' not in crosswalk:
240
+ crosswalk['payer_id'] = {}
241
+ crosswalk['payer_id'][ins1_payer_id] = {
242
+ 'medisoft_id': [], # Placeholder for future Medisoft IDs
243
+ 'medisoft_medicare_id': [], # Placeholder for future Medicare IDs
244
+ 'endpoint': None # Placeholder for future endpoint
245
+ }
246
+ continue # Skip further processing for this Payer ID
247
+
248
+ # If only one Medisoft ID is associated, assign it directly
249
+ if len(medisoft_ids) == 1:
250
+ try:
251
+ medisoft_id = int(medisoft_ids[0])
252
+ row['Ins1 Insurance ID'] = medisoft_id
253
+ MediLink_ConfigLoader.log("Assigned Medisoft ID '{}' to row number {} with Payer ID '{}'.".format(medisoft_id, csv_data.index(row) + 1, ins1_payer_id), level="DEBUG")
254
+ except ValueError as e:
255
+ MediLink_ConfigLoader.log("Error converting Medisoft ID '{}' to integer for Payer ID '{}': {}".format(medisoft_ids[0], ins1_payer_id, e), level="ERROR")
256
+ row['Ins1 Insurance ID'] = None
257
+ continue # Move to the next row
258
+
259
+ # If multiple Medisoft IDs are associated, perform fuzzy matching
260
+ insurance_name = row.get('Primary Insurance', '').strip()
261
+ if not insurance_name:
262
+ MediLink_ConfigLoader.log("Row with Payer ID '{}' missing 'Primary Insurance', skipping assignment.".format(ins1_payer_id), level="WARNING")
263
+ continue # Skip if insurance name is missing
264
+
265
+ best_medisoft_id = find_best_medisoft_id(insurance_name, medisoft_ids, medisoft_to_mains_names)
266
+
267
+ if best_medisoft_id:
268
+ row['Ins1 Insurance ID'] = best_medisoft_id
269
+ MediLink_ConfigLoader.log("Assigned Medisoft ID '{}' to row with Payer ID '{}' based on fuzzy match.".format(best_medisoft_id, ins1_payer_id), level="INFO")
270
+ else:
271
+ # Default to the first Medisoft ID if no good match is found
272
+ try:
273
+ default_medisoft_id = int(medisoft_ids[0])
274
+ row['Ins1 Insurance ID'] = default_medisoft_id
275
+ MediLink_ConfigLoader.log("No suitable match found. Defaulted to Medisoft ID '{}' for Payer ID '{}'.".format(default_medisoft_id, ins1_payer_id), level="INFO")
276
+ except ValueError as e:
277
+ MediLink_ConfigLoader.log("Error converting default Medisoft ID '{}' to integer for Payer ID '{}': {}".format(medisoft_ids[0], ins1_payer_id, e), level="ERROR")
278
+ row['Ins1 Insurance ID'] = None
279
+
280
+ def update_insurance_ids(csv_data, config, crosswalk):
281
+ MediLink_ConfigLoader.log("Starting update_insurance_ids function.", level="DEBUG")
282
+
283
+ # Create a dictionary to hold Medisoft IDs for each payer ID in the crosswalk
284
+ payer_id_to_medisoft = {}
285
+ MediLink_ConfigLoader.log("Initialized payer_id_to_medisoft dictionary.", level="DEBUG")
286
+
287
+ # Populate the dictionary with data from the crosswalk
288
+ for payer_id, details in crosswalk.get('payer_id', {}).items():
289
+ medisoft_ids = details.get('medisoft_id', [])
290
+ # Filter out empty strings and take the first valid ID
291
+ medisoft_ids = [id for id in medisoft_ids if id]
292
+ payer_id_to_medisoft[payer_id] = int(medisoft_ids[0]) if medisoft_ids else None
293
+ MediLink_ConfigLoader.log("Processed Payer ID '{}': Medisoft IDs found: {}".format(payer_id, medisoft_ids), level="DEBUG")
294
+
295
+ # Process the csv_data
296
+ for row in csv_data:
297
+ ins1_payer_id = row.get('Ins1 Payer ID', '').strip()
298
+ MediLink_ConfigLoader.log("Processing row #{} with Ins1 Payer ID '{}'.".format(csv_data.index(row) + 1, ins1_payer_id), level="DEBUG")
299
+
300
+ if ins1_payer_id not in payer_id_to_medisoft:
301
+ # Add placeholder entry for new payer ID
302
+ payer_id_to_medisoft[ins1_payer_id] = None # No Medisoft ID available
303
+ crosswalk.setdefault('payer_id', {})[ins1_payer_id] = {
304
+ 'medisoft_id': [], # Placeholder for future Medisoft IDs
305
+ 'medisoft_medicare_id': [], # Placeholder for future Medicare IDs
306
+ 'endpoint': None # Placeholder for future endpoint
307
+ }
308
+ MediLink_ConfigLoader.log("Added placeholder entry for new Payer ID '{}'.".format(ins1_payer_id), level="INFO")
309
+
310
+ # Assign the Medisoft ID to the row
311
+ row['Ins1 Insurance ID'] = payer_id_to_medisoft[ins1_payer_id]
312
+ MediLink_ConfigLoader.log("Assigned Medisoft ID '{}' to row with Ins1 Payer ID '{}'.".format(row['Ins1 Insurance ID'], ins1_payer_id), level="DEBUG")
203
313
 
204
- # Define the procedure codes to diagnostic codes dictionary
205
- procedure_to_diagnosis = {
206
- "00142": ["H25.811", "H25.812", "H25.12", "H25.11", "T85.29XA"],
207
- "00145": ["H43.01", "H43.02"],
208
- "00140": ["H40.11X2", "H40.51X3"]
314
+ def update_procedure_codes(csv_data, crosswalk):
315
+
316
+ # Get Medisoft shorthand dictionary from crosswalk and reverse it
317
+ diagnosis_to_medisoft = crosswalk.get('diagnosis_to_medisoft', {}) # BUG We need to be careful here in case we decide we need to change the crosswalk data specifically with regard to the T8/H usage.
318
+ medisoft_to_diagnosis = {v: k for k, v in diagnosis_to_medisoft.items()}
319
+
320
+ # Get procedure code to diagnosis dictionary from crosswalk and reverse it for easier lookup
321
+ diagnosis_to_procedure = {
322
+ diagnosis_code: procedure_code
323
+ for procedure_code, diagnosis_codes in crosswalk.get('procedure_to_diagnosis', {}).items()
324
+ for diagnosis_code in diagnosis_codes
209
325
  }
210
326
 
211
- # Reverse the dictionary for easier lookup from diagnostic code to procedure code
212
- diagnosis_to_procedure = {}
213
- for procedure_code, diagnosis_codes in procedure_to_diagnosis.items():
214
- for diagnosis_code in diagnosis_codes:
215
- diagnosis_to_procedure[diagnosis_code] = procedure_code
216
-
217
327
  # Initialize counter for updated rows
218
328
  updated_count = 0
219
329
 
@@ -242,7 +352,7 @@ def update_procedure_codes(csv_data):
242
352
  def update_diagnosis_codes(csv_data):
243
353
  try:
244
354
  # Load configuration and crosswalk
245
- config, _ = MediLink_ConfigLoader.load_configuration()
355
+ config, crosswalk = MediLink_ConfigLoader.load_configuration()
246
356
 
247
357
  # Extract the local storage path from the configuration
248
358
  local_storage_path = config['MediLink_Config']['local_storage_path']
@@ -250,48 +360,80 @@ def update_diagnosis_codes(csv_data):
250
360
  # Initialize a dictionary to hold diagnosis codes from all DOCX files
251
361
  all_patient_data = {}
252
362
 
253
- # Iterate through all files in the specified directory
254
- for filename in os.listdir(local_storage_path):
255
- if filename.endswith(".docx"):
256
- filepath = os.path.join(local_storage_path, filename)
257
- MediLink_ConfigLoader.log("Processing DOCX file: {}".format(filepath), level="INFO")
258
- try:
259
- patient_data = parse_docx(filepath)
260
- for patient_id, service_dates in patient_data.items():
261
- if patient_id not in all_patient_data:
262
- all_patient_data[patient_id] = {}
263
- for date_of_service, diagnosis_data in service_dates.items():
264
- all_patient_data[patient_id][date_of_service] = diagnosis_data
265
- except Exception as e:
266
- MediLink_ConfigLoader.log("Error parsing DOCX file {}: {}".format(filepath, e), level="ERROR")
363
+ # Convert surgery dates in CSV data
364
+ convert_surgery_date(csv_data)
365
+
366
+ # Extract all valid surgery dates from csv_data
367
+ surgery_dates = [row['Surgery Date'] for row in csv_data if row['Surgery Date'] != datetime.min]
368
+
369
+ if not surgery_dates:
370
+ raise ValueError("No valid surgery dates found in csv_data.")
371
+
372
+ # Determine the minimum and maximum surgery dates
373
+ min_surgery_date = min(surgery_dates)
374
+ max_surgery_date = max(surgery_dates)
375
+
376
+ # Apply a ±5-day margin to the surgery dates
377
+ margin = timedelta(days=5)
378
+ threshold_start = min_surgery_date - margin
379
+ threshold_end = max_surgery_date + margin
380
+
381
+ MediLink_ConfigLoader.log("Processing DOCX files modified between {} and {}.".format(threshold_start, threshold_end), level="INFO")
382
+
383
+ # Gather all relevant DOCX files in the specified directory
384
+ docx_files = [
385
+ os.path.join(local_storage_path, filename)
386
+ for filename in os.listdir(local_storage_path)
387
+ if filename.endswith(".docx")
388
+ ]
389
+
390
+ # Filter files based on modification time
391
+ valid_files = [
392
+ filepath for filepath in docx_files
393
+ if threshold_start <= datetime.fromtimestamp(os.path.getmtime(filepath)) <= threshold_end
394
+ ]
395
+
396
+ # Process valid DOCX files
397
+ for filepath in valid_files:
398
+ MediLink_ConfigLoader.log("Processing DOCX file: {}".format(filepath), level="INFO")
399
+ try:
400
+ patient_data = parse_docx(filepath, surgery_dates) # Pass surgery_dates to parse_docx
401
+ for patient_id, service_dates in patient_data.items():
402
+ if patient_id not in all_patient_data:
403
+ all_patient_data[patient_id] = {}
404
+ for date_of_service, diagnosis_data in service_dates.items():
405
+ all_patient_data[patient_id][date_of_service] = diagnosis_data
406
+ except Exception as e:
407
+ MediLink_ConfigLoader.log("Error parsing DOCX file {}: {}".format(filepath, e), level="ERROR")
408
+
409
+ # Log if no valid files were found
410
+ if not valid_files:
411
+ MediLink_ConfigLoader.log("No valid DOCX files found within the modification time threshold.", level="INFO")
267
412
 
268
413
  # Debug logging for all_patient_data
269
- MediLink_ConfigLoader.log("All patient data collected from DOCX files: {}".format(all_patient_data), level="INFO")
270
-
271
- # Define the diagnosis to Medisoft shorthand dictionary
272
- diagnosis_to_medisoft = {
273
- "H25.811": "25811",
274
- "H25.812": "25812",
275
- "H25.12": "2512",
276
- "H25.11": "2511",
277
- "T85.29XA": "529XA",
278
- "H43.01": "4301",
279
- "H43.02": "4302",
280
- "H40.11X2": "011X2",
281
- "H40.51X3": "051X3",
282
- "T85.398A": "5398A"
283
- }
414
+ MediLink_ConfigLoader.log("All patient data collected from DOCX files: {}".format(all_patient_data), level="DEBUG")
284
415
 
285
- # Convert surgery dates in CSV data
286
- convert_surgery_date(csv_data)
416
+ # Extract patient IDs from csv_data for efficient matching
417
+ patient_ids_in_csv = {row.get('Patient ID', '').strip() for row in csv_data}
418
+
419
+ # Check if any patient data was collected
420
+ if not all_patient_data or not patient_ids_in_csv.intersection(all_patient_data.keys()):
421
+ MediLink_ConfigLoader.log("No patient data collected or no matching Patient IDs found. Skipping further processing.", level="INFO")
422
+ return # Exit the function early if no data is available
423
+
424
+ # Get Medisoft shorthand dictionary from crosswalk.
425
+ diagnosis_to_medisoft = crosswalk.get('diagnosis_to_medisoft', {})
287
426
 
288
427
  # Initialize counter for updated rows
289
428
  updated_count = 0
290
429
 
291
430
  # Update the "Default Diagnosis #1" column in the CSV data
292
431
  for row_num, row in enumerate(csv_data, start=1):
293
- MediLink_ConfigLoader.log("Processing row number {}.".format(row_num), level="INFO")
294
432
  patient_id = row.get('Patient ID', '').strip()
433
+ if patient_id not in patient_ids_in_csv:
434
+ continue # Skip rows that do not match any patient ID
435
+
436
+ MediLink_ConfigLoader.log("Processing row number {}.".format(row_num), level="DEBUG")
295
437
  surgery_date = row.get('Surgery Date', '')
296
438
 
297
439
  # Convert surgery_date to string format for lookup
@@ -300,17 +442,19 @@ def update_diagnosis_codes(csv_data):
300
442
  else:
301
443
  surgery_date_str = ''
302
444
 
303
- MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
445
+ MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
304
446
 
305
447
  if patient_id in all_patient_data:
306
448
  if surgery_date_str in all_patient_data[patient_id]:
307
449
  diagnosis_code, left_or_right_eye, femto_yes_or_no = all_patient_data[patient_id][surgery_date_str]
308
- MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
450
+ MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
309
451
 
310
452
  # Convert diagnosis code to Medisoft shorthand format.
311
- defaulted_code = diagnosis_code[1:].replace('.', '')[-5:] if diagnosis_code else ''
312
- medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, defaulted_code)
313
- MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="INFO")
453
+ medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, None)
454
+ if medisoft_shorthand is None and diagnosis_code:
455
+ defaulted_code = diagnosis_code.lstrip('H').lstrip('T8').replace('.', '')[-5:]
456
+ medisoft_shorthand = defaulted_code
457
+ MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="DEBUG")
314
458
 
315
459
  row['Default Diagnosis #1'] = medisoft_shorthand
316
460
  updated_count += 1
@@ -379,6 +523,7 @@ def load_insurance_data_from_mains(config):
379
523
  # TODO (Low) Performance: There probably needs to be a dictionary proxy for MAINS that gets updated.
380
524
  # Meh, this just has to be part of the new architecture plan where we make Medisoft a downstream
381
525
  # recipient from the db.
526
+ # TODO (High) The Medisoft Medicare flag needs to be brought in here.
382
527
  mains_path = config['MAINS_MED_PATH']
383
528
  mains_slices = crosswalk['mains_mapping']['slices']
384
529
 
@@ -419,7 +564,7 @@ def load_insurance_data_from_mapat(config, crosswalk):
419
564
 
420
565
  return patient_id_to_insurance_id
421
566
 
422
- def parse_z_dat(z_dat_path, config):
567
+ def parse_z_dat(z_dat_path, config): # Why is this in MediBot and not MediLink?
423
568
  """
424
569
  Parses the Z.dat file to map Patient IDs to Insurance Names using the provided fixed-width file format.
425
570
 
@@ -494,11 +639,12 @@ def load_historical_payer_to_patient_mappings(config):
494
639
 
495
640
  # Log the accumulated count for this CSV file
496
641
  if patient_count > 0:
497
- MediLink_ConfigLoader.log("CSV file '{}' has {} Patient IDs with Payer IDs.".format(filename, patient_count))
642
+ MediLink_ConfigLoader.log("CSV file '{}' has {} Patient IDs with Payer IDs.".format(filename, patient_count), level="DEBUG")
498
643
  else:
499
- MediLink_ConfigLoader.log("CSV file '{}' is empty or does not have valid Patient ID or Payer ID mappings.".format(filename))
644
+ MediLink_ConfigLoader.log("CSV file '{}' is empty or does not have valid Patient ID or Payer ID mappings.".format(filename), level="DEBUG")
500
645
  except Exception as e:
501
646
  print("Error processing file {}: {}".format(filename, e))
647
+ MediLink_ConfigLoader.log("Error processing file '{}': {}".format(filename, e), level="ERROR")
502
648
  except FileNotFoundError as e:
503
649
  print("Error: {}".format(e))
504
650