medicafe 0.240419.2__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 +166 -38
- MediBot/MediBot.py +74 -44
- MediBot/MediBot_Crosswalk_Library.py +280 -0
- MediBot/MediBot_Preprocessor.py +155 -191
- MediBot/MediBot_Preprocessor_lib.py +357 -0
- MediBot/MediBot_UI.py +80 -30
- MediBot/MediBot_dataformat_library.py +88 -35
- MediBot/MediBot_docx_decoder.py +80 -0
- MediBot/update_medicafe.py +46 -8
- MediLink/MediLink.py +138 -34
- MediLink/MediLink_837p_encoder.py +319 -209
- MediLink/MediLink_837p_encoder_library.py +453 -242
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +137 -0
- MediLink/MediLink_ConfigLoader.py +44 -32
- MediLink/MediLink_DataMgmt.py +85 -33
- MediLink/MediLink_Down.py +12 -35
- MediLink/MediLink_ERA_decoder.py +4 -4
- MediLink/MediLink_Gmail.py +99 -3
- MediLink/MediLink_Mailer.py +7 -0
- MediLink/MediLink_Scheduler.py +41 -0
- MediLink/MediLink_UI.py +19 -17
- MediLink/MediLink_Up.py +297 -31
- MediLink/MediLink_batch.bat +1 -1
- 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.240419.2.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +5 -5
- medicafe-0.240419.2.dist-info/METADATA +0 -19
- medicafe-0.240419.2.dist-info/RECORD +0 -32
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/top_level.txt +0 -0
|
@@ -2,36 +2,35 @@ import re
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
import argparse
|
|
4
4
|
import os
|
|
5
|
-
import MediLink_837p_encoder_library
|
|
6
5
|
import MediLink_ConfigLoader
|
|
7
6
|
from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
7
|
+
import MediLink_837p_encoder_library
|
|
8
8
|
#from tqdm import tqdm
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
|
-
|
|
11
|
+
Single File Processing Flow:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
4. Batch Processing and Output Handling: Enhance output file management to support efficient batch operations, including systematic naming and organization for output files.
|
|
17
|
-
5. Comprehensive Documentation: Maintain up-to-date and detailed documentation within the codebase, ensuring all functions and complex logic are clearly explained.
|
|
18
|
-
6. De-persisting Intermediate Files.
|
|
19
|
-
"""
|
|
20
|
-
def create_interchange_elements(config, endpoint, transaction_set_control_number):
|
|
21
|
-
"""
|
|
22
|
-
Create interchange headers and trailers for an 837P document.
|
|
13
|
+
This flow is triggered when the -d (directory) flag is not set. It handles the conversion of a single file specified by the -p flag.
|
|
14
|
+
It directly processes the single file specified, without the need to iterate over a directory.
|
|
15
|
+
The conversion initializes and processes this single file, appending the necessary EDI segments, and directly writes the output once processing is complete.
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
- config: Configuration settings loaded from a JSON file.
|
|
26
|
-
- endpoint_key: The endpoint for which the data is being processed.
|
|
27
|
-
- transaction_set_control_number: The starting transaction set control number.
|
|
17
|
+
Batch Directory Processing Flow:
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
Activated when the -d flag is set, indicating that the -p flag points to a directory rather than a single file.
|
|
20
|
+
Iterates over all files in the specified directory, processing only those that end with the ".DAT" extension.
|
|
21
|
+
Each file is processed in sequence, with each undergoing a full cycle of reading, processing, and output file generation as in the single file flow.
|
|
22
|
+
|
|
23
|
+
Development Task List:
|
|
24
|
+
|
|
25
|
+
- [ ] 1. File Path Management: Enhance the handling of input paths to efficiently manage both individual files and directories, accommodating a range of file processing scenarios.
|
|
26
|
+
- [ ] 2. User Interface Improvement: Advance the CLI for intuitive user interaction, offering clear options for file processing and real-time progress updates.
|
|
27
|
+
- [ ] 3. Validation and Logging: Strengthen validation processes for input data, incorporating thorough checks against business rules and enhanced detailed logging for improved traceability and troubleshooting.
|
|
28
|
+
- [ ] 4. Batch Processing and Output Handling: Enhance output file management to support efficient batch operations, including systematic naming and organization for output files.
|
|
29
|
+
- [ ] 5. Comprehensive Documentation: Maintain up-to-date and detailed documentation within the codebase, ensuring all functions and complex logic are clearly explained.
|
|
30
|
+
- [ ] 6. De-persisting Intermediate Files.
|
|
31
|
+
- [ ] 7. Determination of Relationship to Patient for insurance holder. Can Compare Insured Name & closeness of DOB (usually spouse [2], child [3]).
|
|
32
|
+
- [ ] 8. Consolidation of certain functions needs to happen here.
|
|
33
|
+
"""
|
|
35
34
|
|
|
36
35
|
def format_single_claim(patient_data, config, endpoint, transaction_set_control_number):
|
|
37
36
|
"""
|
|
@@ -46,6 +45,9 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
46
45
|
Returns:
|
|
47
46
|
- String representation of the formatted 837P claim.
|
|
48
47
|
"""
|
|
48
|
+
# Pre-resolve and enrich with Payer Name and ID for special case handling like Florida Blue.
|
|
49
|
+
patient_data = MediLink_837p_encoder_library.payer_id_to_payer_name(patient_data, config, endpoint)
|
|
50
|
+
|
|
49
51
|
segments = []
|
|
50
52
|
|
|
51
53
|
# Initialize with standard segments for all claims
|
|
@@ -53,7 +55,7 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
53
55
|
segments.append(MediLink_837p_encoder_library.create_bht_segment(patient_data))
|
|
54
56
|
|
|
55
57
|
# Submitter Name Segment and PER Contact Information (1000A Loop)
|
|
56
|
-
segments.extend(MediLink_837p_encoder_library.create_1000A_submitter_name_segment(config, endpoint))
|
|
58
|
+
segments.extend(MediLink_837p_encoder_library.create_1000A_submitter_name_segment(patient_data, config, endpoint))
|
|
57
59
|
|
|
58
60
|
# Receiver Name Segment (1000B Loop)
|
|
59
61
|
segments.extend([MediLink_837p_encoder_library.create_1000B_receiver_name_segment(config, endpoint)])
|
|
@@ -76,7 +78,10 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
76
78
|
segments.append(MediLink_837p_encoder_library.create_dmg_segment(patient_data))
|
|
77
79
|
|
|
78
80
|
# Payer information (2010BB loop)
|
|
79
|
-
|
|
81
|
+
# TODO This function now includes detailed outputs and potential user interactions with the new implementation.
|
|
82
|
+
# The new implementation introduces user inputs directly in the flow, which could disrupt automated batch processes.
|
|
83
|
+
# Ensure that there are mechanisms or workflows in place to handle such interruptions appropriately.
|
|
84
|
+
segments.extend([MediLink_837p_encoder_library.create_2010BB_payer_information_segment(patient_data)])
|
|
80
85
|
#segments.extend(MediLink_837p_encoder_library.create_payer_address_segments(config)) OMITTED
|
|
81
86
|
|
|
82
87
|
# Rendering Provider (2310B Loop)
|
|
@@ -92,8 +97,8 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
92
97
|
formatted_837p = MediLink_837p_encoder_library.generate_segment_counts('\n'.join(filter(None, segments)), transaction_set_control_number)
|
|
93
98
|
|
|
94
99
|
# Optionally, print or log the formatted 837P for debugging or verification
|
|
95
|
-
#
|
|
96
|
-
|
|
100
|
+
# TODO (Low UI/usability) Add chart number to this.
|
|
101
|
+
MediLink_ConfigLoader.log("Formatted 837P for endpoint {}.".format(endpoint), config, level="INFO")
|
|
97
102
|
|
|
98
103
|
return formatted_837p
|
|
99
104
|
|
|
@@ -118,194 +123,217 @@ def write_output_file(document_segments, output_directory, endpoint_key, input_f
|
|
|
118
123
|
Returns:
|
|
119
124
|
- String specifying the path to the successfully created output file. Consider returning a tuple (path, status) for enhanced error handling.
|
|
120
125
|
"""
|
|
121
|
-
|
|
126
|
+
# Check if document segments are empty
|
|
127
|
+
if not document_segments:
|
|
128
|
+
MediLink_ConfigLoader.log("Error: Empty document segments provided. No output file created.", config, level="ERROR")
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# Check if the output directory exists and is writable
|
|
132
|
+
if not os.path.exists(output_directory):
|
|
133
|
+
try:
|
|
134
|
+
os.makedirs(output_directory)
|
|
135
|
+
except OSError as e:
|
|
136
|
+
MediLink_ConfigLoader.log("Error: Failed to create output directory. {}".format(e), config, level="ERROR")
|
|
137
|
+
return None
|
|
138
|
+
elif not os.access(output_directory, os.W_OK):
|
|
139
|
+
MediLink_ConfigLoader.log("Error: Output directory is not writable.", config, level="ERROR")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# Generate new output file path
|
|
122
143
|
base_name = os.path.splitext(os.path.basename(input_file_path))[0]
|
|
123
144
|
timestamp = datetime.now().strftime("%m%d%H%M")
|
|
124
145
|
new_output_file_name = "{}_{}_{}.txt".format(base_name, endpoint_key.lower(), timestamp)
|
|
125
146
|
new_output_file_path = os.path.join(output_directory, new_output_file_name)
|
|
126
147
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
# Write formatted 837P document to the output file
|
|
149
|
+
try:
|
|
150
|
+
with open(new_output_file_path, 'w') as output_file:
|
|
151
|
+
output_file.write('\n'.join(document_segments))
|
|
152
|
+
MediLink_ConfigLoader.log("Successfully converted and saved to {}".format(new_output_file_path), config, level="INFO")
|
|
153
|
+
return new_output_file_path
|
|
154
|
+
except Exception as e:
|
|
155
|
+
MediLink_ConfigLoader.log("Error: Failed to write output file. {}".format(e), config, level="ERROR")
|
|
156
|
+
return None
|
|
135
157
|
|
|
136
158
|
def process_file(file_path, config, endpoint_key, transaction_set_control_number):
|
|
137
159
|
"""
|
|
138
|
-
|
|
160
|
+
Process the claim data from a file into the 837P format.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
file_path (str): The path to the file containing the claim data.
|
|
164
|
+
config (dict): Configuration settings loaded from a JSON file.
|
|
165
|
+
endpoint_key (str): The key representing the endpoint for which the claim is being processed.
|
|
166
|
+
transaction_set_control_number (int): The starting transaction set control number for 837P segments.
|
|
139
167
|
|
|
140
|
-
:
|
|
141
|
-
|
|
142
|
-
:param endpoint_key: The key representing the endpoint for which the claim is being processed.
|
|
143
|
-
:param transaction_set_control_number: The starting transaction set control number for 837P segments.
|
|
144
|
-
:return: A tuple containing the formatted claim segments and the next transaction set control number.
|
|
168
|
+
Returns:
|
|
169
|
+
tuple: A tuple containing the formatted claim segments and the next transaction set control number.
|
|
145
170
|
"""
|
|
146
|
-
|
|
171
|
+
valid_claims, validation_errors = read_and_validate_claims(file_path, config)
|
|
147
172
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# Assume validate_claim_data is a function that validates the parsed data
|
|
153
|
-
# DISABLED
|
|
154
|
-
#if not validate_claim_data(parsed_data, config):
|
|
155
|
-
# MediLink_837p_encoder_library.log("Validation failed for claim data in file: {}".format(file_path), config, level="ERROR")
|
|
156
|
-
# continue # Skip invalid claims
|
|
157
|
-
|
|
158
|
-
# Format the claim into 837P segments
|
|
159
|
-
formatted_claim = format_single_claim(parsed_data, config, endpoint_key, transaction_set_control_number)
|
|
160
|
-
formatted_claims.append(formatted_claim)
|
|
161
|
-
transaction_set_control_number += 1 # Increment for each successfully processed claim
|
|
173
|
+
# Handle validation errors
|
|
174
|
+
if validation_errors:
|
|
175
|
+
if not MediLink_837p_encoder_library.handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
176
|
+
return None, transaction_set_control_number # Halt processing if the user chooses
|
|
162
177
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
# Process each valid claim
|
|
179
|
+
formatted_claims, transaction_set_control_number = format_claims(valid_claims, config, endpoint_key, transaction_set_control_number)
|
|
180
|
+
|
|
181
|
+
formatted_claims_str = '\n'.join(formatted_claims) # Join formatted claims into a single string
|
|
166
182
|
return formatted_claims_str, transaction_set_control_number
|
|
167
183
|
|
|
168
|
-
def
|
|
184
|
+
def read_and_validate_claims(file_path, config):
|
|
169
185
|
"""
|
|
170
|
-
|
|
171
|
-
If spaces are found, prompts the user to input a new path.
|
|
172
|
-
If the directory doesn't exist, creates it.
|
|
173
|
-
"""
|
|
174
|
-
while ' ' in output_directory:
|
|
175
|
-
print("\nWARNING: The output directory path contains spaces, which can cause issues with upload operations.")
|
|
176
|
-
print(" Current proposed path: {}".format(output_directory))
|
|
177
|
-
new_path = input("Please enter a new path for the output directory: ")
|
|
178
|
-
output_directory = new_path.strip() # Remove leading/trailing spaces
|
|
179
|
-
|
|
180
|
-
# Check if the directory exists, if not, create it
|
|
181
|
-
if not os.path.exists(output_directory):
|
|
182
|
-
os.makedirs(output_directory)
|
|
183
|
-
print("INFO: Created output directory: {}".format(output_directory))
|
|
184
|
-
|
|
185
|
-
return output_directory
|
|
186
|
+
Read and validate claim data from a file.
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
Args:
|
|
189
|
+
file_path (str): The path to the file containing the claim data.
|
|
190
|
+
config (dict): Configuration settings loaded from a JSON file.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
tuple: A tuple containing a list of valid parsed data and a list of validation errors.
|
|
194
|
+
"""
|
|
195
|
+
valid_claims = [] # List to store valid parsed data
|
|
196
|
+
validation_errors = [] # List to store validation errors
|
|
197
|
+
|
|
198
|
+
# Iterate over data in the file
|
|
199
|
+
for personal_info, insurance_info, service_info in read_fixed_width_data(file_path):
|
|
200
|
+
# Parse data into a usable format
|
|
201
|
+
parsed_data = parse_fixed_width_data(personal_info, insurance_info, service_info, config.get('MediLink_Config', config))
|
|
202
|
+
# Validate the parsed data
|
|
203
|
+
is_valid, errors = validate_claim_data(parsed_data, config)
|
|
204
|
+
if is_valid:
|
|
205
|
+
valid_claims.append(parsed_data) # Add valid data to the list
|
|
206
|
+
else:
|
|
207
|
+
validation_errors.append(errors) # Add validation errors to the list
|
|
208
|
+
# Log validation failure
|
|
209
|
+
MediLink_ConfigLoader.log("Validation failed for claim data in file: {}. Errors: {}".format(file_path, errors), config, level="ERROR")
|
|
210
|
+
|
|
211
|
+
return valid_claims, validation_errors
|
|
212
|
+
|
|
213
|
+
def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_control_number):
|
|
214
|
+
"""
|
|
215
|
+
Formats a list of parsed claim data into 837P segments.
|
|
191
216
|
|
|
192
217
|
Parameters:
|
|
193
|
-
-
|
|
218
|
+
- parsed_data_list: List of dictionaries containing parsed claim data.
|
|
194
219
|
- config: Configuration settings loaded from a JSON file.
|
|
220
|
+
- endpoint: The endpoint key representing the specific endpoint.
|
|
221
|
+
- starting_transaction_set_control_number: Starting transaction set control number for 837P segments.
|
|
195
222
|
|
|
196
223
|
Returns:
|
|
197
|
-
- A list of
|
|
224
|
+
- A list of formatted 837P claims and the next transaction set control number.
|
|
198
225
|
"""
|
|
226
|
+
formatted_claims = []
|
|
227
|
+
transaction_set_control_number = starting_transaction_set_control_number
|
|
199
228
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
for data in detailed_patient_data:
|
|
205
|
-
endpoint = data['confirmed_endpoint']
|
|
206
|
-
if endpoint not in data_by_endpoint:
|
|
207
|
-
data_by_endpoint[endpoint] = []
|
|
208
|
-
data_by_endpoint[endpoint].append(data)
|
|
209
|
-
|
|
210
|
-
# List to store paths of converted files for each endpoint
|
|
211
|
-
converted_files_paths = []
|
|
212
|
-
|
|
213
|
-
# Determine the total number of unique endpoints for progress bar
|
|
214
|
-
# total_endpoints = len(data_by_endpoint)
|
|
229
|
+
for parsed_data in parsed_data_list:
|
|
230
|
+
formatted_claim = format_single_claim(parsed_data, config, endpoint, transaction_set_control_number)
|
|
231
|
+
formatted_claims.append(formatted_claim)
|
|
232
|
+
transaction_set_control_number += 1 # Increment for each successfully processed claim
|
|
215
233
|
|
|
216
|
-
|
|
217
|
-
for endpoint, patient_data_list in data_by_endpoint.items():
|
|
218
|
-
# tqdm(data_by_endpoint.items(), desc="Creating 837p(s)", total=total_endpoints, ascii=True)
|
|
219
|
-
# Each endpoint might have multiple patients' data to be processed into a single 837P file
|
|
220
|
-
if patient_data_list:
|
|
221
|
-
converted_path = process_claim(config['MediLink_Config'], endpoint, patient_data_list)
|
|
222
|
-
if converted_path:
|
|
223
|
-
converted_files_paths.append(converted_path)
|
|
224
|
-
|
|
225
|
-
return converted_files_paths
|
|
234
|
+
return formatted_claims, transaction_set_control_number
|
|
226
235
|
|
|
227
|
-
|
|
236
|
+
# Validation Function checks the completeness and correctness of each claim's data
|
|
237
|
+
def validate_claim_data(parsed_data, config, required_fields=[]):
|
|
228
238
|
"""
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
Used by both paths.
|
|
240
|
+
|
|
241
|
+
Validates the completeness and correctness of each claim's data based on configurable requirements.
|
|
231
242
|
|
|
232
243
|
Parameters:
|
|
244
|
+
- parsed_data: Dictionary containing claim data to validate.
|
|
233
245
|
- config: Configuration settings loaded from a JSON file.
|
|
234
|
-
-
|
|
235
|
-
- patient_data_list: A list of dictionaries, each containing detailed patient data.
|
|
246
|
+
- required_fields: Optional list of tuples indicating required fields and their respective regex patterns for validation.
|
|
236
247
|
|
|
237
248
|
Returns:
|
|
238
|
-
-
|
|
239
|
-
"""
|
|
240
|
-
# Retrieve endpoint-specific configuration
|
|
241
|
-
endpoint_config = config['endpoints'].get(endpoint)
|
|
242
|
-
if not endpoint_config:
|
|
243
|
-
print("Endpoint configuration for {} not found.".format(endpoint))
|
|
244
|
-
return None
|
|
249
|
+
- (bool, list): Tuple containing a boolean indicating whether the data is valid and a list of error messages if any.
|
|
245
250
|
|
|
246
|
-
#
|
|
247
|
-
output_directory = config.get('outputFilePath', '')
|
|
251
|
+
# TODO This required fields thing needs to be redone.
|
|
248
252
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
253
|
+
if required_fields is None:
|
|
254
|
+
required_fields = [
|
|
255
|
+
('CHART', None),
|
|
256
|
+
('billing_provider_npi', r'^\d{10}$'),
|
|
257
|
+
('IPOLICY', None),
|
|
258
|
+
('CODEA', None),
|
|
259
|
+
('DATE', r'^\d{8}$'),
|
|
260
|
+
('AMOUNT', None),
|
|
261
|
+
('TOS', None),
|
|
262
|
+
('DIAG', None)
|
|
263
|
+
]
|
|
264
|
+
"""
|
|
265
|
+
errors = []
|
|
266
|
+
MediLink_ConfigLoader.log("Starting claim vata validation...")
|
|
267
|
+
if not required_fields:
|
|
268
|
+
# If no required fields are specified, assume validation is true
|
|
269
|
+
return True, []
|
|
252
270
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
for
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
expected_keys = {field[0] for field in required_fields} # Set of expected field keys
|
|
272
|
+
received_keys = set(parsed_data.keys()) # Set of keys present in the parsed data
|
|
273
|
+
|
|
274
|
+
# Check if there is any intersection between expected keys and received keys
|
|
275
|
+
if not expected_keys & received_keys:
|
|
276
|
+
# Log the preview of expected and received keys
|
|
277
|
+
preview_msg = "Validation skipped: No matching fields found between expected and received data."
|
|
278
|
+
error_msg = "{}\nExpected keys: {}\nReceived keys: {}".format(preview_msg, expected_keys, received_keys)
|
|
279
|
+
MediLink_ConfigLoader.log(error_msg, config, level="WARNING")
|
|
280
|
+
print(error_msg) # Optionally print to console for immediate feedback
|
|
281
|
+
return True, [preview_msg] # Return true to say that it's valid data anyway.
|
|
282
|
+
|
|
283
|
+
# Check for missing or empty fields and validate patterns
|
|
284
|
+
for field, pattern in required_fields:
|
|
285
|
+
value = parsed_data.get(field)
|
|
286
|
+
if not value:
|
|
287
|
+
errors.append("Missing or empty field: {}".format(field))
|
|
288
|
+
elif pattern and not re.match(pattern, value):
|
|
289
|
+
errors.append("Invalid format in field {}: {}".format(field, value))
|
|
290
|
+
|
|
291
|
+
# Validate date fields if required and ensure they are in the correct format
|
|
292
|
+
date_field = 'DATE'
|
|
293
|
+
date_value = parsed_data.get(date_field)
|
|
294
|
+
if date_value:
|
|
295
|
+
try:
|
|
296
|
+
datetime.strptime(date_value, "%Y%m%d")
|
|
297
|
+
except ValueError:
|
|
298
|
+
errors.append("Invalid date format: {}".format(date_value))
|
|
299
|
+
|
|
300
|
+
# Log validation errors and return
|
|
301
|
+
if errors:
|
|
302
|
+
for error in errors:
|
|
303
|
+
MediLink_ConfigLoader.log(error, config, level="ERROR")
|
|
304
|
+
return False, errors
|
|
276
305
|
|
|
277
|
-
|
|
278
|
-
return write_output_file(document_segments, output_directory, endpoint, patient_data_list[0]['file_path'], config)
|
|
306
|
+
return True, []
|
|
279
307
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
errors = []
|
|
308
|
+
def process_and_write_file(file_path, config, endpoint, starting_tscn=1):
|
|
309
|
+
"""
|
|
310
|
+
Process a single file, create complete 837P document with headers and trailers, and write to output file.
|
|
284
311
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
312
|
+
Parameters:
|
|
313
|
+
- file_path: Path to the .DAT file to be processed.
|
|
314
|
+
- config: Configuration settings.
|
|
315
|
+
- endpoint: Endpoint key.
|
|
316
|
+
- starting_tscn: Starting Transaction Set Control Number.
|
|
317
|
+
"""
|
|
318
|
+
print("Processing: {}".format(file_path))
|
|
319
|
+
MediLink_ConfigLoader.log("Processing: {}".format(file_path))
|
|
320
|
+
formatted_data, transaction_set_control_number = process_file(file_path, config, endpoint, starting_tscn)
|
|
321
|
+
isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
|
|
289
322
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
errors.append("Invalid date format: {}".format(parsed_data.get('DATE')))
|
|
299
|
-
|
|
300
|
-
# Log validation errors
|
|
301
|
-
if errors:
|
|
302
|
-
for error in errors:
|
|
303
|
-
MediLink_837p_encoder_library.log(error, config, level="ERROR")
|
|
304
|
-
return False
|
|
323
|
+
# Combine everything into a single document
|
|
324
|
+
complete_document = "{}\n{}\n{}\n{}\n{}".format(
|
|
325
|
+
isa_header,
|
|
326
|
+
gs_header,
|
|
327
|
+
formatted_data,
|
|
328
|
+
ge_trailer,
|
|
329
|
+
iea_trailer
|
|
330
|
+
)
|
|
305
331
|
|
|
306
|
-
|
|
332
|
+
# Write to output file
|
|
333
|
+
output_file_path = write_output_file([complete_document], config.get('outputFilePath', ''), endpoint, file_path, config)
|
|
334
|
+
print("File processed. Output saved to: {}".format(output_file_path))
|
|
307
335
|
|
|
308
|
-
|
|
336
|
+
def main():
|
|
309
337
|
"""
|
|
310
338
|
Converts fixed-width files to 837P format for health claim submissions.
|
|
311
339
|
|
|
@@ -349,44 +377,126 @@ if __name__ == "__main__":
|
|
|
349
377
|
|
|
350
378
|
print("Starting the conversion process for {}. Processing {} at '{}'.".format(args.endpoint, 'directory' if args.is_directory else 'file', args.path))
|
|
351
379
|
|
|
352
|
-
# Load configuration
|
|
353
380
|
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
381
|
+
config = config.get('MediLink_Config', config)
|
|
354
382
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
process_files(args.path, config, args.endpoint, args.is_directory)
|
|
384
|
+
print("Conversion complete.")
|
|
385
|
+
|
|
386
|
+
def process_files(path, config, endpoint, is_directory):
|
|
387
|
+
"""
|
|
388
|
+
Processes either a single file or all files within a directory.
|
|
389
|
+
|
|
390
|
+
Parameters:
|
|
391
|
+
- path: Path to the input fixed-width file or directory to process.
|
|
392
|
+
- config: Configuration settings loaded from a JSON file.
|
|
393
|
+
- endpoint: The endpoint for which the conversion is intended.
|
|
394
|
+
- is_directory: Boolean flag indicating if the path is a directory.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
- None
|
|
398
|
+
"""
|
|
399
|
+
if is_directory:
|
|
400
|
+
MediLink_ConfigLoader.log("Processing all .DAT files in: {}".format(path))
|
|
401
|
+
for file_name in os.listdir(path):
|
|
402
|
+
if file_name.endswith(".DAT"):
|
|
403
|
+
file_path = os.path.join(path, file_name)
|
|
404
|
+
process_and_write_file(file_path, config, endpoint)
|
|
405
|
+
else:
|
|
406
|
+
MediLink_ConfigLoader.log("Processing the single file: {}".format(path))
|
|
407
|
+
process_and_write_file(path, config, endpoint)
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
main()
|
|
411
|
+
|
|
412
|
+
# The functions below are the ones that are used as non-main library by outside scripts.
|
|
413
|
+
#######################################################################################
|
|
414
|
+
|
|
415
|
+
def convert_files_for_submission(detailed_patient_data, config):
|
|
416
|
+
"""
|
|
417
|
+
Processes detailed patient data for submission based on their confirmed endpoints,
|
|
418
|
+
generating a separate 837P file for each endpoint.
|
|
419
|
+
|
|
420
|
+
Parameters:
|
|
421
|
+
- detailed_patient_data: A list containing detailed patient data with endpoint information.
|
|
422
|
+
- config: Configuration settings loaded from a JSON file.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
- A list of paths to the converted files ready for submission.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
# Initialize a dictionary to hold patient data segregated by confirmed endpoints
|
|
429
|
+
data_by_endpoint = {}
|
|
368
430
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
431
|
+
# Populate the dictionary with patient data
|
|
432
|
+
for data in detailed_patient_data:
|
|
433
|
+
endpoint = data['confirmed_endpoint']
|
|
434
|
+
if endpoint not in data_by_endpoint:
|
|
435
|
+
data_by_endpoint[endpoint] = []
|
|
436
|
+
data_by_endpoint[endpoint].append(data)
|
|
437
|
+
|
|
438
|
+
# List to store paths of converted files for each endpoint
|
|
439
|
+
converted_files_paths = []
|
|
440
|
+
|
|
441
|
+
# Determine the total number of unique endpoints for progress bar
|
|
442
|
+
# total_endpoints = len(data_by_endpoint)
|
|
443
|
+
|
|
444
|
+
# Iterate over each endpoint and process its corresponding patient data
|
|
445
|
+
for endpoint, patient_data_list in data_by_endpoint.items():
|
|
446
|
+
# tqdm(data_by_endpoint.items(), desc="Creating 837p(s)", total=total_endpoints, ascii=True)
|
|
447
|
+
# Each endpoint might have multiple patients' data to be processed into a single 837P file
|
|
448
|
+
if patient_data_list:
|
|
449
|
+
converted_path = process_claim(config['MediLink_Config'], endpoint, patient_data_list)
|
|
450
|
+
if converted_path:
|
|
451
|
+
converted_files_paths.append(converted_path)
|
|
377
452
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
453
|
+
return converted_files_paths
|
|
454
|
+
|
|
455
|
+
def process_claim(config, endpoint, patient_data_list):
|
|
456
|
+
"""
|
|
457
|
+
Processes patient data for a specified endpoint, converting it into the 837P format.
|
|
458
|
+
Generates a separate 837P file for each endpoint based on detailed patient data.
|
|
459
|
+
|
|
460
|
+
Parameters:
|
|
461
|
+
- config: Configuration settings loaded from a JSON file.
|
|
462
|
+
- endpoint_key: The key representing the endpoint for which the data is being processed.
|
|
463
|
+
- patient_data_list: A list of dictionaries, each containing detailed patient data.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
- Path to the converted file, or None if an error occurs.
|
|
381
467
|
|
|
382
|
-
if
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# Process a single file
|
|
390
|
-
process_and_write_file(args.path, config, args.endpoint)
|
|
468
|
+
TODO (LOW) Why are there duplicated interchange flows? Because the arg if we're doing a .dat directory or not.
|
|
469
|
+
Although, that shouldn't be making duplicates of these interchange headers. That's still confusing and could end up making
|
|
470
|
+
duplicate interchange headers because processing .dat in batch might be fast enough to be a problem.
|
|
471
|
+
"""
|
|
472
|
+
output_directory = MediLink_837p_encoder_library.get_output_directory(config)
|
|
473
|
+
if not output_directory:
|
|
474
|
+
return None
|
|
391
475
|
|
|
392
|
-
|
|
476
|
+
# Initialize the transaction set control number and document segments
|
|
477
|
+
transaction_set_control_number = 1
|
|
478
|
+
document_segments = []
|
|
479
|
+
|
|
480
|
+
# Process each patient's data in the list
|
|
481
|
+
for patient_data in patient_data_list:
|
|
482
|
+
# Validate each patient's data
|
|
483
|
+
is_valid, validation_errors = validate_claim_data(patient_data, config)
|
|
484
|
+
if is_valid:
|
|
485
|
+
# Format the claim if data is valid
|
|
486
|
+
formatted_claim = format_single_claim(patient_data, config, endpoint, transaction_set_control_number)
|
|
487
|
+
document_segments.append(formatted_claim)
|
|
488
|
+
transaction_set_control_number += 1
|
|
489
|
+
else:
|
|
490
|
+
if MediLink_837p_encoder_library.handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
491
|
+
continue # Skip the current patient
|
|
492
|
+
|
|
493
|
+
# Create interchange elements with the final transaction set control number
|
|
494
|
+
isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
|
|
495
|
+
|
|
496
|
+
# Insert headers at the beginning and append trailers at the end of document segments
|
|
497
|
+
document_segments.insert(0, gs_header)
|
|
498
|
+
document_segments.insert(0, isa_header)
|
|
499
|
+
document_segments.extend([ge_trailer, iea_trailer])
|
|
500
|
+
|
|
501
|
+
# Write the complete 837P document to an output file and return its path
|
|
502
|
+
return write_output_file(document_segments, output_directory, endpoint, patient_data_list[0]['file_path'], config)
|