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.

Files changed (42) hide show
  1. MediBot/MediBot.bat +198 -0
  2. MediBot/MediBot.py +346 -0
  3. MediBot/MediBot_Charges.py +28 -0
  4. MediBot/MediBot_Crosswalk_Library.py +280 -0
  5. MediBot/MediBot_Preprocessor.py +247 -0
  6. MediBot/MediBot_Preprocessor_lib.py +357 -0
  7. MediBot/MediBot_UI.py +240 -0
  8. MediBot/MediBot_dataformat_library.py +198 -0
  9. MediBot/MediBot_docx_decoder.py +80 -0
  10. MediBot/MediPost.py +5 -0
  11. MediBot/PDF_to_CSV_Cleaner.py +211 -0
  12. MediBot/__init__.py +0 -0
  13. MediBot/update_json.py +43 -0
  14. MediBot/update_medicafe.py +57 -0
  15. MediLink/MediLink.py +381 -0
  16. MediLink/MediLink_277_decoder.py +92 -0
  17. MediLink/MediLink_837p_encoder.py +502 -0
  18. MediLink/MediLink_837p_encoder_library.py +890 -0
  19. MediLink/MediLink_API_v2.py +174 -0
  20. MediLink/MediLink_APIs.py +137 -0
  21. MediLink/MediLink_ConfigLoader.py +81 -0
  22. MediLink/MediLink_DataMgmt.py +258 -0
  23. MediLink/MediLink_Down.py +128 -0
  24. MediLink/MediLink_ERA_decoder.py +192 -0
  25. MediLink/MediLink_Gmail.py +100 -0
  26. MediLink/MediLink_Mailer.py +7 -0
  27. MediLink/MediLink_Scheduler.py +173 -0
  28. MediLink/MediLink_StatusCheck.py +4 -0
  29. MediLink/MediLink_UI.py +118 -0
  30. MediLink/MediLink_Up.py +383 -0
  31. MediLink/MediLink_batch.bat +7 -0
  32. MediLink/Soumit_api.py +22 -0
  33. MediLink/__init__.py +0 -0
  34. MediLink/test.py +74 -0
  35. medicafe-0.240517.0.dist-info/METADATA +53 -0
  36. medicafe-0.240517.0.dist-info/RECORD +39 -0
  37. {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +1 -1
  38. medicafe-0.240517.0.dist-info/top_level.txt +2 -0
  39. medicafe-0.240415.1.dist-info/METADATA +0 -17
  40. medicafe-0.240415.1.dist-info/RECORD +0 -5
  41. medicafe-0.240415.1.dist-info/top_level.txt +0 -1
  42. {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)