medicafe 0.240415.1__py3-none-any.whl → 0.240517.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +198 -0
- MediBot/MediBot.py +346 -0
- MediBot/MediBot_Charges.py +28 -0
- MediBot/MediBot_Crosswalk_Library.py +280 -0
- MediBot/MediBot_Preprocessor.py +247 -0
- MediBot/MediBot_Preprocessor_lib.py +357 -0
- MediBot/MediBot_UI.py +240 -0
- MediBot/MediBot_dataformat_library.py +198 -0
- MediBot/MediBot_docx_decoder.py +80 -0
- MediBot/MediPost.py +5 -0
- MediBot/PDF_to_CSV_Cleaner.py +211 -0
- MediBot/__init__.py +0 -0
- MediBot/update_json.py +43 -0
- MediBot/update_medicafe.py +57 -0
- MediLink/MediLink.py +381 -0
- MediLink/MediLink_277_decoder.py +92 -0
- MediLink/MediLink_837p_encoder.py +502 -0
- MediLink/MediLink_837p_encoder_library.py +890 -0
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +137 -0
- MediLink/MediLink_ConfigLoader.py +81 -0
- MediLink/MediLink_DataMgmt.py +258 -0
- MediLink/MediLink_Down.py +128 -0
- MediLink/MediLink_ERA_decoder.py +192 -0
- MediLink/MediLink_Gmail.py +100 -0
- MediLink/MediLink_Mailer.py +7 -0
- MediLink/MediLink_Scheduler.py +173 -0
- MediLink/MediLink_StatusCheck.py +4 -0
- MediLink/MediLink_UI.py +118 -0
- MediLink/MediLink_Up.py +383 -0
- MediLink/MediLink_batch.bat +7 -0
- MediLink/Soumit_api.py +22 -0
- MediLink/__init__.py +0 -0
- MediLink/test.py +74 -0
- medicafe-0.240517.0.dist-info/METADATA +53 -0
- medicafe-0.240517.0.dist-info/RECORD +39 -0
- {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +1 -1
- medicafe-0.240517.0.dist-info/top_level.txt +2 -0
- medicafe-0.240415.1.dist-info/METADATA +0 -17
- medicafe-0.240415.1.dist-info/RECORD +0 -5
- medicafe-0.240415.1.dist-info/top_level.txt +0 -1
- {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# Add parent directory of the project to the Python path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
9
|
+
sys.path.append(project_dir)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import MediLink_ConfigLoader
|
|
13
|
+
except ImportError:
|
|
14
|
+
from MediLink import MediLink_ConfigLoader
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from MediLink_API_v2 import fetch_payer_name_from_api
|
|
18
|
+
except ImportError:
|
|
19
|
+
from MediLink import MediLink_API_v2
|
|
20
|
+
fetch_payer_name_from_api = MediLink_API_v2.fetch_payer_name_from_api
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from MediBot import MediBot_Preprocessor_lib
|
|
24
|
+
except ImportError:
|
|
25
|
+
import MediBot_Preprocessor_lib
|
|
26
|
+
|
|
27
|
+
def check_and_initialize_crosswalk(config):
|
|
28
|
+
"""
|
|
29
|
+
Checks if the 'payer_id' key exists in the crosswalk. If not, prompts the user
|
|
30
|
+
to initialize the crosswalk.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
config (dict): Configuration settings.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
boolean: True if succeeded.
|
|
37
|
+
"""
|
|
38
|
+
# Reload for safety
|
|
39
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration(None, config.get('crosswalkPath', 'crosswalk.json'))
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Attempt to access the 'payer_id' key to ensure it exists
|
|
43
|
+
if 'payer_id' not in crosswalk:
|
|
44
|
+
raise KeyError("Missing 'payer_id' key in crosswalk.")
|
|
45
|
+
except KeyError:
|
|
46
|
+
error_message = "The 'payer_id' key does not exist in the crosswalk configuration. \n" \
|
|
47
|
+
"This could be because the crosswalk is not initialized. \n" \
|
|
48
|
+
"Consider running the Crosswalk initializer."
|
|
49
|
+
print(error_message)
|
|
50
|
+
MediLink_ConfigLoader.log(error_message, config, level="ERROR")
|
|
51
|
+
|
|
52
|
+
# Prompt user to initialize crosswalk
|
|
53
|
+
initialize_choice = input("\nADVANCED OPTION: The crosswalk may not be initialized. \nType 'yes' to initialize it now: ").strip().lower()
|
|
54
|
+
if initialize_choice == 'yes':
|
|
55
|
+
initialize_crosswalk_from_mapat()
|
|
56
|
+
_, crosswalk = MediLink_ConfigLoader.load_configuration() # Reload crosswalk
|
|
57
|
+
MediLink_ConfigLoader.log("Crosswalk reloaded successfully.", config, level="INFO")
|
|
58
|
+
else:
|
|
59
|
+
raise KeyError(error_message)
|
|
60
|
+
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def validate_and_correct_payer_ids(crosswalk, config):
|
|
64
|
+
"""Validates payer IDs via API and handles invalid IDs through user intervention."""
|
|
65
|
+
for payer_id in list(crosswalk['payer_id'].keys()):
|
|
66
|
+
try:
|
|
67
|
+
fetch_payer_name_from_api(payer_id, config, primary_endpoint=None)
|
|
68
|
+
MediLink_ConfigLoader.log("Payer ID {} validated successfully.".format(payer_id), config, level="INFO")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
MediLink_ConfigLoader.log("Payer ID validation failed for {}: {}".format(payer_id, e), config, level="WARNING")
|
|
71
|
+
|
|
72
|
+
while True:
|
|
73
|
+
corrected_payer_id = input("WWARNING: Invalid Payer ID {}. Enter the correct Payer ID for replacement or type 'FORCE' to continue with the unresolved Payer ID: ".format(payer_id))
|
|
74
|
+
|
|
75
|
+
if corrected_payer_id.strip().upper() == 'FORCE':
|
|
76
|
+
MediLink_ConfigLoader.log("User opted to force-continue with unresolved Payer ID {}. Warning: This may indicate an underlying issue.".format(payer_id), config, level="WARNING")
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if corrected_payer_id:
|
|
80
|
+
try:
|
|
81
|
+
fetch_payer_name_from_api(corrected_payer_id, config, primary_endpoint=None)
|
|
82
|
+
MediLink_ConfigLoader.log("Corrected Payer ID {} validated successfully.".format(corrected_payer_id), config, level="INFO")
|
|
83
|
+
|
|
84
|
+
if update_crosswalk_with_corrected_payer_id(payer_id, corrected_payer_id, config, crosswalk):
|
|
85
|
+
if 'csv_replacements' not in crosswalk:
|
|
86
|
+
crosswalk['csv_replacements'] = {}
|
|
87
|
+
crosswalk['csv_replacements'][payer_id] = corrected_payer_id
|
|
88
|
+
MediLink_ConfigLoader.log("Added replacement filter: {} -> {}".format(payer_id, corrected_payer_id), config, level="INFO")
|
|
89
|
+
else:
|
|
90
|
+
print("Failed to update crosswalk with the corrected Payer ID {}.".format(corrected_payer_id))
|
|
91
|
+
MediLink_ConfigLoader.log("Failed to update crosswalk with the corrected Payer ID {}.".format(corrected_payer_id), config, level="ERROR")
|
|
92
|
+
break
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print("Corrected Payer ID {} validation failed: {}".format(corrected_payer_id, e))
|
|
95
|
+
MediLink_ConfigLoader.log("Corrected Payer ID {} validation failed: {}".format(corrected_payer_id, e), config, level="ERROR")
|
|
96
|
+
else:
|
|
97
|
+
print("Exiting initialization. Please correct the Payer ID and retry.")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
def initialize_crosswalk_from_mapat():
|
|
101
|
+
"""
|
|
102
|
+
Input: Historical Carol's CSVs and MAPAT data.
|
|
103
|
+
|
|
104
|
+
Process:
|
|
105
|
+
Extract mappings from Carol's old CSVs to identify Payer IDs and associated Patient IDs.
|
|
106
|
+
Use MAPAT to correlate these Patient IDs with Insurance IDs.
|
|
107
|
+
Compile these mappings into the crosswalk, setting Payer IDs as keys and corresponding Insurance IDs as values.
|
|
108
|
+
|
|
109
|
+
Output: A fully populated crosswalk.json file that serves as a baseline for future updates.
|
|
110
|
+
"""
|
|
111
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
112
|
+
|
|
113
|
+
# Load historical mappings
|
|
114
|
+
try:
|
|
115
|
+
patient_id_to_insurance_id, payer_id_to_patient_ids = MediBot_Preprocessor_lib.load_data_sources(config, crosswalk)
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
print(e)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
# Map Payer IDs to Insurance IDs
|
|
121
|
+
payer_id_to_details = MediBot_Preprocessor_lib.map_payer_ids_to_insurance_ids(patient_id_to_insurance_id, payer_id_to_patient_ids)
|
|
122
|
+
|
|
123
|
+
# Update the crosswalk for payer IDs only, retaining other mappings
|
|
124
|
+
crosswalk['payer_id'] = payer_id_to_details
|
|
125
|
+
|
|
126
|
+
# Validate payer IDs via API and handle invalid IDs
|
|
127
|
+
validate_and_correct_payer_ids(crosswalk, config)
|
|
128
|
+
|
|
129
|
+
# Save the initial crosswalk
|
|
130
|
+
if save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk):
|
|
131
|
+
message = "Crosswalk initialized with mappings for {} payers.".format(len(crosswalk.get('payer_id', {})))
|
|
132
|
+
print(message)
|
|
133
|
+
MediLink_ConfigLoader.log(message, config, level="INFO")
|
|
134
|
+
else:
|
|
135
|
+
print("Failed to save the crosswalk.")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
return payer_id_to_details
|
|
138
|
+
|
|
139
|
+
def crosswalk_update(config, crosswalk):
|
|
140
|
+
"""
|
|
141
|
+
Updates the `crosswalk.json` file using mappings from MAINS, Z.dat, and Carol's CSV. This function integrates
|
|
142
|
+
user-defined insurance mappings from Z.dat with existing payer-to-insurance mappings in the crosswalk,
|
|
143
|
+
and validates these mappings using MAINS.
|
|
144
|
+
|
|
145
|
+
Steps:
|
|
146
|
+
1. Load mappings from MAINS for translating insurance names to IDs.
|
|
147
|
+
2. Load mappings from the latest Carol's CSV for new patient entries mapping Patient IDs to Payer IDs.
|
|
148
|
+
3. Parse incremental data from Z.dat which contains recent user interactions mapping Patient IDs to Insurance Names.
|
|
149
|
+
4. Update the crosswalk using the loaded and parsed data, ensuring each Payer ID maps to the correct Insurance IDs.
|
|
150
|
+
5. Persist the updated mappings back to the crosswalk file.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
config (dict): Configuration dictionary containing paths and other settings.
|
|
154
|
+
crosswalk (dict): Existing crosswalk mapping Payer IDs to sets of Insurance IDs.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
bool: True if the crosswalk was successfully updated and saved, False otherwise.
|
|
158
|
+
"""
|
|
159
|
+
# Load insurance mappings from MAINS (Insurance Name to Insurance ID)
|
|
160
|
+
insurance_name_to_id = MediBot_Preprocessor_lib.load_insurance_data_from_mains(config)
|
|
161
|
+
MediLink_ConfigLoader.log("Loaded insurance data from MAINS...")
|
|
162
|
+
|
|
163
|
+
# Load new Patient ID to Payer ID mappings from Carol's CSV (if necessary)
|
|
164
|
+
# TODO This is a low performance strategy.
|
|
165
|
+
patient_id_to_payer_id = MediBot_Preprocessor_lib.load_historical_payer_to_patient_mappings(config)
|
|
166
|
+
MediLink_ConfigLoader.log("Loaded historical mappings...")
|
|
167
|
+
|
|
168
|
+
# Load incremental mapping data from Z.dat (Patient ID to Insurance Name)
|
|
169
|
+
# TODO This may be a redundant approach?
|
|
170
|
+
patient_id_to_insurance_name = MediBot_Preprocessor_lib.parse_z_dat(config['MediLink_Config']['Z_DAT_PATH'], config['MediLink_Config'])
|
|
171
|
+
MediLink_ConfigLoader.log("Parsed Z data...")
|
|
172
|
+
|
|
173
|
+
# Update the crosswalk with new or revised mappings
|
|
174
|
+
for patient_id, payer_id in patient_id_to_payer_id.items():
|
|
175
|
+
insurance_name = patient_id_to_insurance_name.get(patient_id)
|
|
176
|
+
if insurance_name and insurance_name in insurance_name_to_id:
|
|
177
|
+
insurance_id = insurance_name_to_id[insurance_name]
|
|
178
|
+
|
|
179
|
+
# Ensure payer ID is in the crosswalk and initialize if not
|
|
180
|
+
MediLink_ConfigLoader.log("Initializing payer_id key...")
|
|
181
|
+
if 'payer_id' not in crosswalk:
|
|
182
|
+
crosswalk['payer_id'] = {}
|
|
183
|
+
if payer_id not in crosswalk['payer_id']:
|
|
184
|
+
# TODO The OPTUMEDI default here should be gathered via API and not just a default. There are 2 of these defaults!!
|
|
185
|
+
crosswalk['payer_id'][payer_id] = {'endpoint': 'OPTUMEDI', 'medisoft_id': set(), 'medisoft_medicare_id': set()}
|
|
186
|
+
|
|
187
|
+
# Update the medisoft_id set, temporarily using a set to avoid duplicates
|
|
188
|
+
crosswalk['payer_id'][payer_id]['medisoft_id'].add(insurance_id)
|
|
189
|
+
MediLink_ConfigLoader.log("Added new insurance ID {} to payer ID {}".format(insurance_id, payer_id))
|
|
190
|
+
|
|
191
|
+
# Convert sets to lists just before saving
|
|
192
|
+
for payer_id in crosswalk['payer_id']:
|
|
193
|
+
if isinstance(crosswalk['payer_id'][payer_id]['medisoft_id'], set):
|
|
194
|
+
crosswalk['payer_id'][payer_id]['medisoft_id'] = list(crosswalk['payer_id'][payer_id]['medisoft_id'])
|
|
195
|
+
if isinstance(crosswalk['payer_id'][payer_id]['medisoft_medicare_id'], set):
|
|
196
|
+
crosswalk['payer_id'][payer_id]['medisoft_medicare_id'] = list(crosswalk['payer_id'][payer_id]['medisoft_medicare_id'])
|
|
197
|
+
|
|
198
|
+
# Save the updated crosswalk to the specified file
|
|
199
|
+
return save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk)
|
|
200
|
+
|
|
201
|
+
def update_crosswalk_with_corrected_payer_id(old_payer_id, corrected_payer_id, config, crosswalk):
|
|
202
|
+
"""Updates the crosswalk with the corrected payer ID."""
|
|
203
|
+
# Update the payer_id section
|
|
204
|
+
if old_payer_id in crosswalk['payer_id']:
|
|
205
|
+
crosswalk['payer_id'][corrected_payer_id] = crosswalk['payer_id'].pop(old_payer_id)
|
|
206
|
+
MediLink_ConfigLoader.log("Crosswalk updated: replaced Payer ID {} with {}".format(old_payer_id, corrected_payer_id), config, level="INFO")
|
|
207
|
+
else:
|
|
208
|
+
MediLink_ConfigLoader.log("Failed to update crosswalk: could not find old Payer ID {}".format(old_payer_id), config, level="ERROR")
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
# Update the csv_replacements section
|
|
212
|
+
if 'csv_replacements' not in crosswalk:
|
|
213
|
+
crosswalk['csv_replacements'] = {}
|
|
214
|
+
crosswalk['csv_replacements'][old_payer_id] = corrected_payer_id
|
|
215
|
+
MediLink_ConfigLoader.log("Crosswalk csv_replacements updated: added {} -> {}".format(old_payer_id, corrected_payer_id), config, level="INFO")
|
|
216
|
+
|
|
217
|
+
# Save the updated crosswalk
|
|
218
|
+
return save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk)
|
|
219
|
+
|
|
220
|
+
def update_crosswalk_with_new_payer_id(insurance_name, payer_id, config):
|
|
221
|
+
"""Updates the crosswalk with a new payer ID."""
|
|
222
|
+
_, crosswalk = MediLink_ConfigLoader.load_configuration(None, config.get('crosswalkPath', 'crosswalk.json'))
|
|
223
|
+
medisoft_id = MediBot_Preprocessor_lib.load_insurance_data_from_mains(config).get(insurance_name)
|
|
224
|
+
|
|
225
|
+
if medisoft_id:
|
|
226
|
+
medisoft_id_str = str(medisoft_id)
|
|
227
|
+
if payer_id not in crosswalk['payer_id']:
|
|
228
|
+
crosswalk['payer_id'][payer_id] = {"medisoft_id": [medisoft_id_str], "medisoft_medicare_id": []}
|
|
229
|
+
else:
|
|
230
|
+
crosswalk['payer_id'][payer_id]['medisoft_id'].append(medisoft_id_str)
|
|
231
|
+
save_crosswalk(config['MediLink_Config']['crosswalkPath'], crosswalk)
|
|
232
|
+
MediLink_ConfigLoader.log("Updated crosswalk with new payer ID {} for insurance name {}".format(payer_id, insurance_name), config, level="INFO")
|
|
233
|
+
else:
|
|
234
|
+
message = "Failed to update crosswalk: Medisoft ID not found for insurance name {}".format(insurance_name)
|
|
235
|
+
print(message)
|
|
236
|
+
MediLink_ConfigLoader.log(message, config, level="ERROR")
|
|
237
|
+
|
|
238
|
+
def save_crosswalk(crosswalk_path, crosswalk):
|
|
239
|
+
"""
|
|
240
|
+
Saves the updated crosswalk to a JSON file.
|
|
241
|
+
Args:
|
|
242
|
+
crosswalk_path (str): Path to the crosswalk.json file.
|
|
243
|
+
crosswalk (dict): The updated crosswalk data.
|
|
244
|
+
Returns:
|
|
245
|
+
bool: True if the file was successfully saved, False otherwise.
|
|
246
|
+
"""
|
|
247
|
+
try:
|
|
248
|
+
# Initialize 'payer_id' key if not present
|
|
249
|
+
if 'payer_id' not in crosswalk:
|
|
250
|
+
print("save_crosswalk is initializing 'payer_id' key...")
|
|
251
|
+
crosswalk['payer_id'] = {}
|
|
252
|
+
|
|
253
|
+
# Convert all 'medisoft_id' fields from sets to lists if necessary
|
|
254
|
+
for k, v in crosswalk.get('payer_id', {}).items():
|
|
255
|
+
if isinstance(v.get('medisoft_id'), set):
|
|
256
|
+
v['medisoft_id'] = list(v['medisoft_id'])
|
|
257
|
+
|
|
258
|
+
with open(crosswalk_path, 'w') as file:
|
|
259
|
+
json.dump(crosswalk, file, indent=4) # Save the entire dictionary
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
except KeyError as e:
|
|
263
|
+
# Log the KeyError with specific information about what was missing
|
|
264
|
+
print("Key Error: A required key is missing in the crosswalk data -", e)
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
except TypeError as e:
|
|
268
|
+
# Handle data type errors (e.g., non-serializable types)
|
|
269
|
+
print("Type Error: There was a type issue with the data being saved in the crosswalk -", e)
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
except IOError as e:
|
|
273
|
+
# Handle I/O errors related to file operations
|
|
274
|
+
print("I/O Error: An error occurred while writing to the crosswalk file -", e)
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
# A general exception catch to log any other exceptions that may not have been anticipated
|
|
279
|
+
print("Unexpected crosswalk error:", e)
|
|
280
|
+
return False
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from collections import OrderedDict # so that the field_mapping stays in order.
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
import MediBot_Crosswalk_Library
|
|
8
|
+
|
|
9
|
+
# Add parent directory of the project to the Python path
|
|
10
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
11
|
+
sys.path.append(project_dir)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from MediLink import MediLink_ConfigLoader
|
|
15
|
+
except ImportError:
|
|
16
|
+
import MediLink_ConfigLoader
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import MediBot_Preprocessor_lib
|
|
20
|
+
except ImportError:
|
|
21
|
+
from MediBot import MediBot_Preprocessor_lib
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
Preprocessing Enhancements
|
|
25
|
+
- [X] Preprocess Insurance Policy Numbers and Group Numbers to replace '-' with ''.
|
|
26
|
+
- [X] De-duplicate entries in the CSV and only entering the px once even if they show up twice in the file.
|
|
27
|
+
- [ ] Implement dynamic field combination in CSV pre-processing for flexibility with various CSV formats.
|
|
28
|
+
- [ ] Enhance SSN cleaning logic to handle more variations of sensitive data masking.
|
|
29
|
+
- [ ] Optimize script startup and CSV loading to reduce initial latency.
|
|
30
|
+
|
|
31
|
+
Data Integrity and Validation
|
|
32
|
+
- [ ] Conduct a thorough CSV integrity check before processing to flag potential issues upfront.
|
|
33
|
+
- [ ] Implement a mechanism to confirm the accuracy of entered data, potentially through a verification step or summary report.
|
|
34
|
+
- [ ] Explore the possibility of integrating direct database queries for existing patient checks to streamline the process.
|
|
35
|
+
- [ ] Automate the replacement of spaces with underscores ('_') in last names for Medicare entries.
|
|
36
|
+
- [ ] Enhance CSV integrity checks to identify and report potential issues with data format, especially concerning insurance policy numbers and special character handling.
|
|
37
|
+
|
|
38
|
+
Known Issues and Bugs
|
|
39
|
+
- [ ] Address the handling of '.' and other special characters that may disrupt parsing, especially under Windows XP.
|
|
40
|
+
- [ ] Investigate the issue with Excel modifying long policy numbers in the CSV and provide guidance or a workaround.
|
|
41
|
+
|
|
42
|
+
Future Work
|
|
43
|
+
- [X] Check for PatientID number in La Forma Z to link back to Carol's table for mapping Medisoft insurance name to payerID and payer name and address.
|
|
44
|
+
- [X] Check for PatientID to Medisoft custom insurance name mapping in MAPAT.
|
|
45
|
+
- [X] Middle Names should all be single letters. Make sure it gets truncated before submitting.
|
|
46
|
+
- [ ] Consolidate data from multiple sources (Provider_Notes.csv, Surgery_Schedule.csv, and Carols_CSV.csv) into a single table with Patient ID as the key, ensuring all data elements are aligned and duplicate entries are minimized.
|
|
47
|
+
- [ ] Implement logic to verify and match Patient IDs across different files to ensure data integrity before consolidation. (Catching errors between source data)
|
|
48
|
+
- [ ] Optimize the preprocessing of surgery dates and diagnosis codes for use in patient billing and scheduling systems.
|
|
49
|
+
- [ ] Read Surgery Schedule doc and parse out a Patient ID : Diagnosis Code table.
|
|
50
|
+
- [ ] The Minutes & Cancellation data with logic to consolidate into one table in memory.
|
|
51
|
+
- [ ] Dynamically list the endpoint for a new Payer ID via API or user interaction to update the crosswalk.json efficiently.
|
|
52
|
+
- [ ] Pull listed addresses of insurance from the CSV. (Not really necessary)
|
|
53
|
+
- [ ] Retroactively learn Medisoft insurance name and payerID from the provided data sources.
|
|
54
|
+
|
|
55
|
+
Development Roadmap for crosswalk_update():
|
|
56
|
+
- [X] Automation required for updating the crosswalk.json when new Medisoft insurance is discovered.
|
|
57
|
+
- [X] New Medisoft insurances are identified based on the payer ID number.
|
|
58
|
+
- [X] Check the existence of the payer ID in crosswalk.json under existing endpoints.
|
|
59
|
+
- [X] Facilitate grouping of IDs for insurances like CIGNA with multiple addresses but few payer IDs.
|
|
60
|
+
- [X] Retroactive learning based on selected insurances in Medisoft
|
|
61
|
+
- [ ] Prompt user via endpoint APIs to add new payer ID to an endpoint if it does not exist.
|
|
62
|
+
- [ ] Retain payer IDs without Insurance ID for future assignments.
|
|
63
|
+
- [ ] Check for free payer IDs and determine the appropriate endpoint for assignment.
|
|
64
|
+
- [ ] Present unrecognized payer IDs with Carol's Insurance Name to users for assignment to Insurance ID. (Try API Call)
|
|
65
|
+
- [ ] Integrate API checks to verify payer ID availability and related information.
|
|
66
|
+
- [ ] Implement "Fax/Mail or Other" endpoint for unavailable payer IDs.
|
|
67
|
+
- [ ] Present user with a list of top insurances for selection based on fuzzy search scores.
|
|
68
|
+
- [ ] Establish payer ID to insurance ID relationship based on user selection.
|
|
69
|
+
- [ ] Implicitly establish payer ID to endpoint mapping based on user selection.
|
|
70
|
+
- [ ] Implement validation mechanisms to prevent incorrect mappings and ensure data integrity.
|
|
71
|
+
- [ ] Considerations for extracting insurance addresses (if necessary)
|
|
72
|
+
- [ ] Handle better the case where a payer_id doesn't exist (When Carol's CSV doesn't bring the Payer ID).
|
|
73
|
+
Maybe ask the user what the payer ID is for that patient? I dont know.
|
|
74
|
+
- [ ] TODO (MED) Crosswalk (both initializing and updating) needs to pull AFTER the Preprocessor for Carol's CSV because
|
|
75
|
+
all that data lives in-memory and then gets corrections or replacements before being used so we need
|
|
76
|
+
the post-correction data to be used to build and update the crosswalk.
|
|
77
|
+
"""
|
|
78
|
+
# Load configuration
|
|
79
|
+
# Should this also take args? Path for ./MediLink needed to be added for this to resolve
|
|
80
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
81
|
+
|
|
82
|
+
# CSV Preprocessor built for Carol
|
|
83
|
+
def preprocess_csv_data(csv_data, crosswalk):
|
|
84
|
+
try:
|
|
85
|
+
# Add the "Ins1 Insurance ID" column to the CSV data.
|
|
86
|
+
# This initializes the column with empty values for each row.
|
|
87
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Adding 'Ins1 Insurance ID' column to the CSV data...", level="INFO")
|
|
88
|
+
MediBot_Preprocessor_lib.add_insurance_id_column(csv_data)
|
|
89
|
+
|
|
90
|
+
# Filter out rows without a Patient ID and rows where the Primary Insurance
|
|
91
|
+
# is 'AETNA', 'AETNA MEDICARE', or 'HUMANA MED HMO'.
|
|
92
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Filtering out missing Patient IDs and 'AETNA', 'AETNA MEDICARE', or 'HUMANA MED HMO'...", level="INFO")
|
|
93
|
+
MediBot_Preprocessor_lib.filter_rows(csv_data)
|
|
94
|
+
|
|
95
|
+
# Convert 'Surgery Date' from string format to datetime objects for sorting purposes.
|
|
96
|
+
# Sort the patients by 'Surgery Date' and then by 'Patient Last' name alphabetically.
|
|
97
|
+
# Deduplicate patient records based on Patient ID, keeping the entry with the earliest surgery date.
|
|
98
|
+
# Update the CSV data to include only unique patient records.
|
|
99
|
+
# Re-sort the CSV data after deduplication to ensure the correct order.
|
|
100
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Sorting and de-duplicating patient records...", level="INFO")
|
|
101
|
+
MediBot_Preprocessor_lib.convert_surgery_date(csv_data)
|
|
102
|
+
MediBot_Preprocessor_lib.sort_and_deduplicate(csv_data)
|
|
103
|
+
|
|
104
|
+
# Convert 'Surgery Date' back to string format if needed for further processing.
|
|
105
|
+
# Combine 'Patient First', 'Patient Middle', and 'Patient Last' into a single 'Patient Name' field.
|
|
106
|
+
# Combine 'Patient Address1' and 'Patient Address2' into a single 'Patient Street' field.
|
|
107
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Constructing Patient Name and Address for Medisoft...", level="INFO")
|
|
108
|
+
MediBot_Preprocessor_lib.combine_fields(csv_data)
|
|
109
|
+
|
|
110
|
+
# Retrieve replacement values from the crosswalk.
|
|
111
|
+
# Iterate over each key-value pair in the replacements dictionary and replace the old value
|
|
112
|
+
# with the new value in the corresponding fields of each row.
|
|
113
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Applying mandatory replacements per Crosswalk...", level="INFO")
|
|
114
|
+
MediBot_Preprocessor_lib.apply_replacements(csv_data, crosswalk)
|
|
115
|
+
|
|
116
|
+
# Update the "Ins1 Insurance ID" column based on the crosswalk and the "Ins1 Payer ID" column for each row.
|
|
117
|
+
# If the Payer ID is not found in the crosswalk, create a placeholder entry in the crosswalk and mark the row for review.
|
|
118
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Populating 'Ins1 Insurance ID' based on Crosswalk...", level="INFO")
|
|
119
|
+
MediBot_Preprocessor_lib.update_insurance_ids(csv_data, crosswalk)
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
message = "An error occurred while pre-processing CSV data. Please repair the CSV directly and try again: {}".format(e)
|
|
123
|
+
MediLink_ConfigLoader.log(message, level="ERROR")
|
|
124
|
+
print(message)
|
|
125
|
+
|
|
126
|
+
def check_existing_patients(selected_patient_ids, MAPAT_MED_PATH):
|
|
127
|
+
existing_patients = []
|
|
128
|
+
patients_to_process = list(selected_patient_ids) # Clone the selected patient IDs list
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
with open(MAPAT_MED_PATH, 'r') as file:
|
|
132
|
+
next(file) # Skip header row
|
|
133
|
+
for line in file:
|
|
134
|
+
if line.startswith("0"): # 1 is a flag for a deleted record so it would need to be re-entered.
|
|
135
|
+
patient_id = line[194:202].strip() # Extract Patient ID (Columns 195-202)
|
|
136
|
+
patient_name = line[9:39].strip() # Extract Patient Name (Columns 10-39)
|
|
137
|
+
|
|
138
|
+
if patient_id in selected_patient_ids:
|
|
139
|
+
existing_patients.append((patient_id, patient_name))
|
|
140
|
+
# Remove all occurrences of this patient_id from patients_to_process as a filter rather than .remove because
|
|
141
|
+
# then it only makes one pass and removes the first instance.
|
|
142
|
+
except FileNotFoundError:
|
|
143
|
+
# Handle the case where MAPAT_MED_PATH is not found
|
|
144
|
+
print("MAPAT.med was not found at location indicated in config file.")
|
|
145
|
+
print("Skipping existing patient check and continuing...")
|
|
146
|
+
|
|
147
|
+
# Filter out all instances of existing patient IDs
|
|
148
|
+
patients_to_process = [id for id in patients_to_process if id not in [patient[0] for patient in existing_patients]]
|
|
149
|
+
|
|
150
|
+
return existing_patients, patients_to_process
|
|
151
|
+
|
|
152
|
+
def intake_scan(csv_headers, field_mapping):
|
|
153
|
+
identified_fields = OrderedDict()
|
|
154
|
+
missing_fields_warnings = []
|
|
155
|
+
required_fields = config["required_fields"]
|
|
156
|
+
|
|
157
|
+
# MediLink_ConfigLoader.log("Intake Scan - Field Mapping: {}".format(field_mapping))
|
|
158
|
+
# MediLink_ConfigLoader.log("Intake Scan - CSV Headers: {}".format(csv_headers))
|
|
159
|
+
|
|
160
|
+
# Iterate over the Medisoft fields defined in field_mapping
|
|
161
|
+
for medisoft_field in field_mapping.keys():
|
|
162
|
+
for pattern in field_mapping[medisoft_field]:
|
|
163
|
+
matched_headers = [header for header in csv_headers if re.search(pattern, header, re.IGNORECASE)]
|
|
164
|
+
if matched_headers:
|
|
165
|
+
# Assuming the first matched header is the desired one
|
|
166
|
+
identified_fields[matched_headers[0]] = medisoft_field
|
|
167
|
+
# MediLink_ConfigLoader.log("Found Header: {}".format(identified_fields[matched_headers[0]]))
|
|
168
|
+
break
|
|
169
|
+
else:
|
|
170
|
+
# Check if the missing field is a required field before appending the warning
|
|
171
|
+
if medisoft_field in required_fields:
|
|
172
|
+
missing_fields_warnings.append("WARNING: No matching CSV header found for Medisoft field '{0}'".format(medisoft_field))
|
|
173
|
+
|
|
174
|
+
# CSV Integrity Checks
|
|
175
|
+
# Check for blank or partially blank CSV
|
|
176
|
+
if len(csv_headers) == 0 or all(header == "" for header in csv_headers):
|
|
177
|
+
missing_fields_warnings.append("WARNING: The CSV appears to be blank or contains only headers without data.")
|
|
178
|
+
|
|
179
|
+
# Display the identified fields and missing fields warnings
|
|
180
|
+
#MediLink_ConfigLoader.log("The following Medisoft fields have been identified in the CSV:")
|
|
181
|
+
#for header, medisoft_field in identified_fields.items():
|
|
182
|
+
# MediLink_ConfigLoader.log("{} (CSV header: {})".format(medisoft_field, header))
|
|
183
|
+
|
|
184
|
+
# This section interprets the information from identified_fields and decides if there are significant issues.
|
|
185
|
+
# e.g. If the 'Street' value:key is 'Address', then any warnings about City, State, Zip can be ignored.
|
|
186
|
+
for header, field in identified_fields.items():
|
|
187
|
+
# Insurance Policy Numbers should be all alphanumeric with no other characters.
|
|
188
|
+
if 'Insurance Policy Number' in field:
|
|
189
|
+
policy_number = identified_fields.get(header)
|
|
190
|
+
if not bool(re.match("^[a-zA-Z0-9]*$", policy_number)):
|
|
191
|
+
missing_fields_warnings.append("WARNING: Insurance Policy Number '{}' contains invalid characters.".format(policy_number))
|
|
192
|
+
# Additional checks can be added as needed for other fields
|
|
193
|
+
|
|
194
|
+
if missing_fields_warnings:
|
|
195
|
+
MediLink_ConfigLoader.log("\nSome required fields could not be matched:")
|
|
196
|
+
for warning in missing_fields_warnings:
|
|
197
|
+
MediLink_ConfigLoader.log(warning)
|
|
198
|
+
|
|
199
|
+
return identified_fields
|
|
200
|
+
|
|
201
|
+
def main():
|
|
202
|
+
parser = argparse.ArgumentParser(description='Run MediLink Data Management Tasks')
|
|
203
|
+
parser.add_argument('--update-crosswalk', action='store_true',
|
|
204
|
+
help='Run the crosswalk update independently')
|
|
205
|
+
parser.add_argument('--init-crosswalk', action='store_true',
|
|
206
|
+
help='Initialize the crosswalk using historical data from MAPAT and Carols CSV')
|
|
207
|
+
parser.add_argument('--load-csv', action='store_true',
|
|
208
|
+
help='Load and process CSV data')
|
|
209
|
+
parser.add_argument('--preprocess-csv', action='store_true',
|
|
210
|
+
help='Preprocess CSV data based on specific rules')
|
|
211
|
+
parser.add_argument('--open-csv', action='store_true',
|
|
212
|
+
help='Open CSV for manual editing')
|
|
213
|
+
|
|
214
|
+
args = parser.parse_args()
|
|
215
|
+
|
|
216
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
217
|
+
|
|
218
|
+
# If no arguments provided, print usage instructions
|
|
219
|
+
if not any(vars(args).values()):
|
|
220
|
+
parser.print_help()
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
if args.update_crosswalk:
|
|
224
|
+
print("Updating the crosswalk...")
|
|
225
|
+
MediBot_Crosswalk_Library.crosswalk_update(config, crosswalk)
|
|
226
|
+
|
|
227
|
+
if args.init_crosswalk:
|
|
228
|
+
MediBot_Crosswalk_Library.initialize_crosswalk_from_mapat()
|
|
229
|
+
|
|
230
|
+
if args.load_csv:
|
|
231
|
+
print("Loading CSV data...")
|
|
232
|
+
csv_data = MediBot_Preprocessor_lib.load_csv_data(config['CSV_FILE_PATH'])
|
|
233
|
+
print("Loaded {} records from the CSV.".format(len(csv_data)))
|
|
234
|
+
|
|
235
|
+
if args.preprocess_csv:
|
|
236
|
+
if 'csv_data' in locals():
|
|
237
|
+
print("Preprocessing CSV data...")
|
|
238
|
+
preprocess_csv_data(csv_data, crosswalk)
|
|
239
|
+
else:
|
|
240
|
+
print("Error: CSV data needs to be loaded before preprocessing. Use --load-csv.")
|
|
241
|
+
|
|
242
|
+
if args.open_csv:
|
|
243
|
+
print("Opening CSV for editing...")
|
|
244
|
+
MediBot_Preprocessor_lib.open_csv_for_editing(config['CSV_FILE_PATH'])
|
|
245
|
+
|
|
246
|
+
if __name__ == '__main__':
|
|
247
|
+
main()
|