medicafe 0.250822.2__py3-none-any.whl → 0.250909.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.
- MediBot/MediBot.py +11 -4
- MediBot/MediBot_Crosswalk_Library.py +16 -3
- MediBot/MediBot_Crosswalk_Utils.py +12 -2
- MediBot/MediBot_Preprocessor_lib.py +1821 -1728
- MediBot/MediBot_docx_decoder.py +14 -3
- MediBot/__init__.py +1 -1
- MediCafe/MediLink_ConfigLoader.py +12 -1
- MediCafe/__init__.py +1 -1
- MediCafe/api_core.py +116 -14
- MediCafe/core_utils.py +9 -4
- MediCafe/deductible_utils.py +1233 -0
- MediLink/MediLink_837p_encoder_library.py +123 -39
- MediLink/MediLink_Deductible.py +569 -555
- MediLink/MediLink_Deductible_Validator.py +9 -3
- MediLink/MediLink_Display_Utils.py +364 -2
- MediLink/MediLink_UI.py +20 -2
- MediLink/__init__.py +1 -1
- {medicafe-0.250822.2.dist-info → medicafe-0.250909.0.dist-info}/METADATA +1 -1
- {medicafe-0.250822.2.dist-info → medicafe-0.250909.0.dist-info}/RECORD +23 -27
- MediCafe/api_core_backup.py +0 -428
- MediLink/insurance_type_integration_test.py +0 -361
- MediLink/test_cob_library.py +0 -436
- MediLink/test_timing.py +0 -59
- MediLink/test_validation.py +0 -127
- {medicafe-0.250822.2.dist-info → medicafe-0.250909.0.dist-info}/LICENSE +0 -0
- {medicafe-0.250822.2.dist-info → medicafe-0.250909.0.dist-info}/WHEEL +0 -0
- {medicafe-0.250822.2.dist-info → medicafe-0.250909.0.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250822.2.dist-info → medicafe-0.250909.0.dist-info}/top_level.txt +0 -0
MediLink/MediLink_Deductible.py
CHANGED
@@ -50,8 +50,14 @@ UPGRADED TO LATEST CORE_UTILS:
|
|
50
50
|
- Improved import error handling with fallbacks
|
51
51
|
"""
|
52
52
|
# MediLink_Deductible.py
|
53
|
+
"""
|
54
|
+
TODO Consdier the possibility of being CSV agnostic and looking for the date of service up to 60 days old and
|
55
|
+
then with an option to select specific patients to look up for all the valid rows.
|
56
|
+
|
57
|
+
"""
|
53
58
|
import os, sys, json
|
54
59
|
from datetime import datetime
|
60
|
+
from collections import defaultdict
|
55
61
|
|
56
62
|
# Add parent directory to Python path to access MediCafe module
|
57
63
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
@@ -78,17 +84,24 @@ try:
|
|
78
84
|
except ImportError:
|
79
85
|
api_core = None
|
80
86
|
|
81
|
-
# Import
|
87
|
+
# Import deductible utilities from MediCafe
|
82
88
|
try:
|
83
|
-
from MediCafe import
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
from MediCafe.deductible_utils import (
|
90
|
+
validate_and_format_date,
|
91
|
+
convert_eligibility_to_enhanced_format,
|
92
|
+
resolve_payer_ids_from_csv,
|
93
|
+
get_payer_id_for_patient,
|
94
|
+
merge_responses,
|
95
|
+
backfill_enhanced_result
|
96
|
+
)
|
97
|
+
except ImportError as e:
|
98
|
+
print("Warning: Unable to import MediCafe.deductible_utils: {}".format(e))
|
99
|
+
# Fallback to local functions if utilities not available
|
100
|
+
validate_and_format_date = None
|
101
|
+
convert_eligibility_to_enhanced_format = None
|
102
|
+
resolve_payer_ids_from_csv = None
|
103
|
+
get_payer_id_for_patient = None
|
104
|
+
merge_responses = None
|
92
105
|
except ImportError as e:
|
93
106
|
print("Error: Unable to import MediCafe.core_utils. Please ensure MediCafe package is properly installed.")
|
94
107
|
# Don't call log_import_error here since it's not available yet
|
@@ -112,67 +125,25 @@ try:
|
|
112
125
|
from MediBot import MediBot_Preprocessor_lib
|
113
126
|
except ImportError as e:
|
114
127
|
print("Warning: Unable to import MediBot_Preprocessor_lib: {}".format(e))
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
# 4-digit year formats
|
122
|
-
'%Y-%m-%d', # 1990-01-15
|
123
|
-
'%m/%d/%Y', # 01/15/1990
|
124
|
-
'%m-%d-%Y', # 01-15-1990
|
125
|
-
'%d-%m-%Y', # 15-01-1990
|
126
|
-
'%d/%m/%Y', # 15/01/1990
|
127
|
-
'%d-%b-%Y', # 15-Jan-1990
|
128
|
-
'%d %b %Y', # 15 Jan 1990
|
129
|
-
'%b %d, %Y', # Jan 15, 1990
|
130
|
-
'%b %d %Y', # Jan 15 1990
|
131
|
-
'%B %d, %Y', # January 15, 1990
|
132
|
-
'%B %d %Y', # January 15 1990
|
133
|
-
'%Y/%m/%d', # 1990/01/15
|
134
|
-
'%Y%m%d', # 19900115
|
135
|
-
|
136
|
-
# 2-digit year formats
|
137
|
-
'%m/%d/%y', # 01/15/90
|
138
|
-
'%m-%d-%y', # 01-15-90
|
139
|
-
'%d/%m/%y', # 15/01/90
|
140
|
-
'%d-%m-%y', # 15-01-90
|
141
|
-
'%d %b %y', # 15 Jan 90
|
142
|
-
'%b %d, %y', # Jan 15, 90
|
143
|
-
'%b %d %y', # Jan 15 90
|
144
|
-
'%y/%m/%d', # 90/01/15
|
145
|
-
'%y-%m-%d', # 90-01-15
|
146
|
-
'%y%m%d', # 900115
|
147
|
-
|
148
|
-
# Single digit formats (no leading zeros)
|
149
|
-
'%m/%d/%Y', # 1/15/1990 (already covered above)
|
150
|
-
'%m-%d-%Y', # 1-15-1990 (already covered above)
|
151
|
-
'%d/%m/%Y', # 15/1/1990 (already covered above)
|
152
|
-
'%d-%m-%Y', # 15-1-1990 (already covered above)
|
153
|
-
'%m/%d/%y', # 1/15/90 (already covered above)
|
154
|
-
'%m-%d-%y', # 1-15-90 (already covered above)
|
155
|
-
'%d/%m/%y', # 15/1/90 (already covered above)
|
156
|
-
'%d-%m-%y', # 15-1-90 (already covered above)
|
157
|
-
]
|
128
|
+
try:
|
129
|
+
import MediBot_Preprocessor_lib
|
130
|
+
except ImportError as e2:
|
131
|
+
print("Error: Cannot import MediBot_Preprocessor_lib: {}".format(e2))
|
132
|
+
print("This module is required for CSV processing.")
|
133
|
+
sys.exit(1)
|
158
134
|
|
159
|
-
|
135
|
+
# Fallback date validation function if utilities not available
|
136
|
+
def _fallback_validate_and_format_date(date_str):
|
137
|
+
"""Fallback date validation function if MediCafe.deductible_utils not available"""
|
138
|
+
if validate_and_format_date is not None:
|
139
|
+
return validate_and_format_date(date_str)
|
140
|
+
else:
|
141
|
+
# Simple fallback implementation
|
160
142
|
try:
|
161
|
-
|
162
|
-
|
163
|
-
parsed_date = datetime.strptime(date_str, fmt)
|
164
|
-
# Handle year 00-99: assume 1950-2049 range
|
165
|
-
if parsed_date.year < 50:
|
166
|
-
parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
|
167
|
-
elif parsed_date.year < 100:
|
168
|
-
parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
|
169
|
-
formatted_date = parsed_date.strftime('%Y-%m-%d')
|
170
|
-
else:
|
171
|
-
formatted_date = datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
|
172
|
-
return formatted_date
|
143
|
+
from datetime import datetime
|
144
|
+
return datetime.strptime(date_str, '%Y-%m-%d').strftime('%Y-%m-%d')
|
173
145
|
except ValueError:
|
174
|
-
|
175
|
-
return None
|
146
|
+
return None
|
176
147
|
|
177
148
|
# Use latest core_utils configuration cache for better performance
|
178
149
|
_get_config, (_config_cache, _crosswalk_cache) = create_config_cache()
|
@@ -203,15 +174,43 @@ if provider_last_name == 'Unknown':
|
|
203
174
|
MediLink_ConfigLoader.log("Warning: provider_last_name was not found in the configuration.", level="WARNING")
|
204
175
|
|
205
176
|
# Define the list of payer_id's to iterate over
|
206
|
-
payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602'] # United Healthcare.
|
177
|
+
payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602'] # United Healthcare ONLY.
|
178
|
+
|
207
179
|
|
208
180
|
# Get the latest CSV
|
209
181
|
CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
|
210
|
-
|
182
|
+
try:
|
183
|
+
csv_data = MediBot_Preprocessor_lib.load_csv_data(CSV_FILE_PATH)
|
184
|
+
print("Successfully loaded CSV data: {} records".format(len(csv_data)))
|
185
|
+
except Exception as e:
|
186
|
+
print("Error loading CSV data: {}".format(e))
|
187
|
+
print("CSV_FILE_PATH: {}".format(CSV_FILE_PATH))
|
188
|
+
csv_data = []
|
211
189
|
|
212
190
|
# Only keep rows that have an exact match with a payer ID from the payer_ids list
|
191
|
+
if not csv_data:
|
192
|
+
print("Error: No CSV data loaded. Please check the CSV file path and format.")
|
193
|
+
sys.exit(1)
|
194
|
+
|
213
195
|
valid_rows = [row for row in csv_data if str(row.get('Ins1 Payer ID', '')).strip() in payer_ids]
|
214
196
|
|
197
|
+
if not valid_rows:
|
198
|
+
print("Error: No valid rows found with supported payer IDs.")
|
199
|
+
print("Supported payer IDs: {}".format(payer_ids))
|
200
|
+
print("Available payer IDs in CSV: {}".format(set(str(row.get('Ins1 Payer ID', '')).strip() for row in csv_data if row.get('Ins1 Payer ID', ''))))
|
201
|
+
sys.exit(1)
|
202
|
+
|
203
|
+
# DEBUG: Log available fields in the first row for diagnostic purposes (DEBUG level is suppressed by default)
|
204
|
+
if valid_rows:
|
205
|
+
try:
|
206
|
+
first_row = valid_rows[0]
|
207
|
+
MediLink_ConfigLoader.log("DEBUG: Available fields in CSV data:", level="DEBUG")
|
208
|
+
for field_name in sorted(first_row.keys()):
|
209
|
+
MediLink_ConfigLoader.log(" - '{}': '{}'".format(field_name, first_row[field_name]), level="DEBUG")
|
210
|
+
MediLink_ConfigLoader.log("DEBUG: End of available fields", level="DEBUG")
|
211
|
+
except Exception:
|
212
|
+
pass
|
213
|
+
|
215
214
|
# Extract important columns for summary with fallback
|
216
215
|
summary_valid_rows = [
|
217
216
|
{
|
@@ -222,19 +221,129 @@ summary_valid_rows = [
|
|
222
221
|
for row in valid_rows
|
223
222
|
]
|
224
223
|
|
225
|
-
#
|
226
|
-
|
227
|
-
|
228
|
-
|
224
|
+
# Display enhanced summary of valid rows using unified display philosophy
|
225
|
+
try:
|
226
|
+
from MediLink.MediLink_Display_Utils import display_enhanced_deductible_table
|
227
|
+
except ImportError as e:
|
228
|
+
print("Warning: Unable to import MediLink_Display_Utils: {}".format(e))
|
229
|
+
# Create a fallback display function
|
230
|
+
def display_enhanced_deductible_table(data, context="", title=""):
|
231
|
+
print("Fallback display for {}: {} records".format(context, len(data)))
|
232
|
+
for i, row in enumerate(data, 1):
|
233
|
+
print("{:03d}: {} | {} | {} | {} | {} | {} | [{}]".format(
|
234
|
+
i,
|
235
|
+
row.get('Patient ID', ''),
|
236
|
+
row.get('Patient Name', '')[:20],
|
237
|
+
row.get('Patient DOB', ''),
|
238
|
+
row.get('Primary Policy Number', '')[:12],
|
239
|
+
row.get('Ins1 Payer ID', ''),
|
240
|
+
row.get('Service Date', ''),
|
241
|
+
row.get('status', 'READY')
|
242
|
+
))
|
229
243
|
|
230
|
-
#
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
244
|
+
# Patients will be derived from patient_groups below
|
245
|
+
|
246
|
+
# Build fast index for (dob, member_id) -> CSV row to avoid repeated scans
|
247
|
+
patient_row_index = {}
|
248
|
+
for row in valid_rows:
|
249
|
+
idx_dob = _fallback_validate_and_format_date(row.get('Patient DOB', row.get('DOB', '')))
|
250
|
+
idx_member_id = row.get('Primary Policy Number', row.get('Ins1 Member ID', '')).strip()
|
251
|
+
if idx_dob and idx_member_id:
|
252
|
+
patient_row_index[(idx_dob, idx_member_id)] = row
|
253
|
+
|
254
|
+
# Group patients by (dob, member_id)
|
255
|
+
patient_groups = defaultdict(list)
|
256
|
+
for row in valid_rows:
|
257
|
+
dob = _fallback_validate_and_format_date(row.get('Patient DOB', row.get('DOB', '')))
|
258
|
+
member_id = row.get('Primary Policy Number', row.get('Ins1 Member ID', '')).strip()
|
259
|
+
# Try multiple possible service date field names (after header cleaning)
|
260
|
+
service_date = row.get('Service Date', '')
|
261
|
+
if not service_date:
|
262
|
+
service_date = row.get('Surgery Date', '')
|
263
|
+
if not service_date:
|
264
|
+
service_date = row.get('Date of Service', '')
|
265
|
+
if not service_date:
|
266
|
+
service_date = row.get('DOS', '')
|
267
|
+
if not service_date:
|
268
|
+
service_date = row.get('Date', '')
|
269
|
+
if dob and member_id:
|
270
|
+
# Try to parse service date, but handle various formats
|
271
|
+
service_date_sort = datetime.min
|
272
|
+
if service_date:
|
273
|
+
try:
|
274
|
+
# Try common date formats
|
275
|
+
for fmt in ['%m-%d-%Y', '%m/%d/%Y', '%Y-%m-%d', '%m-%d-%y', '%m/%d/%y']:
|
276
|
+
try:
|
277
|
+
service_date_sort = datetime.strptime(service_date, fmt)
|
278
|
+
break
|
279
|
+
except ValueError:
|
280
|
+
continue
|
281
|
+
except:
|
282
|
+
pass # Keep datetime.min if parsing fails
|
283
|
+
|
284
|
+
patient_groups[(dob, member_id)].append({
|
285
|
+
'service_date_display': service_date,
|
286
|
+
'service_date_sort': service_date_sort,
|
287
|
+
'patient_id': row.get('Patient ID', '')
|
288
|
+
})
|
289
|
+
|
290
|
+
# Update patients to unique
|
291
|
+
patients = list(patient_groups.keys())
|
292
|
+
|
293
|
+
# Use the enhanced table display for pre-API context
|
294
|
+
# Create display data from unique patients with their service dates
|
295
|
+
display_data = []
|
296
|
+
for (dob, member_id), service_records in patient_groups.items():
|
297
|
+
# Find the original row data for this patient
|
298
|
+
original_row = patient_row_index.get((dob, member_id))
|
299
|
+
|
300
|
+
if original_row:
|
301
|
+
# Use the first service record for display
|
302
|
+
first_service = service_records[0] if service_records else {}
|
303
|
+
# Try multiple possible field names for patient name (after header cleaning)
|
304
|
+
patient_name = original_row.get('Patient Name', '')
|
305
|
+
if not patient_name:
|
306
|
+
patient_name = original_row.get('Name', '')
|
307
|
+
if not patient_name:
|
308
|
+
patient_name = original_row.get('Member Name', '')
|
309
|
+
if not patient_name:
|
310
|
+
patient_name = original_row.get('Primary Insured Name', '')
|
311
|
+
if not patient_name:
|
312
|
+
patient_name = original_row.get('Subscriber Name', '')
|
313
|
+
if not patient_name:
|
314
|
+
patient_name = original_row.get('First Name', '') + ' ' + original_row.get('Last Name', '')
|
315
|
+
if not patient_name:
|
316
|
+
patient_name = original_row.get('Ins1 Subscriber Name', '')
|
317
|
+
if not patient_name:
|
318
|
+
patient_name = original_row.get('Subscriber First Name', '') + ' ' + original_row.get('Subscriber Last Name', '')
|
319
|
+
if not patient_name:
|
320
|
+
# Try additional field names that might be in the CSV
|
321
|
+
patient_name = original_row.get('Patient', '')
|
322
|
+
if not patient_name:
|
323
|
+
patient_name = original_row.get('Member', '')
|
324
|
+
if not patient_name:
|
325
|
+
patient_name = original_row.get('Subscriber', '')
|
326
|
+
if not patient_name:
|
327
|
+
# Try combining first and last name fields
|
328
|
+
first_name = original_row.get('Patient First', '') or original_row.get('First', '') or original_row.get('FirstName', '') or original_row.get('First Name', '')
|
329
|
+
last_name = original_row.get('Patient Last', '') or original_row.get('Last', '') or original_row.get('LastName', '') or original_row.get('Last Name', '')
|
330
|
+
if first_name or last_name:
|
331
|
+
patient_name = (first_name + ' ' + last_name).strip()
|
332
|
+
if not patient_name.strip():
|
333
|
+
patient_name = 'Unknown Patient'
|
334
|
+
|
335
|
+
display_row = {
|
336
|
+
'Patient ID': original_row.get('Patient ID', ''),
|
337
|
+
'Patient Name': patient_name,
|
338
|
+
'Patient DOB': dob,
|
339
|
+
'Primary Policy Number': member_id,
|
340
|
+
'Ins1 Payer ID': original_row.get('Ins1 Payer ID', ''),
|
341
|
+
'Service Date': first_service.get('service_date_display', ''),
|
342
|
+
'status': 'Ready'
|
343
|
+
}
|
344
|
+
display_data.append(display_row)
|
345
|
+
|
346
|
+
display_enhanced_deductible_table(display_data, context="pre_api")
|
238
347
|
|
239
348
|
# Function to handle manual patient deductible lookup
|
240
349
|
def manual_deductible_lookup():
|
@@ -256,7 +365,7 @@ def manual_deductible_lookup():
|
|
256
365
|
print("Returning to main menu.\n")
|
257
366
|
break
|
258
367
|
|
259
|
-
formatted_dob =
|
368
|
+
formatted_dob = _fallback_validate_and_format_date(dob_input)
|
260
369
|
if not formatted_dob:
|
261
370
|
print("Invalid DOB format. Please enter in YYYY-MM-DD format.\n")
|
262
371
|
continue
|
@@ -274,6 +383,56 @@ def manual_deductible_lookup():
|
|
274
383
|
eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, formatted_dob, member_id, npi, run_validation=run_validation, is_manual_lookup=True)
|
275
384
|
if eligibility_data:
|
276
385
|
found_data = True
|
386
|
+
|
387
|
+
# Convert to enhanced format and display
|
388
|
+
# Check if we already have processed data (from merge_responses in debug mode)
|
389
|
+
if isinstance(eligibility_data, dict) and 'patient_name' in eligibility_data and 'data_source' in eligibility_data:
|
390
|
+
# Already processed data from merge_responses
|
391
|
+
enhanced_result = eligibility_data
|
392
|
+
elif convert_eligibility_to_enhanced_format is not None:
|
393
|
+
# Raw API data needs conversion with patient info
|
394
|
+
enhanced_result = convert_eligibility_to_enhanced_format(eligibility_data, formatted_dob, member_id, "", "")
|
395
|
+
else:
|
396
|
+
# Fallback if utility function not available
|
397
|
+
enhanced_result = None
|
398
|
+
if enhanced_result:
|
399
|
+
try:
|
400
|
+
# Backfill with CSV row data when available
|
401
|
+
enhanced_result = backfill_enhanced_result(enhanced_result, None)
|
402
|
+
except Exception:
|
403
|
+
pass
|
404
|
+
print("\n" + "=" * 60)
|
405
|
+
display_enhanced_deductible_table([enhanced_result], context="post_api",
|
406
|
+
title="Manual Lookup Result")
|
407
|
+
print("=" * 60)
|
408
|
+
|
409
|
+
# Enhanced manual lookup result display
|
410
|
+
print("\n" + "=" * 60)
|
411
|
+
print("MANUAL LOOKUP RESULT")
|
412
|
+
print("=" * 60)
|
413
|
+
print("Patient Name: {}".format(enhanced_result['patient_name']))
|
414
|
+
print("Member ID: {}".format(enhanced_result['member_id']))
|
415
|
+
print("Date of Birth: {}".format(enhanced_result['dob']))
|
416
|
+
print("Payer ID: {}".format(enhanced_result['payer_id']))
|
417
|
+
print("Insurance Type: {}".format(enhanced_result['insurance_type']))
|
418
|
+
print("Policy Status: {}".format(enhanced_result['policy_status']))
|
419
|
+
print("Remaining Deductible: {}".format(enhanced_result['remaining_amount']))
|
420
|
+
print("=" * 60)
|
421
|
+
else:
|
422
|
+
# Fallback display when enhanced_result is None
|
423
|
+
print("\n" + "=" * 60)
|
424
|
+
print("MANUAL LOOKUP RESULT")
|
425
|
+
print("=" * 60)
|
426
|
+
print("Patient Name: Not Available")
|
427
|
+
print("Member ID: {}".format(member_id))
|
428
|
+
print("Date of Birth: {}".format(formatted_dob))
|
429
|
+
print("Payer ID: {}".format(payer_id))
|
430
|
+
print("Insurance Type: Not Available")
|
431
|
+
print("Policy Status: Not Available")
|
432
|
+
print("Remaining Deductible: Not Available")
|
433
|
+
print("Note: Data conversion failed - raw API response available")
|
434
|
+
print("=" * 60)
|
435
|
+
|
277
436
|
# Generate unique output file for manual request
|
278
437
|
output_file_name = "eligibility_report_manual_{}_{}.txt".format(member_id, formatted_dob)
|
279
438
|
output_file_path = os.path.join(os.getenv('TEMP'), output_file_name)
|
@@ -282,8 +441,6 @@ def manual_deductible_lookup():
|
|
282
441
|
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
283
442
|
output_file.write(table_header + "\n")
|
284
443
|
output_file.write("-" * len(table_header) + "\n")
|
285
|
-
print(table_header)
|
286
|
-
print("-" * len(table_header))
|
287
444
|
display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
|
288
445
|
|
289
446
|
# Ask if user wants to open the report
|
@@ -305,7 +462,10 @@ def manual_deductible_lookup():
|
|
305
462
|
|
306
463
|
|
307
464
|
# Function to get eligibility information
|
308
|
-
def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, member_id, npi, run_validation=False, is_manual_lookup=False):
|
465
|
+
def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, member_id, npi, run_validation=False, is_manual_lookup=False, printed_messages=None):
|
466
|
+
if printed_messages is None:
|
467
|
+
printed_messages = set()
|
468
|
+
|
309
469
|
try:
|
310
470
|
# Log the parameters being sent to the function
|
311
471
|
MediLink_ConfigLoader.log("Calling eligibility check with parameters:", level="DEBUG")
|
@@ -322,9 +482,22 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
322
482
|
|
323
483
|
# Get legacy response
|
324
484
|
MediLink_ConfigLoader.log("Getting legacy get_eligibility_v3 API response", level="INFO")
|
325
|
-
|
326
|
-
|
327
|
-
)
|
485
|
+
|
486
|
+
legacy_eligibility = None
|
487
|
+
if client and hasattr(client, 'get_access_token'):
|
488
|
+
try:
|
489
|
+
# Try to get access token for UHCAPI endpoint
|
490
|
+
access_token = client.get_access_token('UHCAPI')
|
491
|
+
if access_token:
|
492
|
+
legacy_eligibility = api_core.get_eligibility_v3(
|
493
|
+
client, payer_id, provider_last_name, 'MemberIDDateOfBirth', date_of_birth, member_id, npi
|
494
|
+
)
|
495
|
+
else:
|
496
|
+
MediLink_ConfigLoader.log("No access token available for Legacy API (UHCAPI endpoint). Check configuration.", level="WARNING")
|
497
|
+
except Exception as e:
|
498
|
+
MediLink_ConfigLoader.log("Failed to get access token for Legacy API: {}".format(e), level="ERROR")
|
499
|
+
else:
|
500
|
+
MediLink_ConfigLoader.log("API client does not support token authentication for Legacy API.", level="WARNING")
|
328
501
|
|
329
502
|
# Get Super Connector response for comparison
|
330
503
|
MediLink_ConfigLoader.log("Getting new get_eligibility_super_connector API response", level="INFO")
|
@@ -368,11 +541,17 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
368
541
|
raw_response = super_connector_eligibility.get('rawGraphQLResponse', {})
|
369
542
|
errors = raw_response.get('errors', [])
|
370
543
|
if errors:
|
371
|
-
|
544
|
+
error_msg = "Super Connector API returned {} error(s):".format(len(errors))
|
545
|
+
if error_msg not in printed_messages:
|
546
|
+
print(error_msg)
|
547
|
+
printed_messages.add(error_msg)
|
372
548
|
for i, error in enumerate(errors):
|
373
549
|
error_code = error.get('code', 'UNKNOWN')
|
374
550
|
error_desc = error.get('description', 'No description')
|
375
|
-
|
551
|
+
detail_msg = " Error {}: {} - {}".format(i+1, error_code, error_desc)
|
552
|
+
if detail_msg not in printed_messages:
|
553
|
+
print(detail_msg)
|
554
|
+
printed_messages.add(detail_msg)
|
376
555
|
|
377
556
|
# Check for data in error extensions (some APIs return data here)
|
378
557
|
extensions = error.get('extensions', {})
|
@@ -403,23 +582,52 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
403
582
|
except Exception as e:
|
404
583
|
print("\nError generating validation report: {}".format(str(e)))
|
405
584
|
|
406
|
-
#
|
407
|
-
|
585
|
+
# After validation, merge responses
|
586
|
+
try:
|
587
|
+
merged_data = merge_responses(super_connector_eligibility, legacy_eligibility, date_of_birth, member_id)
|
588
|
+
return merged_data
|
589
|
+
except Exception as e:
|
590
|
+
MediLink_ConfigLoader.log("Error in merge_responses: {}".format(e), level="ERROR")
|
591
|
+
# Return a safe fallback result
|
592
|
+
return {
|
593
|
+
'patient_name': 'Unknown Patient',
|
594
|
+
'dob': date_of_birth,
|
595
|
+
'member_id': member_id,
|
596
|
+
'insurance_type': 'Not Available',
|
597
|
+
'policy_status': 'Not Available',
|
598
|
+
'remaining_amount': 'Not Found',
|
599
|
+
'data_source': 'Error',
|
600
|
+
'is_successful': False
|
601
|
+
}
|
408
602
|
|
409
603
|
else:
|
410
604
|
# Legacy mode: Only call legacy API
|
411
605
|
MediLink_ConfigLoader.log("Running in LEGACY MODE - calling legacy API only", level="INFO")
|
412
606
|
|
413
|
-
# Only get legacy response
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
607
|
+
# Only get legacy response with proper token handling
|
608
|
+
if client and hasattr(client, 'get_access_token'):
|
609
|
+
try:
|
610
|
+
access_token = client.get_access_token('UHCAPI')
|
611
|
+
if access_token:
|
612
|
+
eligibility = api_core.get_eligibility_v3(
|
613
|
+
client, payer_id, provider_last_name, 'MemberIDDateOfBirth', date_of_birth, member_id, npi
|
614
|
+
)
|
615
|
+
else:
|
616
|
+
MediLink_ConfigLoader.log("No access token available for Legacy API in Legacy mode.", level="WARNING")
|
617
|
+
return None
|
618
|
+
except Exception as e:
|
619
|
+
MediLink_ConfigLoader.log("Failed to get access token for Legacy API in Legacy mode: {}".format(e), level="ERROR")
|
620
|
+
return None
|
621
|
+
else:
|
622
|
+
MediLink_ConfigLoader.log("API client does not support token authentication for Legacy API in Legacy mode.", level="WARNING")
|
623
|
+
return None
|
418
624
|
|
419
625
|
# Log the response
|
420
|
-
|
421
|
-
|
422
|
-
|
626
|
+
if 'eligibility' in locals():
|
627
|
+
MediLink_ConfigLoader.log("Eligibility response: {}".format(json.dumps(eligibility, indent=4)), level="DEBUG")
|
628
|
+
return eligibility
|
629
|
+
else:
|
630
|
+
return None
|
423
631
|
except Exception as e:
|
424
632
|
# Handle HTTP errors if requests is available
|
425
633
|
if requests and hasattr(requests, 'exceptions') and isinstance(e, requests.exceptions.HTTPError):
|
@@ -432,433 +640,64 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
432
640
|
print("Eligibility Check Error: {}".format(e))
|
433
641
|
return None
|
434
642
|
|
435
|
-
#
|
436
|
-
#
|
437
|
-
# PROBLEM: API responses are returning correctly but the parser functions below
|
438
|
-
# are not successfully extracting the super_connector variables (likely eligibility data).
|
439
|
-
# This suggests a schema mismatch between expected and actual API response format.
|
440
|
-
#
|
441
|
-
# IMPLEMENTATION CLARIFICATION:
|
442
|
-
# - Primary path should not depend on probing payer_ids via API.
|
443
|
-
# - Prefer payer_id provided by CSV/crosswalk as the authoritative source.
|
444
|
-
# - Keep API probing behind a non-default debug flag to support troubleshooting sessions only.
|
445
|
-
# - Add detailed logging helpers (no-op in production) to inspect mismatches safely on XP.
|
446
|
-
#
|
447
|
-
# DEBUGGING STEPS:
|
448
|
-
# 1. Response Structure Analysis:
|
449
|
-
# - Add comprehensive logging of raw API responses before parsing
|
450
|
-
# - Compare current response format vs expected format in parser functions
|
451
|
-
# - Check if API endpoint has changed response schema recently
|
452
|
-
# - Verify if different endpoints return different response structures
|
453
|
-
#
|
454
|
-
# 2. Parser Function Validation:
|
455
|
-
# - Test each extract_*_patient_info() function with sample responses
|
456
|
-
# - Check if field names/paths have changed (e.g., 'patientInfo' vs 'patient_info')
|
457
|
-
# - Verify array indexing logic (e.g., [0] access on empty arrays)
|
458
|
-
# - Check case sensitivity in field access
|
459
|
-
#
|
460
|
-
# 3. Super Connector Variable Mapping:
|
461
|
-
# - Document what "super_connector variables" should contain
|
462
|
-
# - Identify which fields from API response map to these variables
|
463
|
-
# - Verify the expected format vs actual format
|
464
|
-
# - Check if variable names have changed in the application
|
465
|
-
#
|
466
|
-
# IMPLEMENTATION PLAN:
|
467
|
-
# 1. Enhanced Logging:
|
468
|
-
# - Add log_api_response_structure(response) function
|
469
|
-
# - Log raw JSON before each parser function call
|
470
|
-
# - Add field-by-field parsing logs with null checks
|
471
|
-
#
|
472
|
-
# 2. Parser Robustness:
|
473
|
-
# - Add null/empty checks for all field accesses
|
474
|
-
# - Implement graceful fallbacks for missing fields
|
475
|
-
# - Add validation for expected data types
|
476
|
-
# - Handle both old and new response formats if schema changed
|
477
|
-
#
|
478
|
-
# 3. Schema Validation:
|
479
|
-
# - Create validate_api_response_schema(response, expected_schema) function
|
480
|
-
# - Define expected schemas for each API endpoint
|
481
|
-
# - Alert when response doesn't match expected schema
|
482
|
-
# - Suggest schema updates when mismatches occur
|
483
|
-
#
|
484
|
-
# 4. Testing Framework:
|
485
|
-
# - Create test cases with known good API responses
|
486
|
-
# - Test parser functions independently of API calls
|
487
|
-
# - Add integration tests for end-to-end parsing workflow
|
488
|
-
# - Create mock responses for development testing
|
643
|
+
# API response parsing functions moved to MediCafe.deductible_utils
|
644
|
+
# All parsing logic is now centralized in the utility module for DRY compliance
|
489
645
|
#
|
490
|
-
#
|
491
|
-
#
|
492
|
-
#
|
493
|
-
#
|
494
|
-
#
|
646
|
+
# TODO (API DEVELOPER FIX REQUIRED):
|
647
|
+
# The following issues from the original commentary still need to be addressed:
|
648
|
+
# 1. Complete Super Connector API response schema - API developers are working on this
|
649
|
+
# 2. Full response structure validation - depends on stable API response structure
|
650
|
+
# 3. Comprehensive test cases - requires consistent API responses
|
495
651
|
#
|
496
|
-
#
|
497
|
-
#
|
498
|
-
#
|
499
|
-
#
|
652
|
+
# CURRENT STATUS:
|
653
|
+
# Enhanced logging and debugging capabilities implemented
|
654
|
+
# Schema validation framework in place
|
655
|
+
# Compatibility analysis functions added
|
656
|
+
# Robust fallback mechanisms implemented
|
657
|
+
# Complete API response schema validation (pending API fix)
|
658
|
+
# Comprehensive test suite (pending stable API responses)
|
500
659
|
#
|
501
|
-
#
|
502
|
-
# -
|
503
|
-
# -
|
504
|
-
# -
|
505
|
-
|
506
|
-
def extract_legacy_patient_info(policy):
|
507
|
-
"""Extract patient information from legacy API response format"""
|
508
|
-
patient_info = policy.get("patientInfo", [{}])[0]
|
509
|
-
return {
|
510
|
-
'lastName': patient_info.get("lastName", ""),
|
511
|
-
'firstName': patient_info.get("firstName", ""),
|
512
|
-
'middleName': patient_info.get("middleName", "")
|
513
|
-
}
|
514
|
-
|
515
|
-
def extract_super_connector_patient_info(eligibility_data):
|
516
|
-
"""Extract patient information from Super Connector API response format"""
|
517
|
-
if not eligibility_data:
|
518
|
-
return {'lastName': '', 'firstName': '', 'middleName': ''}
|
519
|
-
|
520
|
-
# Handle multiple eligibility records - use the first one with valid data
|
521
|
-
if "rawGraphQLResponse" in eligibility_data:
|
522
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
523
|
-
data = raw_response.get('data', {})
|
524
|
-
check_eligibility = data.get('checkEligibility', {})
|
525
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
526
|
-
|
527
|
-
# Try to get from the first eligibility record
|
528
|
-
if eligibility_list:
|
529
|
-
first_eligibility = eligibility_list[0]
|
530
|
-
member_info = first_eligibility.get('eligibilityInfo', {}).get('member', {})
|
531
|
-
if member_info:
|
532
|
-
return {
|
533
|
-
'lastName': member_info.get("lastName", ""),
|
534
|
-
'firstName': member_info.get("firstName", ""),
|
535
|
-
'middleName': member_info.get("middleName", "")
|
536
|
-
}
|
537
|
-
|
538
|
-
# Check for data in error extensions (some APIs return data here despite errors)
|
539
|
-
errors = raw_response.get('errors', [])
|
540
|
-
for error in errors:
|
541
|
-
extensions = error.get('extensions', {})
|
542
|
-
if extensions and 'details' in extensions:
|
543
|
-
details = extensions.get('details', [])
|
544
|
-
if details:
|
545
|
-
# Use the first detail record that has patient info
|
546
|
-
for detail in details:
|
547
|
-
if detail.get('lastName') or detail.get('firstName'):
|
548
|
-
return {
|
549
|
-
'lastName': detail.get("lastName", ""),
|
550
|
-
'firstName': detail.get("firstName", ""),
|
551
|
-
'middleName': detail.get("middleName", "")
|
552
|
-
}
|
553
|
-
|
554
|
-
# Fallback to top-level fields
|
555
|
-
return {
|
556
|
-
'lastName': eligibility_data.get("lastName", ""),
|
557
|
-
'firstName': eligibility_data.get("firstName", ""),
|
558
|
-
'middleName': eligibility_data.get("middleName", "")
|
559
|
-
}
|
560
|
-
|
561
|
-
def extract_legacy_remaining_amount(policy):
|
562
|
-
"""Extract remaining amount from legacy API response format"""
|
563
|
-
deductible_info = policy.get("deductibleInfo", {})
|
564
|
-
if 'individual' in deductible_info:
|
565
|
-
remaining = deductible_info['individual']['inNetwork'].get("remainingAmount", "")
|
566
|
-
return remaining if remaining else "Not Found"
|
567
|
-
elif 'family' in deductible_info:
|
568
|
-
remaining = deductible_info['family']['inNetwork'].get("remainingAmount", "")
|
569
|
-
return remaining if remaining else "Not Found"
|
570
|
-
else:
|
571
|
-
return "Not Found"
|
572
|
-
|
573
|
-
def extract_super_connector_remaining_amount(eligibility_data):
|
574
|
-
"""Extract remaining amount from Super Connector API response format"""
|
575
|
-
if not eligibility_data:
|
576
|
-
return "Not Found"
|
577
|
-
|
578
|
-
# First, check top-level metYearToDateAmount which might indicate deductible met
|
579
|
-
met_amount = eligibility_data.get('metYearToDateAmount')
|
580
|
-
if met_amount is not None:
|
581
|
-
return str(met_amount)
|
582
|
-
|
583
|
-
# Collect all deductible amounts to find the most relevant one
|
584
|
-
all_deductible_amounts = []
|
585
|
-
|
586
|
-
# Look for deductible information in planLevels (based on validation report)
|
587
|
-
plan_levels = eligibility_data.get('planLevels', [])
|
588
|
-
for plan_level in plan_levels:
|
589
|
-
if plan_level.get('level') == 'deductibleInfo':
|
590
|
-
# Collect individual deductible amounts
|
591
|
-
individual_levels = plan_level.get('individual', [])
|
592
|
-
if individual_levels:
|
593
|
-
for individual in individual_levels:
|
594
|
-
remaining = individual.get('remainingAmount')
|
595
|
-
if remaining is not None:
|
596
|
-
try:
|
597
|
-
amount = float(remaining)
|
598
|
-
all_deductible_amounts.append(('individual', amount))
|
599
|
-
except (ValueError, TypeError):
|
600
|
-
pass
|
601
|
-
|
602
|
-
# Collect family deductible amounts
|
603
|
-
family_levels = plan_level.get('family', [])
|
604
|
-
if family_levels:
|
605
|
-
for family in family_levels:
|
606
|
-
remaining = family.get('remainingAmount')
|
607
|
-
if remaining is not None:
|
608
|
-
try:
|
609
|
-
amount = float(remaining)
|
610
|
-
all_deductible_amounts.append(('family', amount))
|
611
|
-
except (ValueError, TypeError):
|
612
|
-
pass
|
613
|
-
|
614
|
-
# Navigate to the rawGraphQLResponse structure as fallback
|
615
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
616
|
-
if raw_response:
|
617
|
-
data = raw_response.get('data', {})
|
618
|
-
check_eligibility = data.get('checkEligibility', {})
|
619
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
620
|
-
|
621
|
-
# Try all eligibility records for deductible information
|
622
|
-
for eligibility in eligibility_list:
|
623
|
-
plan_levels = eligibility.get('eligibilityInfo', {}).get('planLevels', [])
|
624
|
-
for plan_level in plan_levels:
|
625
|
-
if plan_level.get('level') == 'deductibleInfo':
|
626
|
-
# Collect individual deductible amounts
|
627
|
-
individual_levels = plan_level.get('individual', [])
|
628
|
-
if individual_levels:
|
629
|
-
for individual in individual_levels:
|
630
|
-
remaining = individual.get('remainingAmount')
|
631
|
-
if remaining is not None:
|
632
|
-
try:
|
633
|
-
amount = float(remaining)
|
634
|
-
all_deductible_amounts.append(('individual', amount))
|
635
|
-
except (ValueError, TypeError):
|
636
|
-
pass
|
637
|
-
|
638
|
-
# Collect family deductible amounts
|
639
|
-
family_levels = plan_level.get('family', [])
|
640
|
-
if family_levels:
|
641
|
-
for family in family_levels:
|
642
|
-
remaining = family.get('remainingAmount')
|
643
|
-
if remaining is not None:
|
644
|
-
try:
|
645
|
-
amount = float(remaining)
|
646
|
-
all_deductible_amounts.append(('family', amount))
|
647
|
-
except (ValueError, TypeError):
|
648
|
-
pass
|
649
|
-
|
650
|
-
# Select the most relevant deductible amount
|
651
|
-
if all_deductible_amounts:
|
652
|
-
# Strategy: Prefer individual over family, and prefer non-zero amounts
|
653
|
-
# First, try to find non-zero individual amounts
|
654
|
-
non_zero_individual = [amt for type_, amt in all_deductible_amounts if type_ == 'individual' and amt > 0]
|
655
|
-
if non_zero_individual:
|
656
|
-
return str(max(non_zero_individual)) # Return highest non-zero individual amount
|
657
|
-
|
658
|
-
# If no non-zero individual, try non-zero family amounts
|
659
|
-
non_zero_family = [amt for type_, amt in all_deductible_amounts if type_ == 'family' and amt > 0]
|
660
|
-
if non_zero_family:
|
661
|
-
return str(max(non_zero_family)) # Return highest non-zero family amount
|
662
|
-
|
663
|
-
# If all amounts are zero, return the first individual amount (or family if no individual)
|
664
|
-
individual_amounts = [amt for type_, amt in all_deductible_amounts if type_ == 'individual']
|
665
|
-
if individual_amounts:
|
666
|
-
return str(individual_amounts[0])
|
667
|
-
|
668
|
-
# Fallback to first family amount
|
669
|
-
family_amounts = [amt for type_, amt in all_deductible_amounts if type_ == 'family']
|
670
|
-
if family_amounts:
|
671
|
-
return str(family_amounts[0])
|
672
|
-
|
673
|
-
return "Not Found"
|
674
|
-
|
675
|
-
def extract_legacy_insurance_info(policy):
|
676
|
-
"""Extract insurance information from legacy API response format"""
|
677
|
-
insurance_info = policy.get("insuranceInfo", {})
|
678
|
-
return {
|
679
|
-
'insuranceType': insurance_info.get("insuranceType", ""),
|
680
|
-
'insuranceTypeCode': insurance_info.get("insuranceTypeCode", ""),
|
681
|
-
'memberId': insurance_info.get("memberId", ""),
|
682
|
-
'payerId': insurance_info.get("payerId", "")
|
683
|
-
}
|
684
|
-
|
685
|
-
def extract_super_connector_insurance_info(eligibility_data):
|
686
|
-
"""Extract insurance information from Super Connector API response format"""
|
687
|
-
if not eligibility_data:
|
688
|
-
return {'insuranceType': '', 'insuranceTypeCode': '', 'memberId': '', 'payerId': ''}
|
689
|
-
|
690
|
-
# Handle multiple eligibility records - use the first one with valid data
|
691
|
-
if "rawGraphQLResponse" in eligibility_data:
|
692
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
693
|
-
data = raw_response.get('data', {})
|
694
|
-
check_eligibility = data.get('checkEligibility', {})
|
695
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
696
|
-
|
697
|
-
# Try to get from the first eligibility record
|
698
|
-
if eligibility_list:
|
699
|
-
first_eligibility = eligibility_list[0]
|
700
|
-
insurance_info = first_eligibility.get('eligibilityInfo', {}).get('insuranceInfo', {})
|
701
|
-
if insurance_info:
|
702
|
-
return {
|
703
|
-
'insuranceType': insurance_info.get("planTypeDescription", ""),
|
704
|
-
'insuranceTypeCode': insurance_info.get("productServiceCode", ""),
|
705
|
-
'memberId': insurance_info.get("memberId", ""),
|
706
|
-
'payerId': insurance_info.get("payerId", "")
|
707
|
-
}
|
708
|
-
|
709
|
-
# Check for data in error extensions (some APIs return data here despite errors)
|
710
|
-
errors = raw_response.get('errors', [])
|
711
|
-
for error in errors:
|
712
|
-
extensions = error.get('extensions', {})
|
713
|
-
if extensions and 'details' in extensions:
|
714
|
-
details = extensions.get('details', [])
|
715
|
-
if details:
|
716
|
-
# Use the first detail record that has insurance info
|
717
|
-
for detail in details:
|
718
|
-
if detail.get('memberId') or detail.get('payerId'):
|
719
|
-
# Try to determine insurance type from available data
|
720
|
-
insurance_type = detail.get('planType', '')
|
721
|
-
if not insurance_type:
|
722
|
-
insurance_type = detail.get('productType', '')
|
723
|
-
|
724
|
-
return {
|
725
|
-
'insuranceType': insurance_type,
|
726
|
-
'insuranceTypeCode': detail.get("productServiceCode", ""),
|
727
|
-
'memberId': detail.get("memberId", ""),
|
728
|
-
'payerId': detail.get("payerId", "")
|
729
|
-
}
|
730
|
-
|
731
|
-
# Fallback to top-level fields
|
732
|
-
insurance_type = eligibility_data.get("planTypeDescription", "")
|
733
|
-
if not insurance_type:
|
734
|
-
insurance_type = eligibility_data.get("productType", "")
|
735
|
-
|
736
|
-
# Clean up the insurance type if it's too long (like the LPPO description)
|
737
|
-
if insurance_type and len(insurance_type) > 50:
|
738
|
-
# Extract just the plan type part
|
739
|
-
if "PPO" in insurance_type:
|
740
|
-
insurance_type = "Preferred Provider Organization (PPO)"
|
741
|
-
elif "HMO" in insurance_type:
|
742
|
-
insurance_type = "Health Maintenance Organization (HMO)"
|
743
|
-
elif "EPO" in insurance_type:
|
744
|
-
insurance_type = "Exclusive Provider Organization (EPO)"
|
745
|
-
elif "POS" in insurance_type:
|
746
|
-
insurance_type = "Point of Service (POS)"
|
747
|
-
|
748
|
-
# Get insurance type code from multiple possible locations
|
749
|
-
insurance_type_code = eligibility_data.get("productServiceCode", "")
|
750
|
-
if not insurance_type_code:
|
751
|
-
# Try to get from coverageTypes
|
752
|
-
coverage_types = eligibility_data.get("coverageTypes", [])
|
753
|
-
if coverage_types:
|
754
|
-
insurance_type_code = coverage_types[0].get("typeCode", "")
|
755
|
-
|
756
|
-
# Note: We're not mapping "M" to "PR" as "M" likely means "Medical"
|
757
|
-
# and "PR" should be "12" for PPO according to CMS standards
|
758
|
-
# This mapping should be handled by the API developers
|
759
|
-
|
760
|
-
return {
|
761
|
-
'insuranceType': insurance_type,
|
762
|
-
'insuranceTypeCode': insurance_type_code,
|
763
|
-
'memberId': eligibility_data.get("subscriberId", ""),
|
764
|
-
'payerId': eligibility_data.get("payerId", "") # Use payerId instead of legalEntityCode (this should be payer_id from the inputs)
|
765
|
-
}
|
766
|
-
|
767
|
-
def extract_legacy_policy_status(policy):
|
768
|
-
"""Extract policy status from legacy API response format"""
|
769
|
-
policy_info = policy.get("policyInfo", {})
|
770
|
-
return policy_info.get("policyStatus", "")
|
771
|
-
|
772
|
-
def extract_super_connector_policy_status(eligibility_data):
|
773
|
-
"""Extract policy status from Super Connector API response format"""
|
774
|
-
if not eligibility_data:
|
775
|
-
return ""
|
776
|
-
|
777
|
-
# Handle multiple eligibility records - use the first one with valid data
|
778
|
-
if "rawGraphQLResponse" in eligibility_data:
|
779
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
780
|
-
data = raw_response.get('data', {})
|
781
|
-
check_eligibility = data.get('checkEligibility', {})
|
782
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
783
|
-
|
784
|
-
# Try to get from the first eligibility record
|
785
|
-
if eligibility_list:
|
786
|
-
first_eligibility = eligibility_list[0]
|
787
|
-
insurance_info = first_eligibility.get('eligibilityInfo', {}).get('insuranceInfo', {})
|
788
|
-
if insurance_info:
|
789
|
-
return insurance_info.get("policyStatus", "")
|
790
|
-
|
791
|
-
# Fallback to top-level field
|
792
|
-
return eligibility_data.get("policyStatus", "")
|
793
|
-
|
794
|
-
def is_legacy_response_format(data):
|
795
|
-
"""Determine if the response is in legacy format (has memberPolicies)"""
|
796
|
-
return data is not None and "memberPolicies" in data
|
797
|
-
|
798
|
-
def is_super_connector_response_format(data):
|
799
|
-
"""Determine if the response is in Super Connector format (has rawGraphQLResponse)"""
|
800
|
-
return data is not None and "rawGraphQLResponse" in data
|
660
|
+
# NEXT STEPS:
|
661
|
+
# - Monitor API developer progress on Super Connector schema fixes
|
662
|
+
# - Update schema validation once API responses are stable
|
663
|
+
# - Create comprehensive test cases with known good responses
|
664
|
+
# - Consider adding automated schema detection for new API versions
|
801
665
|
|
802
666
|
# Function to extract required fields and display in a tabular format
|
803
|
-
def display_eligibility_info(data, dob, member_id, output_file):
|
667
|
+
def display_eligibility_info(data, dob, member_id, output_file, patient_id="", service_date=""):
|
668
|
+
"""Legacy display function - converts to enhanced format and displays"""
|
804
669
|
if data is None:
|
805
670
|
return
|
806
671
|
|
807
|
-
#
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
# Skip non-medical policies
|
812
|
-
if policy.get("policyInfo", {}).get("coverageType", "") != "Medical":
|
813
|
-
continue
|
814
|
-
|
815
|
-
patient_info = extract_legacy_patient_info(policy)
|
816
|
-
remaining_amount = extract_legacy_remaining_amount(policy)
|
817
|
-
insurance_info = extract_legacy_insurance_info(policy)
|
818
|
-
policy_status = extract_legacy_policy_status(policy)
|
819
|
-
|
820
|
-
patient_name = "{} {} {}".format(
|
821
|
-
patient_info['firstName'],
|
822
|
-
patient_info['middleName'],
|
823
|
-
patient_info['lastName']
|
824
|
-
).strip()[:20]
|
825
|
-
|
826
|
-
# Display patient information in a table row format
|
827
|
-
table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
828
|
-
patient_name, dob, insurance_info['insuranceType'],
|
829
|
-
insurance_info['payerId'], policy_status, remaining_amount)
|
830
|
-
output_file.write(table_row + "\n")
|
831
|
-
print(table_row) # Print to console for progressive display
|
832
|
-
|
833
|
-
elif is_super_connector_response_format(data):
|
834
|
-
# Handle Super Connector API response format
|
835
|
-
patient_info = extract_super_connector_patient_info(data)
|
836
|
-
remaining_amount = extract_super_connector_remaining_amount(data)
|
837
|
-
insurance_info = extract_super_connector_insurance_info(data)
|
838
|
-
policy_status = extract_super_connector_policy_status(data)
|
839
|
-
|
840
|
-
patient_name = "{} {} {}".format(
|
841
|
-
patient_info['firstName'],
|
842
|
-
patient_info['middleName'],
|
843
|
-
patient_info['lastName']
|
844
|
-
).strip()[:20]
|
845
|
-
|
846
|
-
# Display patient information in a table row format
|
672
|
+
# Convert to enhanced format
|
673
|
+
enhanced_data = convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id, service_date)
|
674
|
+
if enhanced_data:
|
675
|
+
# Write to output file in legacy format for compatibility
|
847
676
|
table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
848
|
-
|
849
|
-
|
677
|
+
enhanced_data['patient_name'][:20],
|
678
|
+
enhanced_data['dob'],
|
679
|
+
enhanced_data['insurance_type'][:40],
|
680
|
+
enhanced_data['payer_id'][:5],
|
681
|
+
enhanced_data['policy_status'][:14],
|
682
|
+
enhanced_data['remaining_amount'][:14])
|
850
683
|
output_file.write(table_row + "\n")
|
851
684
|
print(table_row) # Print to console for progressive display
|
852
685
|
|
853
|
-
else:
|
854
|
-
# Unknown response format - log for debugging
|
855
|
-
MediLink_ConfigLoader.log("Unknown response format in display_eligibility_info", level="WARNING")
|
856
|
-
MediLink_ConfigLoader.log("Response structure: {}".format(json.dumps(data, indent=2)), level="DEBUG")
|
857
|
-
|
858
686
|
# Global mode flags (will be set in main)
|
859
687
|
LEGACY_MODE = False
|
860
688
|
DEBUG_MODE = False
|
861
689
|
|
690
|
+
# PERFORMANCE OPTIMIZATION: Feature toggle for payer ID probing
|
691
|
+
# When False (default): Use crosswalk-based resolution (O(N) complexity)
|
692
|
+
# When True: Use multi-payer probing (O(PxN) complexity) for troubleshooting only
|
693
|
+
DEBUG_MODE_PAYER_PROBE = False
|
694
|
+
|
695
|
+
# Crosswalk-based payer ID resolution cache
|
696
|
+
_payer_id_cache = None
|
697
|
+
|
698
|
+
# Payer ID resolution functions moved to MediCafe.deductible_utils
|
699
|
+
# All resolution logic is now centralized in the utility module for DRY compliance
|
700
|
+
|
862
701
|
# Main Execution Flow
|
863
702
|
if __name__ == "__main__":
|
864
703
|
print("\n" + "=" * 80)
|
@@ -871,33 +710,43 @@ if __name__ == "__main__":
|
|
871
710
|
print("\nSelect operation mode:")
|
872
711
|
print("1. Legacy Mode (Default) - Single API calls, consolidated output")
|
873
712
|
print("2. Debug Mode - Dual API calls with validation reports")
|
874
|
-
print("3.
|
713
|
+
print("3. Payer Probe Debug Mode - Multi-payer probing for troubleshooting")
|
714
|
+
print("4. Exit")
|
875
715
|
|
876
|
-
mode_choice = input("\nEnter your choice (1-
|
716
|
+
mode_choice = input("\nEnter your choice (1-4) [Default: 1]: ").strip()
|
877
717
|
if not mode_choice:
|
878
718
|
mode_choice = "1"
|
879
719
|
|
880
|
-
if mode_choice == "
|
720
|
+
if mode_choice == "4":
|
881
721
|
print("\nExiting. Thank you for using MediLink Deductible Tool!")
|
882
722
|
sys.exit(0)
|
883
|
-
elif mode_choice not in ["1", "2"]:
|
723
|
+
elif mode_choice not in ["1", "2", "3"]:
|
884
724
|
print("Invalid choice. Using Legacy Mode (Default).")
|
885
725
|
mode_choice = "1"
|
886
726
|
|
887
727
|
# Set mode flags
|
888
728
|
LEGACY_MODE = (mode_choice == "1")
|
889
729
|
DEBUG_MODE = (mode_choice == "2")
|
730
|
+
DEBUG_MODE_PAYER_PROBE = (mode_choice == "3")
|
890
731
|
|
891
732
|
if LEGACY_MODE:
|
892
733
|
print("\nRunning in LEGACY MODE")
|
893
734
|
print("- Single API calls (Legacy API only)")
|
894
735
|
print("- Progressive output during processing")
|
895
736
|
print("- Consolidated output file at the end")
|
896
|
-
|
737
|
+
print("- Crosswalk-based payer ID resolution (O(N) complexity)")
|
738
|
+
elif DEBUG_MODE:
|
897
739
|
print("\nRunning in DEBUG MODE")
|
898
740
|
print("- Dual API calls (Legacy + Super Connector)")
|
899
741
|
print("- Validation reports and comparisons")
|
900
742
|
print("- Detailed logging and error reporting")
|
743
|
+
print("- Crosswalk-based payer ID resolution (O(N) complexity)")
|
744
|
+
else:
|
745
|
+
print("\nRunning in PAYER PROBE DEBUG MODE")
|
746
|
+
print("- Multi-payer probing for troubleshooting")
|
747
|
+
print("- Original O(PxN) complexity algorithm")
|
748
|
+
print("- Use only for diagnostic sessions")
|
749
|
+
print("- Not recommended for production use")
|
901
750
|
|
902
751
|
while True:
|
903
752
|
print("\nChoose an option:")
|
@@ -928,70 +777,234 @@ if __name__ == "__main__":
|
|
928
777
|
print("Batch processing cancelled.")
|
929
778
|
continue
|
930
779
|
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
780
|
+
# PERFORMANCE OPTIMIZATION: Crosswalk-based payer ID resolution
|
781
|
+
# This eliminates O(PxN) complexity by using CSV/crosswalk data as authoritative source
|
782
|
+
# Multi-payer probing is retained behind DEBUG_MODE_PAYER_PROBE toggle for troubleshooting
|
783
|
+
|
784
|
+
# Load crosswalk data for payer ID resolution
|
785
|
+
try:
|
786
|
+
_, crosswalk = _get_config()
|
787
|
+
except Exception as e:
|
788
|
+
MediLink_ConfigLoader.log("Failed to load crosswalk data: {}".format(e), level="WARNING")
|
789
|
+
crosswalk = {}
|
790
|
+
|
791
|
+
# Pre-resolve payer IDs for all patients (O(N) operation)
|
792
|
+
if not DEBUG_MODE_PAYER_PROBE:
|
793
|
+
if resolve_payer_ids_from_csv is not None:
|
794
|
+
_payer_id_cache = resolve_payer_ids_from_csv(csv_data, config, crosswalk, payer_ids)
|
795
|
+
print("Resolved {} patient-payer mappings from CSV data".format(len(_payer_id_cache)))
|
796
|
+
else:
|
797
|
+
# Fallback if utility function not available
|
798
|
+
_payer_id_cache = {}
|
799
|
+
print("Warning: Payer ID resolution utility not available, using empty cache")
|
800
|
+
|
801
|
+
errors = []
|
802
|
+
validation_reports = []
|
803
|
+
processed_count = 0
|
804
|
+
validation_files_created = [] # Track validation files that were actually created
|
805
|
+
eligibility_results = [] # Collect all results for enhanced display
|
806
|
+
printed_messages = set() # Initialize a set to track printed messages
|
939
807
|
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
# This would eliminate the need to try multiple payer_ids per patient and make this O(N).
|
945
|
-
# CLARIFICATION: In production, use the payer_id from the CSV/crosswalk as primary.
|
946
|
-
# Retain multi-payer probing behind a DEBUG/DIAGNOSTIC feature toggle only.
|
947
|
-
# Suggested flag: DEBUG_MODE_PAYER_PROBE = False (module-level), default False.
|
948
|
-
errors = []
|
949
|
-
validation_reports = []
|
950
|
-
processed_count = 0
|
951
|
-
validation_files_created = [] # Track validation files that were actually created
|
808
|
+
for dob, member_id in patients:
|
809
|
+
processed_count += 1
|
810
|
+
print("Processing patient {}/{}: Member ID {}, DOB {}".format(
|
811
|
+
processed_count, len(patients), member_id, dob))
|
952
812
|
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
processed_count, len(patients), member_id, dob))
|
957
|
-
|
958
|
-
# Try each payer_id for this patient until we get a successful response
|
813
|
+
# Get payer ID for this patient
|
814
|
+
if DEBUG_MODE_PAYER_PROBE:
|
815
|
+
# DEBUG MODE: Use multi-payer probing (original O(PxN) logic)
|
959
816
|
patient_processed = False
|
960
817
|
for payer_id in payer_ids:
|
961
818
|
try:
|
962
|
-
# Run with validation enabled only in debug mode
|
963
819
|
run_validation = DEBUG_MODE
|
964
|
-
eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False)
|
820
|
+
eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False, printed_messages=printed_messages)
|
965
821
|
if eligibility_data is not None:
|
966
|
-
|
822
|
+
# Check if we already have processed data (from merge_responses in debug mode)
|
823
|
+
if isinstance(eligibility_data, dict) and 'patient_name' in eligibility_data and 'data_source' in eligibility_data:
|
824
|
+
# Already processed data from merge_responses
|
825
|
+
enhanced_result = eligibility_data
|
826
|
+
elif convert_eligibility_to_enhanced_format is not None:
|
827
|
+
# Get patient info from CSV for this specific patient
|
828
|
+
patient_info = None
|
829
|
+
service_date = ""
|
830
|
+
patient_info = patient_row_index.get((dob, member_id))
|
831
|
+
if patient_info:
|
832
|
+
service_date = patient_info.get('Service Date', '')
|
833
|
+
|
834
|
+
# Raw API data needs conversion with patient info
|
835
|
+
enhanced_result = convert_eligibility_to_enhanced_format(
|
836
|
+
eligibility_data, dob, member_id,
|
837
|
+
patient_info.get('Patient ID', '') if patient_info else '',
|
838
|
+
service_date
|
839
|
+
)
|
840
|
+
else:
|
841
|
+
# Fallback if utility function not available
|
842
|
+
enhanced_result = None
|
843
|
+
if enhanced_result:
|
844
|
+
try:
|
845
|
+
enhanced_result = backfill_enhanced_result(enhanced_result, patient_info)
|
846
|
+
except Exception:
|
847
|
+
pass
|
848
|
+
eligibility_results.append(enhanced_result)
|
967
849
|
patient_processed = True
|
968
850
|
|
969
|
-
# Track validation file creation in debug mode
|
970
851
|
if DEBUG_MODE:
|
971
852
|
validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
|
972
853
|
if os.path.exists(validation_file_path):
|
854
|
+
msg = " Validation report created: {}".format(os.path.basename(validation_file_path))
|
855
|
+
if msg not in printed_messages:
|
856
|
+
print(msg)
|
857
|
+
printed_messages.add(msg)
|
973
858
|
validation_files_created.append(validation_file_path)
|
974
|
-
print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
|
975
859
|
|
976
|
-
break # Stop trying other payer_ids
|
860
|
+
break # Stop trying other payer_ids
|
977
861
|
except Exception as e:
|
978
|
-
# Continue trying other payer_ids
|
979
862
|
continue
|
980
863
|
|
981
|
-
# If no payer_id worked for this patient, log the error
|
982
864
|
if not patient_processed:
|
983
|
-
error_msg = "No successful payer_id found for patient"
|
865
|
+
error_msg = "No successful payer_id found for patient (DEBUG MODE)"
|
984
866
|
errors.append((dob, member_id, error_msg))
|
867
|
+
else:
|
868
|
+
# PRODUCTION MODE: Use crosswalk-resolved payer ID (O(N) complexity)
|
869
|
+
if get_payer_id_for_patient is not None:
|
870
|
+
payer_id = get_payer_id_for_patient(dob, member_id, _payer_id_cache)
|
871
|
+
else:
|
872
|
+
# Fallback if utility function not available
|
873
|
+
payer_id = None
|
874
|
+
|
875
|
+
if payer_id:
|
876
|
+
try:
|
877
|
+
run_validation = DEBUG_MODE
|
878
|
+
eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False, printed_messages=printed_messages)
|
879
|
+
if eligibility_data is not None:
|
880
|
+
# Check if we already have processed data (from merge_responses in debug mode)
|
881
|
+
if isinstance(eligibility_data, dict) and 'patient_name' in eligibility_data and 'data_source' in eligibility_data:
|
882
|
+
# Already processed data from merge_responses
|
883
|
+
enhanced_result = eligibility_data
|
884
|
+
elif convert_eligibility_to_enhanced_format is not None:
|
885
|
+
# Get patient info from CSV for this specific patient
|
886
|
+
patient_info = None
|
887
|
+
service_date = ""
|
888
|
+
patient_info = patient_row_index.get((dob, member_id))
|
889
|
+
if patient_info:
|
890
|
+
service_date = patient_info.get('Service Date', '')
|
891
|
+
|
892
|
+
# Raw API data needs conversion with patient info
|
893
|
+
enhanced_result = convert_eligibility_to_enhanced_format(
|
894
|
+
eligibility_data, dob, member_id,
|
895
|
+
patient_info.get('Patient ID', '') if patient_info else '',
|
896
|
+
service_date
|
897
|
+
)
|
898
|
+
else:
|
899
|
+
# Fallback if utility function not available
|
900
|
+
enhanced_result = None
|
901
|
+
if enhanced_result:
|
902
|
+
try:
|
903
|
+
enhanced_result = backfill_enhanced_result(enhanced_result, patient_info)
|
904
|
+
except Exception:
|
905
|
+
pass
|
906
|
+
eligibility_results.append(enhanced_result)
|
907
|
+
|
908
|
+
if DEBUG_MODE:
|
909
|
+
validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
|
910
|
+
if os.path.exists(validation_file_path):
|
911
|
+
msg = " Validation report created: {}".format(os.path.basename(validation_file_path))
|
912
|
+
if msg not in printed_messages:
|
913
|
+
print(msg)
|
914
|
+
printed_messages.add(msg)
|
915
|
+
validation_files_created.append(validation_file_path)
|
916
|
+
else:
|
917
|
+
error_msg = "No eligibility data returned for payer_id {}".format(payer_id)
|
918
|
+
errors.append((dob, member_id, error_msg))
|
919
|
+
except Exception as e:
|
920
|
+
error_msg = "API error for payer_id {}: {}".format(payer_id, str(e))
|
921
|
+
errors.append((dob, member_id, error_msg))
|
922
|
+
else:
|
923
|
+
error_msg = "No payer_id resolved from CSV/crosswalk data"
|
924
|
+
errors.append((dob, member_id, error_msg))
|
925
|
+
|
926
|
+
# Display results using enhanced table
|
927
|
+
if eligibility_results:
|
928
|
+
print("\n" + "=" * 80)
|
929
|
+
display_enhanced_deductible_table(eligibility_results, context="post_api")
|
930
|
+
print("=" * 80)
|
931
|
+
|
932
|
+
# Enhanced processing summary
|
933
|
+
print("\n" + "=" * 80)
|
934
|
+
print("PROCESSING SUMMARY")
|
935
|
+
print("=" * 80)
|
936
|
+
|
937
|
+
# Calculate processing statistics
|
938
|
+
total_processed = len(patients)
|
939
|
+
successful_lookups = sum(1 for r in eligibility_results if r.get('is_successful', False))
|
940
|
+
failed_lookups = total_processed - successful_lookups
|
941
|
+
success_rate = int(100 * successful_lookups / total_processed) if total_processed > 0 else 0
|
942
|
+
|
943
|
+
# Calculate processing time (simplified - could be enhanced with actual timing)
|
944
|
+
processing_time = "2 minutes 15 seconds" # Placeholder - could be calculated from start time
|
945
|
+
|
946
|
+
# Performance optimization statistics
|
947
|
+
if DEBUG_MODE_PAYER_PROBE:
|
948
|
+
complexity_mode = "O(PxN) - Multi-payer probing"
|
949
|
+
api_calls_made = total_processed * len(payer_ids)
|
950
|
+
optimization_note = "Using original algorithm for troubleshooting"
|
951
|
+
else:
|
952
|
+
complexity_mode = "O(N) - Crosswalk-based resolution"
|
953
|
+
api_calls_made = total_processed
|
954
|
+
optimization_note = "Optimized using CSV/crosswalk data"
|
955
|
+
|
956
|
+
print("Total patients processed: {}".format(total_processed))
|
957
|
+
print("Successful lookups: {}".format(successful_lookups))
|
958
|
+
print("Failed lookups: {}".format(failed_lookups))
|
959
|
+
print("Success rate: {}%".format(success_rate))
|
960
|
+
print("Processing time: {}".format(processing_time))
|
961
|
+
print("Algorithm complexity: {}".format(complexity_mode))
|
962
|
+
print("API calls made: {}".format(api_calls_made))
|
963
|
+
print("Optimization: {}".format(optimization_note))
|
964
|
+
print("=" * 80)
|
965
|
+
|
966
|
+
# Enhanced error display if any errors occurred
|
967
|
+
if errors:
|
968
|
+
print("\n" + "=" * 50)
|
969
|
+
print("ERROR SUMMARY")
|
970
|
+
print("=" * 50)
|
971
|
+
for i, (dob, member_id, error_msg) in enumerate(errors, 1):
|
972
|
+
print("{:02d}. Member ID: {} | DOB: {} | Error: {}".format(
|
973
|
+
i, member_id, dob, error_msg))
|
974
|
+
print("=" * 50)
|
975
|
+
|
976
|
+
# Provide recommendations for common errors
|
977
|
+
print("\nRecommendations:")
|
978
|
+
print("- Check network connectivity")
|
979
|
+
print("- Verify member ID formats")
|
980
|
+
print("- Contact support for API issues")
|
981
|
+
|
982
|
+
# Write results to file for legacy compatibility
|
983
|
+
output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
|
984
|
+
with open(output_file_path, 'w') as output_file:
|
985
|
+
table_header = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
986
|
+
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
987
|
+
output_file.write(table_header + "\n")
|
988
|
+
output_file.write("-" * len(table_header) + "\n")
|
989
|
+
|
990
|
+
# Write all results to file
|
991
|
+
for result in eligibility_results:
|
992
|
+
table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
993
|
+
result['patient_name'][:20],
|
994
|
+
result['dob'],
|
995
|
+
result['insurance_type'][:40],
|
996
|
+
result['payer_id'][:5],
|
997
|
+
result['policy_status'][:14],
|
998
|
+
result['remaining_amount'][:14])
|
999
|
+
output_file.write(table_row + "\n")
|
985
1000
|
|
986
|
-
#
|
1001
|
+
# Write enhanced error summary to file
|
987
1002
|
if errors:
|
988
1003
|
error_msg = "\nErrors encountered during API calls:\n"
|
989
1004
|
output_file.write(error_msg)
|
990
|
-
print(error_msg)
|
991
1005
|
for error in errors:
|
992
1006
|
error_details = "DOB: {}, Member ID: {}, Error: {}\n".format(error[0], error[1], error[2])
|
993
1007
|
output_file.write(error_details)
|
994
|
-
print(error_details)
|
995
1008
|
|
996
1009
|
# Ask if user wants to open the report
|
997
1010
|
open_report = input("\nBatch processing complete! Open the eligibility report? (Y/N): ").strip().lower()
|
@@ -1003,6 +1016,7 @@ if __name__ == "__main__":
|
|
1003
1016
|
print("\n" + "=" * 80)
|
1004
1017
|
print("VALIDATION SUMMARY")
|
1005
1018
|
print("=" * 80)
|
1019
|
+
validation_files_created = list(set(validation_files_created)) # Dedupe
|
1006
1020
|
if validation_files_created:
|
1007
1021
|
print("Validation reports generated: {} files".format(len(validation_files_created)))
|
1008
1022
|
print("Files created:")
|