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
MediLink/MediLink.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import MediLink_Down
|
|
3
|
+
import MediLink_Up
|
|
4
|
+
import MediLink_ConfigLoader
|
|
5
|
+
import MediLink_837p_encoder
|
|
6
|
+
|
|
7
|
+
# For UI Functions
|
|
8
|
+
import os
|
|
9
|
+
import MediLink_UI # Import UI module for handling all user interfaces
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
# Add parent directory of the project to the Python path
|
|
13
|
+
import sys
|
|
14
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
15
|
+
sys.path.append(project_dir)
|
|
16
|
+
|
|
17
|
+
from MediBot import MediBot_Preprocessor_lib
|
|
18
|
+
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
19
|
+
from MediBot import MediBot_Crosswalk_Library
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
Development Tasks for Backend Enhancement in MediSoft Claims Submittal (MediLink) Script:
|
|
23
|
+
|
|
24
|
+
Implement dynamic configurations for multiple endpoints (Availity, Optum, PNT Data) with environmental settings support.
|
|
25
|
+
Enhance file detection with detailed logging and introduce integrity checks for pre-processing validation.
|
|
26
|
+
Verify file transmissions via WinSCP log analysis for successful endpoint acknowledgments and secure data transfer.
|
|
27
|
+
Automate response file handling from endpoints and integrate feedback into MediSoft with exception alerts.
|
|
28
|
+
De-persisting Intermediate Files.
|
|
29
|
+
When transmissions fail, there is some retaining of patient data in memory or something that seems to default
|
|
30
|
+
any new endpoint changes to Optum. May need to "de-confirm" patients, but leave the suggested endpoints as the previously
|
|
31
|
+
confirmed endpoints. This should be similar logic to if the user made a mistake and wants to go back and fix it.
|
|
32
|
+
These tasks involve backend enhancements such as dynamic configurations, file detection improvements, file transmission verification, automation of response file handling, and management of intermediate files and transmission failures.
|
|
33
|
+
|
|
34
|
+
TODO (Low) Availity has a response file that says "File was received at TIME. File was sent for processing." as a confirmation
|
|
35
|
+
that sits in the SendFiles folder after a submittal.
|
|
36
|
+
|
|
37
|
+
TODO (Crosswalk) When an endpoint is updated in the UI, the crosswalk should also be updated and saved for that payer ID because that payer ID
|
|
38
|
+
would basically forever need to be going to that endpoint for any patient. the suggested_endpoint should eventually be always correct.
|
|
39
|
+
|
|
40
|
+
BUG Suggested Endpoint when you say 'n' to proceed with transmission is not getting updated with the endpoint
|
|
41
|
+
that was selected previously by the user. However, when we go back to the confirmation list, we do have a persist of the assignment.
|
|
42
|
+
This can be confusing for the user.
|
|
43
|
+
|
|
44
|
+
MediLink
|
|
45
|
+
| - import MediLink_Down
|
|
46
|
+
| - import MediLink_ERA_decoder
|
|
47
|
+
| | - from MediLink_ConfigLoader import load_configuration
|
|
48
|
+
| | | - None
|
|
49
|
+
| | - from MediLink_DataMgmt import consolidate_csvs
|
|
50
|
+
| | | - from MediLink import MediLink_ConfigLoader
|
|
51
|
+
| | - None
|
|
52
|
+
| - from MediLink_DataMgmt import operate_winscp
|
|
53
|
+
| - from MediLink import MediLink_ConfigLoader
|
|
54
|
+
| - None
|
|
55
|
+
| - import MediLink_Up
|
|
56
|
+
| - None
|
|
57
|
+
| - import MediLink_ConfigLoader
|
|
58
|
+
| - None
|
|
59
|
+
| - import MediLink_837p_encoder
|
|
60
|
+
| - import MediLink_ConfigLoader
|
|
61
|
+
| | - None
|
|
62
|
+
| - from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
63
|
+
| - from MediLink import MediLink_ConfigLoader
|
|
64
|
+
| - None
|
|
65
|
+
| - import MediLink_837p_encoder_library
|
|
66
|
+
| - from MediLink import MediLink_ConfigLoader
|
|
67
|
+
| - None
|
|
68
|
+
| - import MediLink_UI
|
|
69
|
+
| - None
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def detect_and_display_file_summaries(directory_path, config, crosswalk):
|
|
73
|
+
"""
|
|
74
|
+
Detects new files in the specified directory and prepares detailed patient data for processing,
|
|
75
|
+
including suggestions for endpoints based on insurance provider information found in the config.
|
|
76
|
+
|
|
77
|
+
:param directory_path: Path to the directory containing files to be detected.
|
|
78
|
+
:param config: Configuration settings loaded from a JSON file.
|
|
79
|
+
:return: A tuple containing a list of new file paths and the detailed patient data.
|
|
80
|
+
"""
|
|
81
|
+
new_files = detect_new_files(directory_path)
|
|
82
|
+
if not new_files:
|
|
83
|
+
print(" No new claims detected. Check Medisoft claims output.\n")
|
|
84
|
+
return False, []
|
|
85
|
+
|
|
86
|
+
detailed_patient_data = [] # Initialize list for detailed patient data
|
|
87
|
+
for file_path in new_files:
|
|
88
|
+
detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
|
|
89
|
+
detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
|
|
90
|
+
|
|
91
|
+
# Return just the list of new files and the enriched detailed patient data
|
|
92
|
+
return new_files, detailed_patient_data
|
|
93
|
+
|
|
94
|
+
def detect_new_files(directory_path, file_extension='.DAT'):
|
|
95
|
+
"""
|
|
96
|
+
Scans the specified directory for new files with a given extension.
|
|
97
|
+
|
|
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
|
+
"""
|
|
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
|
|
108
|
+
|
|
109
|
+
def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
110
|
+
"""
|
|
111
|
+
Reads a fixed-width file, extracts file details including surgery date, patient ID,
|
|
112
|
+
patient name, primary insurance, and other necessary details for each record. It suggests
|
|
113
|
+
an endpoint based on insurance provider information found in the crosswalk and prepares
|
|
114
|
+
detailed patient data for processing.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
- file_path: Path to the fixed-width file.
|
|
118
|
+
- crosswalk: Crosswalk dictionary loaded from a JSON file.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
- A comprehensive data structure retaining detailed patient claim details needed for processing,
|
|
122
|
+
including new key-value pairs for file path, surgery date, patient name, and primary insurance.
|
|
123
|
+
"""
|
|
124
|
+
detailed_patient_data = []
|
|
125
|
+
|
|
126
|
+
# Load insurance data from MAINS to create a mapping from insurance names to their respective IDs
|
|
127
|
+
insurance_to_id = load_insurance_data_from_mains(config)
|
|
128
|
+
MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
|
|
129
|
+
|
|
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))
|
|
132
|
+
|
|
133
|
+
primary_insurance = parsed_data.get('INAME')
|
|
134
|
+
|
|
135
|
+
# Retrieve the insurance ID associated with the primary insurance
|
|
136
|
+
insurance_id = insurance_to_id.get(primary_insurance)
|
|
137
|
+
MediLink_ConfigLoader.log("Primary insurance ID retrieved for '{}': {}".format(primary_insurance, insurance_id))
|
|
138
|
+
|
|
139
|
+
# Use insurance ID to retrieve the payer ID(s) associated with the insurance
|
|
140
|
+
payer_ids = []
|
|
141
|
+
if insurance_id:
|
|
142
|
+
for payer_id, payer_data in crosswalk.get('payer_id', {}).items():
|
|
143
|
+
medisoft_ids = [str(id) for id in payer_data.get('medisoft_id', [])]
|
|
144
|
+
# MediLink_ConfigLoader.log("Payer ID: {}, Medisoft IDs: {}".format(payer_id, medisoft_ids))
|
|
145
|
+
if str(insurance_id) in medisoft_ids:
|
|
146
|
+
payer_ids.append(payer_id)
|
|
147
|
+
if payer_ids:
|
|
148
|
+
MediLink_ConfigLoader.log("Payer IDs retrieved for insurance '{}': {}".format(primary_insurance, payer_ids))
|
|
149
|
+
else:
|
|
150
|
+
MediLink_ConfigLoader.log("No payer IDs found for insurance '{}'".format(primary_insurance))
|
|
151
|
+
|
|
152
|
+
# Find the suggested endpoint from the crosswalk based on the payer IDs
|
|
153
|
+
suggested_endpoint = 'AVAILITY' # Default endpoint if no matching payer IDs found
|
|
154
|
+
if payer_ids:
|
|
155
|
+
payer_id = payer_ids[0] # Select the first payer ID
|
|
156
|
+
suggested_endpoint = crosswalk['payer_id'].get(payer_id, {}).get('endpoint', 'AVAILITY')
|
|
157
|
+
MediLink_ConfigLoader.log("Suggested endpoint for payer ID '{}': {}".format(payer_id, suggested_endpoint))
|
|
158
|
+
else:
|
|
159
|
+
MediLink_ConfigLoader.log("No suggested endpoint found for payer IDs: {}".format(payer_ids))
|
|
160
|
+
|
|
161
|
+
# Enrich detailed patient data with additional information and suggested endpoint
|
|
162
|
+
detailed_data = parsed_data.copy() # Copy parsed_data to avoid modifying the original dictionary
|
|
163
|
+
detailed_data.update({
|
|
164
|
+
'file_path': file_path,
|
|
165
|
+
'patient_id': parsed_data.get('CHART'),
|
|
166
|
+
'surgery_date': parsed_data.get('DATE'),
|
|
167
|
+
'patient_name': ' '.join([parsed_data.get(key, '') for key in ['FIRST', 'MIDDLE', 'LAST']]),
|
|
168
|
+
'amount': parsed_data.get('AMOUNT'),
|
|
169
|
+
'primary_insurance': primary_insurance,
|
|
170
|
+
'suggested_endpoint': suggested_endpoint
|
|
171
|
+
})
|
|
172
|
+
detailed_patient_data.append(detailed_data)
|
|
173
|
+
|
|
174
|
+
# Return only the enriched detailed patient data, eliminating the need for a separate summary list
|
|
175
|
+
return detailed_patient_data
|
|
176
|
+
|
|
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
|
+
def check_for_new_remittances(config):
|
|
197
|
+
print("\nChecking for new files across all endpoints...")
|
|
198
|
+
endpoints = config['MediLink_Config']['endpoints']
|
|
199
|
+
processed_endpoints = []
|
|
200
|
+
|
|
201
|
+
if isinstance(endpoints, dict): # BUG This check can probably be removed later.
|
|
202
|
+
for endpoint_key, endpoint_info in tqdm(endpoints.items(), desc="Processing endpoints"):
|
|
203
|
+
if 'remote_directory_down' in endpoint_info: # Check if the 'remote_directory_down' key exists
|
|
204
|
+
#print("Processing endpoint: ", endpoint_info['name'])
|
|
205
|
+
# BUG (Debug and verbosity removal) this is really for debug only. Positive statements can be muted.
|
|
206
|
+
try:
|
|
207
|
+
ERA_path = MediLink_Down.main(desired_endpoint=endpoint_key)
|
|
208
|
+
processed_endpoints.append((endpoint_info['name'], ERA_path))
|
|
209
|
+
MediLink_ConfigLoader.log("Results for {} saved to: {}".format(endpoint_info['name'], ERA_path))
|
|
210
|
+
# TODO (Low SFTP - Download side) This needs to check to see if this actually worked maybe winscplog before saying it completed successfully
|
|
211
|
+
# Check if there is commonality with the upload side so we can use the same validation function.
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print("An error occurred while checking remittances for {}: {}".format(endpoint_info['name'], e))
|
|
214
|
+
else:
|
|
215
|
+
MediLink_ConfigLoader.log("Skipping endpoint '{}' as it does not have 'remote_directory_down' configured.".format(endpoint_info['name']))
|
|
216
|
+
else:
|
|
217
|
+
print("Error: Endpoint config is not a 'dictionary' as expected.")
|
|
218
|
+
# Check if all ERA paths are the same
|
|
219
|
+
unique_era_paths = set(path for _, path in processed_endpoints)
|
|
220
|
+
if len(unique_era_paths) == 1:
|
|
221
|
+
common_era_path = unique_era_paths.pop() # Get the common ERA path
|
|
222
|
+
endpoints_list = ", ".join(endpoint for endpoint, _ in processed_endpoints)
|
|
223
|
+
print("\nProcessed Endpoints: {}".format(endpoints_list))
|
|
224
|
+
print("File located at: {}\n".format(common_era_path))
|
|
225
|
+
# TODO (MediPost) These prints will eventually be logs when MediPost is made.
|
|
226
|
+
|
|
227
|
+
else:
|
|
228
|
+
if processed_endpoints:
|
|
229
|
+
print("\nProcessed Endpoints:")
|
|
230
|
+
for endpoint, path in processed_endpoints:
|
|
231
|
+
print("Endpoint: {}, ERA Path: {}".format(endpoint, path))
|
|
232
|
+
else:
|
|
233
|
+
print("No endpoints were processed.")
|
|
234
|
+
|
|
235
|
+
def user_decision_on_suggestions(detailed_patient_data, config):
|
|
236
|
+
"""
|
|
237
|
+
Presents the user with all patient summaries and suggested endpoints,
|
|
238
|
+
then asks for confirmation to proceed with all or specify adjustments manually.
|
|
239
|
+
|
|
240
|
+
BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
|
|
241
|
+
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.
|
|
243
|
+
"""
|
|
244
|
+
# Display summaries of patient details and endpoints.
|
|
245
|
+
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
246
|
+
|
|
247
|
+
# Ask the user if they want to proceed with all suggested endpoints.
|
|
248
|
+
proceed = MediLink_UI.ask_for_proceeding_with_endpoints()
|
|
249
|
+
|
|
250
|
+
# If the user agrees to proceed with all suggested endpoints, confirm them.
|
|
251
|
+
if proceed:
|
|
252
|
+
return confirm_all_suggested_endpoints(detailed_patient_data)
|
|
253
|
+
# Otherwise, allow the user to adjust the endpoints manually.
|
|
254
|
+
else:
|
|
255
|
+
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
|
+
|
|
266
|
+
def select_and_adjust_files(detailed_patient_data, config):
|
|
267
|
+
"""
|
|
268
|
+
Allows users to select patients and adjust their endpoints by interfacing with UI functions.
|
|
269
|
+
|
|
270
|
+
BUG (Med suggested_endpoint) After the user is done making their selection (probably via a class?),
|
|
271
|
+
Then suggested_endpoint should update to persist the user selection as priority over its original suggestion.
|
|
272
|
+
Which means the crosswalk should persist the change in the endpoint as well.
|
|
273
|
+
"""
|
|
274
|
+
# Display options for patients
|
|
275
|
+
MediLink_UI.display_patient_options(detailed_patient_data)
|
|
276
|
+
|
|
277
|
+
# Get user-selected indices for adjustment
|
|
278
|
+
selected_indices = MediLink_UI.get_selected_indices(len(detailed_patient_data))
|
|
279
|
+
|
|
280
|
+
# Get an ordered list of endpoint keys
|
|
281
|
+
endpoint_keys = list(config['MediLink_Config']['endpoints'].keys())
|
|
282
|
+
|
|
283
|
+
# Iterate over each selected index and process endpoint changes
|
|
284
|
+
for i in selected_indices:
|
|
285
|
+
data = detailed_patient_data[i]
|
|
286
|
+
MediLink_UI.display_patient_for_adjustment(data['patient_name'], data.get('suggested_endpoint', 'N/A'))
|
|
287
|
+
|
|
288
|
+
endpoint_change = MediLink_UI.get_endpoint_decision()
|
|
289
|
+
if endpoint_change == 'y':
|
|
290
|
+
MediLink_UI.display_endpoint_options(config['MediLink_Config']['endpoints'])
|
|
291
|
+
endpoint_index = int(MediLink_UI.get_new_endpoint_choice()) - 1 # Adjusting for zero-based index
|
|
292
|
+
|
|
293
|
+
if 0 <= endpoint_index < len(endpoint_keys):
|
|
294
|
+
selected_endpoint_key = endpoint_keys[endpoint_index]
|
|
295
|
+
data['confirmed_endpoint'] = selected_endpoint_key
|
|
296
|
+
print("Endpoint changed to {0} for patient {1}.".format(config['MediLink_Config']['endpoints'][selected_endpoint_key]['name'], data['patient_name']))
|
|
297
|
+
# BUG (Med, Crosswalk & suggested_endpoint) Probably update crosswalk and suggested endpoint here???
|
|
298
|
+
else:
|
|
299
|
+
print("Invalid selection. Keeping the suggested endpoint.")
|
|
300
|
+
else:
|
|
301
|
+
data['confirmed_endpoint'] = data.get('suggested_endpoint', 'N/A')
|
|
302
|
+
|
|
303
|
+
return detailed_patient_data
|
|
304
|
+
|
|
305
|
+
def main_menu():
|
|
306
|
+
"""
|
|
307
|
+
Initializes the main menu loop and handles the overall program flow,
|
|
308
|
+
including loading configurations and managing user input for menu selections.
|
|
309
|
+
"""
|
|
310
|
+
# Load configuration settings and display the initial welcome message.
|
|
311
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
312
|
+
|
|
313
|
+
# Check to make sure payer_id key is available in crosswalk, otherwise, go through that crosswalk initialization flow
|
|
314
|
+
MediBot_Crosswalk_Library.check_and_initialize_crosswalk(config)
|
|
315
|
+
|
|
316
|
+
# Check if the application is in test mode
|
|
317
|
+
if config.get("MediLink_Config", {}).get("TestMode", False):
|
|
318
|
+
print("\n--- MEDILINK TEST MODE --- \nTo enable full functionality, please update the config file \nand set 'TestMode' to 'false'.")
|
|
319
|
+
|
|
320
|
+
# Display Welcome Message
|
|
321
|
+
MediLink_UI.display_welcome()
|
|
322
|
+
|
|
323
|
+
# Normalize the directory path for file operations.
|
|
324
|
+
directory_path = os.path.normpath(config['MediLink_Config']['inputFilePath'])
|
|
325
|
+
|
|
326
|
+
# Detect new files and collect detailed patient data if available.
|
|
327
|
+
new_files, detailed_patient_data = detect_and_display_file_summaries(directory_path, config, crosswalk)
|
|
328
|
+
|
|
329
|
+
while True:
|
|
330
|
+
# Define the menu options. Base options include checking remittances and exiting the program.
|
|
331
|
+
options = ["Check for new remittances", "Exit"]
|
|
332
|
+
# If new files are detected, add the option to submit claims.
|
|
333
|
+
if new_files:
|
|
334
|
+
options.insert(1, "Submit claims")
|
|
335
|
+
|
|
336
|
+
# Display the dynamically adjusted menu options.
|
|
337
|
+
MediLink_UI.display_menu(options)
|
|
338
|
+
# Retrieve user choice and handle it.
|
|
339
|
+
choice = MediLink_UI.get_user_choice()
|
|
340
|
+
|
|
341
|
+
if choice == '1':
|
|
342
|
+
# Handle remittance checking.
|
|
343
|
+
check_for_new_remittances(config)
|
|
344
|
+
elif choice == '2' and new_files:
|
|
345
|
+
# Handle the claims submission flow if new files are present.
|
|
346
|
+
handle_submission(detailed_patient_data, config)
|
|
347
|
+
elif choice == '3' or (choice == '2' and not new_files):
|
|
348
|
+
# Exit the program if the user chooses to exit or if no new files are present.
|
|
349
|
+
MediLink_UI.display_exit_message()
|
|
350
|
+
break
|
|
351
|
+
else:
|
|
352
|
+
# Display an error message if the user's choice does not match any valid option.
|
|
353
|
+
MediLink_UI.display_invalid_choice()
|
|
354
|
+
|
|
355
|
+
def handle_submission(detailed_patient_data, config):
|
|
356
|
+
"""
|
|
357
|
+
Handles the submission process for claims based on detailed patient data.
|
|
358
|
+
This function orchestrates the flow from user decision on endpoint suggestions to the actual submission of claims.
|
|
359
|
+
"""
|
|
360
|
+
# Initiate user interaction to confirm or adjust suggested endpoints.
|
|
361
|
+
adjusted_data = user_decision_on_suggestions(detailed_patient_data, config)
|
|
362
|
+
# Confirm all remaining suggested endpoints.
|
|
363
|
+
confirmed_data = confirm_all_suggested_endpoints(adjusted_data)
|
|
364
|
+
if confirmed_data: # Proceed if there are confirmed data entries.
|
|
365
|
+
# Organize data by confirmed endpoints for submission.
|
|
366
|
+
organized_data = organize_patient_data_by_endpoint(confirmed_data)
|
|
367
|
+
# Confirm transmission with the user and check for internet connectivity.
|
|
368
|
+
if MediLink_Up.confirm_transmission(organized_data):
|
|
369
|
+
if MediLink_Up.check_internet_connection():
|
|
370
|
+
# Submit claims if internet connectivity is confirmed.
|
|
371
|
+
_ = MediLink_Up.submit_claims(organized_data, config)
|
|
372
|
+
# TODO submit_claims will have a receipt return in the future.
|
|
373
|
+
else:
|
|
374
|
+
# Notify the user of an internet connection error.
|
|
375
|
+
print("Internet connection error. Please ensure you're connected and try again.")
|
|
376
|
+
else:
|
|
377
|
+
# Notify the user if the submission is cancelled.
|
|
378
|
+
print("Submission cancelled. No changes were made.")
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
main_menu()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
This script processes a 277 healthcare claim status response file, extracting and structuring key information
|
|
7
|
+
about each claim into a CSV format. The goal is to interpret acknowledgment returns and provide a readable receipt
|
|
8
|
+
of claim statuses for further analysis or record-keeping.
|
|
9
|
+
|
|
10
|
+
Extracted fields and their sources from the 277 transaction set include:
|
|
11
|
+
- Clearing House: Extracted from 'NM1' segment where entity identifier code is '41' (payer) as the clearinghouse or payer name.
|
|
12
|
+
- Received Date: Extracted from the 'DTP' segment with qualifier '050' indicating the date claim information was received.
|
|
13
|
+
- Claim Status Tracking #: Extracted from the 'TRN' segment, representing a unique identifier used to track the claim.
|
|
14
|
+
- Billed Amount: Extracted from the 'AMT' segment with qualifier 'YU' representing the total billed amount.
|
|
15
|
+
- Date of Service: Extracted from the 'DTP' segment with qualifier '472', indicating the date services were rendered.
|
|
16
|
+
- Last and First Name: Extracted from 'NM1' segment where entity identifier is 'QC' (patient) to obtain patient's last and first names.
|
|
17
|
+
- Acknowledged Amount: Extracted from the 'STC' segment, specifically the monetary amount acknowledged.
|
|
18
|
+
- Status: Extracted from the 'STC' segment, indicating the processing status of the claim.
|
|
19
|
+
|
|
20
|
+
Each record corresponds to a single claim, and the script consolidates these records from the raw 277 file into a structured CSV file.
|
|
21
|
+
The CSV output contains headers corresponding to the above fields for ease of review and use in subsequent processes.
|
|
22
|
+
|
|
23
|
+
Prerequisites:
|
|
24
|
+
- Python 3.x
|
|
25
|
+
- Access to a filesystem for reading input files and writing output CSV files.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
The script requires the path to the 277 file as input and specifies an output directory for the CSV files.
|
|
29
|
+
Example command-line usage:
|
|
30
|
+
python3 MediLink_277_decoder.py input_file.txt output_directory
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def parse_277_content(content):
|
|
34
|
+
segments = content.split('~')
|
|
35
|
+
records = []
|
|
36
|
+
current_record = {}
|
|
37
|
+
for segment in segments:
|
|
38
|
+
parts = segment.split('*')
|
|
39
|
+
if parts[0] == 'HL':
|
|
40
|
+
if current_record:
|
|
41
|
+
records.append(current_record)
|
|
42
|
+
current_record = {}
|
|
43
|
+
elif parts[0] == 'NM1':
|
|
44
|
+
if parts[1] == 'QC': # Patient information
|
|
45
|
+
current_record['Last'] = parts[3]
|
|
46
|
+
current_record['First'] = parts[4]
|
|
47
|
+
elif parts[1] == '41': # Payer information
|
|
48
|
+
current_record['Clearing House'] = parts[3]
|
|
49
|
+
elif parts[0] == 'TRN':
|
|
50
|
+
current_record['Claim Status Tracking #'] = parts[2]
|
|
51
|
+
elif parts[0] == 'STC':
|
|
52
|
+
current_record['Status'] = parts[1]
|
|
53
|
+
current_record['Acknowledged Amt'] = parts[4]
|
|
54
|
+
elif parts[0] == 'DTP':
|
|
55
|
+
if parts[1] == '472': # Service date
|
|
56
|
+
current_record['Date of Service'] = parts[3]
|
|
57
|
+
elif parts[1] == '050': # Received date
|
|
58
|
+
current_record['Received Date'] = parts[3]
|
|
59
|
+
elif parts[0] == 'AMT':
|
|
60
|
+
if parts[1] == 'YU':
|
|
61
|
+
current_record['Billed Amt'] = parts[2]
|
|
62
|
+
|
|
63
|
+
if current_record:
|
|
64
|
+
records.append(current_record)
|
|
65
|
+
|
|
66
|
+
return records
|
|
67
|
+
|
|
68
|
+
def write_records_to_csv(records, output_file_path):
|
|
69
|
+
with open(output_file_path, 'w', newline='') as csvfile:
|
|
70
|
+
fieldnames = ['Clearing House', 'Received Date', 'Claim Status Tracking #', 'Billed Amt', 'Date of Service', 'Last', 'First', 'Acknowledged Amt', 'Status']
|
|
71
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
72
|
+
writer.writeheader()
|
|
73
|
+
for record in records:
|
|
74
|
+
writer.writerow(record)
|
|
75
|
+
|
|
76
|
+
def main(file_path, output_directory):
|
|
77
|
+
if not os.path.exists(output_directory):
|
|
78
|
+
os.makedirs(output_directory)
|
|
79
|
+
|
|
80
|
+
output_file_path = os.path.join(output_directory, os.path.basename(file_path) + '_decoded.csv')
|
|
81
|
+
|
|
82
|
+
with open(file_path, 'r') as file:
|
|
83
|
+
content = file.read().replace('\n', '')
|
|
84
|
+
|
|
85
|
+
records = parse_277_content(content)
|
|
86
|
+
write_records_to_csv(records, output_file_path)
|
|
87
|
+
print("Decoded data written to {}".format(output_file_path))
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
file_path = sys.argv[1]
|
|
91
|
+
output_directory = 'output'
|
|
92
|
+
main(file_path, output_directory)
|