medicafe 0.250822.3__py3-none-any.whl → 0.250912.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 +15 -2
- 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/core_utils.py +8 -1
- MediCafe/deductible_utils.py +1233 -0
- MediLink/MediLink_837p_encoder_library.py +123 -39
- MediLink/MediLink_Deductible.py +524 -649
- MediLink/MediLink_Deductible_Validator.py +9 -3
- MediLink/MediLink_Display_Utils.py +44 -6
- MediLink/MediLink_Gmail.py +53 -1
- MediLink/MediLink_UI.py +20 -2
- MediLink/__init__.py +1 -1
- MediLink/webapp.html +1 -0
- {medicafe-0.250822.3.dist-info → medicafe-0.250912.0.dist-info}/METADATA +1 -1
- {medicafe-0.250822.3.dist-info → medicafe-0.250912.0.dist-info}/RECORD +24 -23
- {medicafe-0.250822.3.dist-info → medicafe-0.250912.0.dist-info}/LICENSE +0 -0
- {medicafe-0.250822.3.dist-info → medicafe-0.250912.0.dist-info}/WHEEL +0 -0
- {medicafe-0.250822.3.dist-info → medicafe-0.250912.0.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250822.3.dist-info → medicafe-0.250912.0.dist-info}/top_level.txt +0 -0
MediLink/MediLink_Deductible.py
CHANGED
@@ -57,6 +57,7 @@ then with an option to select specific patients to look up for all the valid row
|
|
57
57
|
"""
|
58
58
|
import os, sys, json
|
59
59
|
from datetime import datetime
|
60
|
+
from collections import defaultdict
|
60
61
|
|
61
62
|
# Add parent directory to Python path to access MediCafe module
|
62
63
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
@@ -83,17 +84,24 @@ try:
|
|
83
84
|
except ImportError:
|
84
85
|
api_core = None
|
85
86
|
|
86
|
-
# Import
|
87
|
-
try:
|
88
|
-
from MediCafe import api_core
|
89
|
-
except ImportError:
|
90
|
-
api_core = None
|
91
|
-
|
92
|
-
# Import api_core for eligibility functions
|
87
|
+
# Import deductible utilities from MediCafe
|
93
88
|
try:
|
94
|
-
from MediCafe import
|
95
|
-
|
96
|
-
|
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
|
97
105
|
except ImportError as e:
|
98
106
|
print("Error: Unable to import MediCafe.core_utils. Please ensure MediCafe package is properly installed.")
|
99
107
|
# Don't call log_import_error here since it's not available yet
|
@@ -117,136 +125,25 @@ try:
|
|
117
125
|
from MediBot import MediBot_Preprocessor_lib
|
118
126
|
except ImportError as e:
|
119
127
|
print("Warning: Unable to import MediBot_Preprocessor_lib: {}".format(e))
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
'%d %b %Y', # 15 Jan 1990
|
135
|
-
'%b %d, %Y', # Jan 15, 1990
|
136
|
-
'%b %d %Y', # Jan 15 1990
|
137
|
-
'%B %d, %Y', # January 15, 1990
|
138
|
-
'%B %d %Y', # January 15 1990
|
139
|
-
'%Y/%m/%d', # 1990/01/15
|
140
|
-
'%Y%m%d', # 19900115
|
141
|
-
'%y%m%d', # 900115 (unambiguous compact format)
|
142
|
-
]
|
143
|
-
|
144
|
-
# Try unambiguous formats first
|
145
|
-
for fmt in unambiguous_formats:
|
146
|
-
try:
|
147
|
-
if '%y' in fmt:
|
148
|
-
parsed_date = datetime.strptime(date_str, fmt)
|
149
|
-
if parsed_date.year < 50:
|
150
|
-
parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
|
151
|
-
elif parsed_date.year < 100:
|
152
|
-
parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
|
153
|
-
return parsed_date.strftime('%Y-%m-%d')
|
154
|
-
else:
|
155
|
-
return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
|
156
|
-
except ValueError:
|
157
|
-
continue
|
158
|
-
|
159
|
-
# Handle potentially ambiguous formats with smart heuristics
|
160
|
-
# Check if it's a MM/DD/YYYY or DD/MM/YYYY pattern
|
161
|
-
ambiguous_pattern = re.match(r'^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$', date_str)
|
162
|
-
if ambiguous_pattern:
|
163
|
-
first_num, second_num, year = map(int, ambiguous_pattern.groups())
|
164
|
-
|
165
|
-
# If first number > 12, it must be DD/MM/YYYY format
|
166
|
-
if first_num > 12:
|
167
|
-
try:
|
168
|
-
return datetime(int(year), int(second_num), int(first_num)).strftime('%Y-%m-%d')
|
169
|
-
except ValueError:
|
170
|
-
return None
|
171
|
-
|
172
|
-
# If second number > 12, it must be MM/DD/YYYY format
|
173
|
-
elif second_num > 12:
|
174
|
-
try:
|
175
|
-
return datetime(int(year), int(first_num), int(second_num)).strftime('%Y-%m-%d')
|
176
|
-
except ValueError:
|
177
|
-
return None
|
178
|
-
|
179
|
-
# Both numbers could be valid months (1-12), need to make an educated guess
|
180
|
-
else:
|
181
|
-
# Preference heuristic: In US context, MM/DD/YYYY is more common
|
182
|
-
# But also consider: if first number is 1-12 and second is 1-31, both are possible
|
183
|
-
# Default to MM/DD/YYYY for US-centric systems, but this could be configurable
|
184
|
-
try:
|
185
|
-
# Try MM/DD/YYYY first (US preference)
|
186
|
-
return datetime(int(year), int(first_num), int(second_num)).strftime('%Y-%m-%d')
|
187
|
-
except ValueError:
|
188
|
-
try:
|
189
|
-
# If that fails, try DD/MM/YYYY
|
190
|
-
return datetime(int(year), int(second_num), int(first_num)).strftime('%Y-%m-%d')
|
191
|
-
except ValueError:
|
192
|
-
return None
|
193
|
-
|
194
|
-
# Handle 2-digit year ambiguous formats
|
195
|
-
ambiguous_2digit_pattern = re.match(r'^(\d{1,2})[/-](\d{1,2})[/-](\d{2})$', date_str)
|
196
|
-
if ambiguous_2digit_pattern:
|
197
|
-
first_num, second_num, year = map(int, ambiguous_2digit_pattern.groups())
|
198
|
-
|
199
|
-
# Apply same logic as above, but handle 2-digit year
|
200
|
-
year = 2000 + year if year < 50 else 1900 + year
|
201
|
-
|
202
|
-
if first_num > 12:
|
203
|
-
try:
|
204
|
-
return datetime(year, second_num, first_num).strftime('%Y-%m-%d')
|
205
|
-
except ValueError:
|
206
|
-
return None
|
207
|
-
elif second_num > 12:
|
208
|
-
try:
|
209
|
-
return datetime(year, first_num, second_num).strftime('%Y-%m-%d')
|
210
|
-
except ValueError:
|
211
|
-
return None
|
212
|
-
else:
|
213
|
-
# Default to MM/DD/YY (US preference)
|
214
|
-
try:
|
215
|
-
return datetime(year, first_num, second_num).strftime('%Y-%m-%d')
|
216
|
-
except ValueError:
|
217
|
-
try:
|
218
|
-
return datetime(year, second_num, first_num).strftime('%Y-%m-%d')
|
219
|
-
except ValueError:
|
220
|
-
return None
|
221
|
-
|
222
|
-
# Try remaining formats that are less likely to be ambiguous
|
223
|
-
remaining_formats = [
|
224
|
-
'%m-%d-%Y', # 01-15-1990
|
225
|
-
'%d-%m-%Y', # 15-01-1990
|
226
|
-
'%d/%m/%Y', # 15/01/1990
|
227
|
-
'%m-%d-%y', # 01-15-90
|
228
|
-
'%d-%m-%y', # 15-01-90
|
229
|
-
'%b %d, %y', # Jan 15, 90
|
230
|
-
'%b %d %y', # Jan 15 90
|
231
|
-
'%y/%m/%d', # 90/01/15
|
232
|
-
'%y-%m-%d', # 90-01-15
|
233
|
-
]
|
234
|
-
|
235
|
-
for fmt in remaining_formats:
|
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)
|
134
|
+
|
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
|
236
142
|
try:
|
237
|
-
|
238
|
-
|
239
|
-
if parsed_date.year < 50:
|
240
|
-
parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
|
241
|
-
elif parsed_date.year < 100:
|
242
|
-
parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
|
243
|
-
return parsed_date.strftime('%Y-%m-%d')
|
244
|
-
else:
|
245
|
-
return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
|
143
|
+
from datetime import datetime
|
144
|
+
return datetime.strptime(date_str, '%Y-%m-%d').strftime('%Y-%m-%d')
|
246
145
|
except ValueError:
|
247
|
-
|
248
|
-
|
249
|
-
return None
|
146
|
+
return None
|
250
147
|
|
251
148
|
# Use latest core_utils configuration cache for better performance
|
252
149
|
_get_config, (_config_cache, _crosswalk_cache) = create_config_cache()
|
@@ -282,11 +179,38 @@ payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '061
|
|
282
179
|
|
283
180
|
# Get the latest CSV
|
284
181
|
CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
|
285
|
-
|
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 = []
|
286
189
|
|
287
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
|
+
|
288
195
|
valid_rows = [row for row in csv_data if str(row.get('Ins1 Payer ID', '')).strip() in payer_ids]
|
289
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
|
+
|
290
214
|
# Extract important columns for summary with fallback
|
291
215
|
summary_valid_rows = [
|
292
216
|
{
|
@@ -298,19 +222,128 @@ summary_valid_rows = [
|
|
298
222
|
]
|
299
223
|
|
300
224
|
# Display enhanced summary of valid rows using unified display philosophy
|
301
|
-
|
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
|
+
))
|
243
|
+
|
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())
|
302
292
|
|
303
293
|
# Use the enhanced table display for pre-API context
|
304
|
-
|
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)
|
305
345
|
|
306
|
-
|
307
|
-
patients = [
|
308
|
-
(validate_and_format_date(row.get('Patient DOB', row.get('DOB', ''))), # Try 'Patient DOB' first, then 'DOB'
|
309
|
-
row.get('Primary Policy Number', row.get('Ins1 Member ID', '')).strip()) # Try 'Primary Policy Number' first, then 'Ins1 Member ID'
|
310
|
-
for row in valid_rows
|
311
|
-
if validate_and_format_date(row.get('Patient DOB', row.get('DOB', ''))) is not None and
|
312
|
-
row.get('Primary Policy Number', row.get('Ins1 Member ID', '')).strip()
|
313
|
-
]
|
346
|
+
display_enhanced_deductible_table(display_data, context="pre_api")
|
314
347
|
|
315
348
|
# Function to handle manual patient deductible lookup
|
316
349
|
def manual_deductible_lookup():
|
@@ -332,7 +365,7 @@ def manual_deductible_lookup():
|
|
332
365
|
print("Returning to main menu.\n")
|
333
366
|
break
|
334
367
|
|
335
|
-
formatted_dob =
|
368
|
+
formatted_dob = _fallback_validate_and_format_date(dob_input)
|
336
369
|
if not formatted_dob:
|
337
370
|
print("Invalid DOB format. Please enter in YYYY-MM-DD format.\n")
|
338
371
|
continue
|
@@ -352,12 +385,53 @@ def manual_deductible_lookup():
|
|
352
385
|
found_data = True
|
353
386
|
|
354
387
|
# Convert to enhanced format and display
|
355
|
-
|
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
|
356
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
|
357
404
|
print("\n" + "=" * 60)
|
358
405
|
display_enhanced_deductible_table([enhanced_result], context="post_api",
|
359
406
|
title="Manual Lookup Result")
|
360
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)
|
361
435
|
|
362
436
|
# Generate unique output file for manual request
|
363
437
|
output_file_name = "eligibility_report_manual_{}_{}.txt".format(member_id, formatted_dob)
|
@@ -388,7 +462,10 @@ def manual_deductible_lookup():
|
|
388
462
|
|
389
463
|
|
390
464
|
# Function to get eligibility information
|
391
|
-
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
|
+
|
392
469
|
try:
|
393
470
|
# Log the parameters being sent to the function
|
394
471
|
MediLink_ConfigLoader.log("Calling eligibility check with parameters:", level="DEBUG")
|
@@ -405,9 +482,22 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
405
482
|
|
406
483
|
# Get legacy response
|
407
484
|
MediLink_ConfigLoader.log("Getting legacy get_eligibility_v3 API response", level="INFO")
|
408
|
-
|
409
|
-
|
410
|
-
)
|
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")
|
411
501
|
|
412
502
|
# Get Super Connector response for comparison
|
413
503
|
MediLink_ConfigLoader.log("Getting new get_eligibility_super_connector API response", level="INFO")
|
@@ -451,11 +541,17 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
451
541
|
raw_response = super_connector_eligibility.get('rawGraphQLResponse', {})
|
452
542
|
errors = raw_response.get('errors', [])
|
453
543
|
if errors:
|
454
|
-
|
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)
|
455
548
|
for i, error in enumerate(errors):
|
456
549
|
error_code = error.get('code', 'UNKNOWN')
|
457
550
|
error_desc = error.get('description', 'No description')
|
458
|
-
|
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)
|
459
555
|
|
460
556
|
# Check for data in error extensions (some APIs return data here)
|
461
557
|
extensions = error.get('extensions', {})
|
@@ -486,23 +582,52 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
486
582
|
except Exception as e:
|
487
583
|
print("\nError generating validation report: {}".format(str(e)))
|
488
584
|
|
489
|
-
#
|
490
|
-
|
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
|
+
}
|
491
602
|
|
492
603
|
else:
|
493
604
|
# Legacy mode: Only call legacy API
|
494
605
|
MediLink_ConfigLoader.log("Running in LEGACY MODE - calling legacy API only", level="INFO")
|
495
606
|
|
496
|
-
# Only get legacy response
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
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
|
501
624
|
|
502
625
|
# Log the response
|
503
|
-
|
504
|
-
|
505
|
-
|
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
|
506
631
|
except Exception as e:
|
507
632
|
# Handle HTTP errors if requests is available
|
508
633
|
if requests and hasattr(requests, 'exceptions') and isinstance(e, requests.exceptions.HTTPError):
|
@@ -515,444 +640,28 @@ def get_eligibility_info(client, payer_id, provider_last_name, date_of_birth, me
|
|
515
640
|
print("Eligibility Check Error: {}".format(e))
|
516
641
|
return None
|
517
642
|
|
518
|
-
#
|
519
|
-
#
|
520
|
-
# PROBLEM: API responses are returning correctly but the parser functions below
|
521
|
-
# are not successfully extracting the super_connector variables (likely eligibility data).
|
522
|
-
# This suggests a schema mismatch between expected and actual API response format.
|
523
|
-
#
|
524
|
-
# IMPLEMENTATION CLARIFICATION:
|
525
|
-
# - Primary path should not depend on probing payer_ids via API.
|
526
|
-
# - Prefer payer_id provided by CSV/crosswalk as the authoritative source.
|
527
|
-
# - Keep API probing behind a non-default debug flag to support troubleshooting sessions only.
|
528
|
-
# - Add detailed logging helpers (no-op in production) to inspect mismatches safely on XP.
|
529
|
-
#
|
530
|
-
# DEBUGGING STEPS:
|
531
|
-
# 1. Response Structure Analysis:
|
532
|
-
# - Add comprehensive logging of raw API responses before parsing
|
533
|
-
# - Compare current response format vs expected format in parser functions
|
534
|
-
# - Check if API endpoint has changed response schema recently
|
535
|
-
# - Verify if different endpoints return different response structures
|
536
|
-
#
|
537
|
-
# 2. Parser Function Validation:
|
538
|
-
# - Test each extract_*_patient_info() function with sample responses
|
539
|
-
# - Check if field names/paths have changed (e.g., 'patientInfo' vs 'patient_info')
|
540
|
-
# - Verify array indexing logic (e.g., [0] access on empty arrays)
|
541
|
-
# - Check case sensitivity in field access
|
542
|
-
#
|
543
|
-
# 3. Super Connector Variable Mapping:
|
544
|
-
# - Document what "super_connector variables" should contain
|
545
|
-
# - Identify which fields from API response map to these variables
|
546
|
-
# - Verify the expected format vs actual format
|
547
|
-
# - Check if variable names have changed in the application
|
548
|
-
#
|
549
|
-
# IMPLEMENTATION PLAN:
|
550
|
-
# 1. Enhanced Logging:
|
551
|
-
# - Add log_api_response_structure(response) function
|
552
|
-
# - Log raw JSON before each parser function call
|
553
|
-
# - Add field-by-field parsing logs with null checks
|
643
|
+
# API response parsing functions moved to MediCafe.deductible_utils
|
644
|
+
# All parsing logic is now centralized in the utility module for DRY compliance
|
554
645
|
#
|
555
|
-
#
|
556
|
-
#
|
557
|
-
#
|
558
|
-
#
|
559
|
-
#
|
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
|
560
651
|
#
|
561
|
-
#
|
562
|
-
#
|
563
|
-
#
|
564
|
-
#
|
565
|
-
#
|
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)
|
566
659
|
#
|
567
|
-
#
|
568
|
-
#
|
569
|
-
#
|
570
|
-
#
|
571
|
-
#
|
572
|
-
#
|
573
|
-
# IMMEDIATE ACTIONS:
|
574
|
-
# 1. Add detailed logging before each extract_*_patient_info() call
|
575
|
-
# 2. Log the structure of the 'policy' object being passed to parsers
|
576
|
-
# 3. Check if the issue is in extract_legacy_patient_info() vs extract_super_connector_patient_info()
|
577
|
-
# 4. Verify which API endpoint is being called and if it matches expected parser
|
578
|
-
#
|
579
|
-
# FILES TO EXAMINE:
|
580
|
-
# - This file: all extract_*_patient_info() functions
|
581
|
-
# - MediCafe/api_core.py: API call implementation and response handling
|
582
|
-
# - Config files: Check if API endpoints or credentials have changed
|
583
|
-
#
|
584
|
-
# RELATED ISSUES:
|
585
|
-
# - May be connected to authentication or endpoint configuration problems
|
586
|
-
# - Could indicate API version updates that changed response format
|
587
|
-
# - Might be related to different payer-specific response formats
|
588
|
-
|
589
|
-
def extract_legacy_patient_info(policy):
|
590
|
-
"""Extract patient information from legacy API response format"""
|
591
|
-
patient_info = policy.get("patientInfo", [{}])[0]
|
592
|
-
return {
|
593
|
-
'lastName': patient_info.get("lastName", ""),
|
594
|
-
'firstName': patient_info.get("firstName", ""),
|
595
|
-
'middleName': patient_info.get("middleName", "")
|
596
|
-
}
|
597
|
-
|
598
|
-
def extract_super_connector_patient_info(eligibility_data):
|
599
|
-
"""Extract patient information from Super Connector API response format"""
|
600
|
-
if not eligibility_data:
|
601
|
-
return {'lastName': '', 'firstName': '', 'middleName': ''}
|
602
|
-
|
603
|
-
# Handle multiple eligibility records - use the first one with valid data
|
604
|
-
if "rawGraphQLResponse" in eligibility_data:
|
605
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
606
|
-
data = raw_response.get('data', {})
|
607
|
-
check_eligibility = data.get('checkEligibility', {})
|
608
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
609
|
-
|
610
|
-
# Try to get from the first eligibility record
|
611
|
-
if eligibility_list:
|
612
|
-
first_eligibility = eligibility_list[0]
|
613
|
-
member_info = first_eligibility.get('eligibilityInfo', {}).get('member', {})
|
614
|
-
if member_info:
|
615
|
-
return {
|
616
|
-
'lastName': member_info.get("lastName", ""),
|
617
|
-
'firstName': member_info.get("firstName", ""),
|
618
|
-
'middleName': member_info.get("middleName", "")
|
619
|
-
}
|
620
|
-
|
621
|
-
# Check for data in error extensions (some APIs return data here despite errors)
|
622
|
-
errors = raw_response.get('errors', [])
|
623
|
-
for error in errors:
|
624
|
-
extensions = error.get('extensions', {})
|
625
|
-
if extensions and 'details' in extensions:
|
626
|
-
details = extensions.get('details', [])
|
627
|
-
if details:
|
628
|
-
# Use the first detail record that has patient info
|
629
|
-
for detail in details:
|
630
|
-
if detail.get('lastName') or detail.get('firstName'):
|
631
|
-
return {
|
632
|
-
'lastName': detail.get("lastName", ""),
|
633
|
-
'firstName': detail.get("firstName", ""),
|
634
|
-
'middleName': detail.get("middleName", "")
|
635
|
-
}
|
636
|
-
|
637
|
-
# Fallback to top-level fields
|
638
|
-
return {
|
639
|
-
'lastName': eligibility_data.get("lastName", ""),
|
640
|
-
'firstName': eligibility_data.get("firstName", ""),
|
641
|
-
'middleName': eligibility_data.get("middleName", "")
|
642
|
-
}
|
643
|
-
|
644
|
-
def extract_legacy_remaining_amount(policy):
|
645
|
-
"""Extract remaining amount from legacy API response format"""
|
646
|
-
deductible_info = policy.get("deductibleInfo", {})
|
647
|
-
if 'individual' in deductible_info:
|
648
|
-
remaining = deductible_info['individual']['inNetwork'].get("remainingAmount", "")
|
649
|
-
return remaining if remaining else "Not Found"
|
650
|
-
elif 'family' in deductible_info:
|
651
|
-
remaining = deductible_info['family']['inNetwork'].get("remainingAmount", "")
|
652
|
-
return remaining if remaining else "Not Found"
|
653
|
-
else:
|
654
|
-
return "Not Found"
|
655
|
-
|
656
|
-
def extract_super_connector_remaining_amount(eligibility_data):
|
657
|
-
"""Extract remaining amount from Super Connector API response format"""
|
658
|
-
if not eligibility_data:
|
659
|
-
return "Not Found"
|
660
|
-
|
661
|
-
# First, check top-level metYearToDateAmount which might indicate deductible met
|
662
|
-
met_amount = eligibility_data.get('metYearToDateAmount')
|
663
|
-
if met_amount is not None:
|
664
|
-
return str(met_amount)
|
665
|
-
|
666
|
-
# Collect all deductible amounts to find the most relevant one
|
667
|
-
all_deductible_amounts = []
|
668
|
-
|
669
|
-
# Look for deductible information in planLevels (based on validation report)
|
670
|
-
plan_levels = eligibility_data.get('planLevels', [])
|
671
|
-
for plan_level in plan_levels:
|
672
|
-
if plan_level.get('level') == 'deductibleInfo':
|
673
|
-
# Collect individual deductible amounts
|
674
|
-
individual_levels = plan_level.get('individual', [])
|
675
|
-
if individual_levels:
|
676
|
-
for individual in individual_levels:
|
677
|
-
remaining = individual.get('remainingAmount')
|
678
|
-
if remaining is not None:
|
679
|
-
try:
|
680
|
-
amount = float(remaining)
|
681
|
-
all_deductible_amounts.append(('individual', amount))
|
682
|
-
except (ValueError, TypeError):
|
683
|
-
pass
|
684
|
-
|
685
|
-
# Collect family deductible amounts
|
686
|
-
family_levels = plan_level.get('family', [])
|
687
|
-
if family_levels:
|
688
|
-
for family in family_levels:
|
689
|
-
remaining = family.get('remainingAmount')
|
690
|
-
if remaining is not None:
|
691
|
-
try:
|
692
|
-
amount = float(remaining)
|
693
|
-
all_deductible_amounts.append(('family', amount))
|
694
|
-
except (ValueError, TypeError):
|
695
|
-
pass
|
696
|
-
|
697
|
-
# Navigate to the rawGraphQLResponse structure as fallback
|
698
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
699
|
-
if raw_response:
|
700
|
-
data = raw_response.get('data', {})
|
701
|
-
check_eligibility = data.get('checkEligibility', {})
|
702
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
703
|
-
|
704
|
-
# Try all eligibility records for deductible information
|
705
|
-
for eligibility in eligibility_list:
|
706
|
-
plan_levels = eligibility.get('eligibilityInfo', {}).get('planLevels', [])
|
707
|
-
for plan_level in plan_levels:
|
708
|
-
if plan_level.get('level') == 'deductibleInfo':
|
709
|
-
# Collect individual deductible amounts
|
710
|
-
individual_levels = plan_level.get('individual', [])
|
711
|
-
if individual_levels:
|
712
|
-
for individual in individual_levels:
|
713
|
-
remaining = individual.get('remainingAmount')
|
714
|
-
if remaining is not None:
|
715
|
-
try:
|
716
|
-
amount = float(remaining)
|
717
|
-
all_deductible_amounts.append(('individual', amount))
|
718
|
-
except (ValueError, TypeError):
|
719
|
-
pass
|
720
|
-
|
721
|
-
# Collect family deductible amounts
|
722
|
-
family_levels = plan_level.get('family', [])
|
723
|
-
if family_levels:
|
724
|
-
for family in family_levels:
|
725
|
-
remaining = family.get('remainingAmount')
|
726
|
-
if remaining is not None:
|
727
|
-
try:
|
728
|
-
amount = float(remaining)
|
729
|
-
all_deductible_amounts.append(('family', amount))
|
730
|
-
except (ValueError, TypeError):
|
731
|
-
pass
|
732
|
-
|
733
|
-
# Select the most relevant deductible amount
|
734
|
-
if all_deductible_amounts:
|
735
|
-
# Strategy: Prefer individual over family, and prefer non-zero amounts
|
736
|
-
# First, try to find non-zero individual amounts
|
737
|
-
non_zero_individual = [amt for type_, amt in all_deductible_amounts if type_ == 'individual' and amt > 0]
|
738
|
-
if non_zero_individual:
|
739
|
-
return str(max(non_zero_individual)) # Return highest non-zero individual amount
|
740
|
-
|
741
|
-
# If no non-zero individual, try non-zero family amounts
|
742
|
-
non_zero_family = [amt for type_, amt in all_deductible_amounts if type_ == 'family' and amt > 0]
|
743
|
-
if non_zero_family:
|
744
|
-
return str(max(non_zero_family)) # Return highest non-zero family amount
|
745
|
-
|
746
|
-
# If all amounts are zero, return the first individual amount (or family if no individual)
|
747
|
-
individual_amounts = [amt for type_, amt in all_deductible_amounts if type_ == 'individual']
|
748
|
-
if individual_amounts:
|
749
|
-
return str(individual_amounts[0])
|
750
|
-
|
751
|
-
# Fallback to first family amount
|
752
|
-
family_amounts = [amt for type_, amt in all_deductible_amounts if type_ == 'family']
|
753
|
-
if family_amounts:
|
754
|
-
return str(family_amounts[0])
|
755
|
-
|
756
|
-
return "Not Found"
|
757
|
-
|
758
|
-
def extract_legacy_insurance_info(policy):
|
759
|
-
"""Extract insurance information from legacy API response format"""
|
760
|
-
insurance_info = policy.get("insuranceInfo", {})
|
761
|
-
return {
|
762
|
-
'insuranceType': insurance_info.get("insuranceType", ""),
|
763
|
-
'insuranceTypeCode': insurance_info.get("insuranceTypeCode", ""),
|
764
|
-
'memberId': insurance_info.get("memberId", ""),
|
765
|
-
'payerId': insurance_info.get("payerId", "")
|
766
|
-
}
|
767
|
-
|
768
|
-
def extract_super_connector_insurance_info(eligibility_data):
|
769
|
-
"""Extract insurance information from Super Connector API response format"""
|
770
|
-
if not eligibility_data:
|
771
|
-
return {'insuranceType': '', 'insuranceTypeCode': '', 'memberId': '', 'payerId': ''}
|
772
|
-
|
773
|
-
# Handle multiple eligibility records - use the first one with valid data
|
774
|
-
if "rawGraphQLResponse" in eligibility_data:
|
775
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
776
|
-
data = raw_response.get('data', {})
|
777
|
-
check_eligibility = data.get('checkEligibility', {})
|
778
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
779
|
-
|
780
|
-
# Try to get from the first eligibility record
|
781
|
-
if eligibility_list:
|
782
|
-
first_eligibility = eligibility_list[0]
|
783
|
-
insurance_info = first_eligibility.get('eligibilityInfo', {}).get('insuranceInfo', {})
|
784
|
-
if insurance_info:
|
785
|
-
return {
|
786
|
-
'insuranceType': insurance_info.get("planTypeDescription", ""),
|
787
|
-
'insuranceTypeCode': insurance_info.get("productServiceCode", ""),
|
788
|
-
'memberId': insurance_info.get("memberId", ""),
|
789
|
-
'payerId': insurance_info.get("payerId", "")
|
790
|
-
}
|
791
|
-
|
792
|
-
# Check for data in error extensions (some APIs return data here despite errors)
|
793
|
-
errors = raw_response.get('errors', [])
|
794
|
-
for error in errors:
|
795
|
-
extensions = error.get('extensions', {})
|
796
|
-
if extensions and 'details' in extensions:
|
797
|
-
details = extensions.get('details', [])
|
798
|
-
if details:
|
799
|
-
# Use the first detail record that has insurance info
|
800
|
-
for detail in details:
|
801
|
-
if detail.get('memberId') or detail.get('payerId'):
|
802
|
-
# Try to determine insurance type from available data
|
803
|
-
insurance_type = detail.get('planType', '')
|
804
|
-
if not insurance_type:
|
805
|
-
insurance_type = detail.get('productType', '')
|
806
|
-
|
807
|
-
return {
|
808
|
-
'insuranceType': insurance_type,
|
809
|
-
'insuranceTypeCode': detail.get("productServiceCode", ""),
|
810
|
-
'memberId': detail.get("memberId", ""),
|
811
|
-
'payerId': detail.get("payerId", "")
|
812
|
-
}
|
813
|
-
|
814
|
-
# Fallback to top-level fields
|
815
|
-
insurance_type = eligibility_data.get("planTypeDescription", "")
|
816
|
-
if not insurance_type:
|
817
|
-
insurance_type = eligibility_data.get("productType", "")
|
818
|
-
|
819
|
-
# Clean up the insurance type if it's too long (like the LPPO description)
|
820
|
-
if insurance_type and len(insurance_type) > 50:
|
821
|
-
# Extract just the plan type part
|
822
|
-
if "PPO" in insurance_type:
|
823
|
-
insurance_type = "Preferred Provider Organization (PPO)"
|
824
|
-
elif "HMO" in insurance_type:
|
825
|
-
insurance_type = "Health Maintenance Organization (HMO)"
|
826
|
-
elif "EPO" in insurance_type:
|
827
|
-
insurance_type = "Exclusive Provider Organization (EPO)"
|
828
|
-
elif "POS" in insurance_type:
|
829
|
-
insurance_type = "Point of Service (POS)"
|
830
|
-
|
831
|
-
# Get insurance type code from multiple possible locations
|
832
|
-
insurance_type_code = eligibility_data.get("productServiceCode", "")
|
833
|
-
if not insurance_type_code:
|
834
|
-
# Try to get from coverageTypes
|
835
|
-
coverage_types = eligibility_data.get("coverageTypes", [])
|
836
|
-
if coverage_types:
|
837
|
-
insurance_type_code = coverage_types[0].get("typeCode", "")
|
838
|
-
|
839
|
-
# Note: We're not mapping "M" to "PR" as "M" likely means "Medical"
|
840
|
-
# and "PR" should be "12" for PPO according to CMS standards
|
841
|
-
# This mapping should be handled by the API developers
|
842
|
-
|
843
|
-
return {
|
844
|
-
'insuranceType': insurance_type,
|
845
|
-
'insuranceTypeCode': insurance_type_code,
|
846
|
-
'memberId': eligibility_data.get("subscriberId", ""),
|
847
|
-
'payerId': eligibility_data.get("payerId", "") # Use payerId instead of legalEntityCode (this should be payer_id from the inputs)
|
848
|
-
}
|
849
|
-
|
850
|
-
def extract_legacy_policy_status(policy):
|
851
|
-
"""Extract policy status from legacy API response format"""
|
852
|
-
policy_info = policy.get("policyInfo", {})
|
853
|
-
return policy_info.get("policyStatus", "")
|
854
|
-
|
855
|
-
def extract_super_connector_policy_status(eligibility_data):
|
856
|
-
"""Extract policy status from Super Connector API response format"""
|
857
|
-
if not eligibility_data:
|
858
|
-
return ""
|
859
|
-
|
860
|
-
# Handle multiple eligibility records - use the first one with valid data
|
861
|
-
if "rawGraphQLResponse" in eligibility_data:
|
862
|
-
raw_response = eligibility_data.get('rawGraphQLResponse', {})
|
863
|
-
data = raw_response.get('data', {})
|
864
|
-
check_eligibility = data.get('checkEligibility', {})
|
865
|
-
eligibility_list = check_eligibility.get('eligibility', [])
|
866
|
-
|
867
|
-
# Try to get from the first eligibility record
|
868
|
-
if eligibility_list:
|
869
|
-
first_eligibility = eligibility_list[0]
|
870
|
-
insurance_info = first_eligibility.get('eligibilityInfo', {}).get('insuranceInfo', {})
|
871
|
-
if insurance_info:
|
872
|
-
return insurance_info.get("policyStatus", "")
|
873
|
-
|
874
|
-
# Fallback to top-level field
|
875
|
-
return eligibility_data.get("policyStatus", "")
|
876
|
-
|
877
|
-
def is_legacy_response_format(data):
|
878
|
-
"""Determine if the response is in legacy format (has memberPolicies)"""
|
879
|
-
return data is not None and "memberPolicies" in data
|
880
|
-
|
881
|
-
def is_super_connector_response_format(data):
|
882
|
-
"""Determine if the response is in Super Connector format (has rawGraphQLResponse)"""
|
883
|
-
return data is not None and "rawGraphQLResponse" in data
|
884
|
-
|
885
|
-
# Function to convert eligibility data to enhanced display format
|
886
|
-
def convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id="", service_date=""):
|
887
|
-
"""Convert API eligibility response to enhanced display format"""
|
888
|
-
if data is None:
|
889
|
-
return None
|
890
|
-
|
891
|
-
# Determine which API response format we're dealing with
|
892
|
-
if is_legacy_response_format(data):
|
893
|
-
# Handle legacy API response format
|
894
|
-
for policy in data.get("memberPolicies", []):
|
895
|
-
# Skip non-medical policies
|
896
|
-
if policy.get("policyInfo", {}).get("coverageType", "") != "Medical":
|
897
|
-
continue
|
898
|
-
|
899
|
-
patient_info = extract_legacy_patient_info(policy)
|
900
|
-
remaining_amount = extract_legacy_remaining_amount(policy)
|
901
|
-
insurance_info = extract_legacy_insurance_info(policy)
|
902
|
-
policy_status = extract_legacy_policy_status(policy)
|
903
|
-
|
904
|
-
patient_name = "{} {} {}".format(
|
905
|
-
patient_info['firstName'],
|
906
|
-
patient_info['middleName'],
|
907
|
-
patient_info['lastName']
|
908
|
-
).strip()
|
909
|
-
|
910
|
-
return {
|
911
|
-
'patient_id': patient_id,
|
912
|
-
'patient_name': patient_name,
|
913
|
-
'dob': dob,
|
914
|
-
'member_id': member_id,
|
915
|
-
'payer_id': insurance_info['payerId'],
|
916
|
-
'service_date_display': service_date,
|
917
|
-
'service_date_sort': datetime.min, # Will be enhanced later
|
918
|
-
'status': 'Processed',
|
919
|
-
'insurance_type': insurance_info['insuranceType'],
|
920
|
-
'policy_status': policy_status,
|
921
|
-
'remaining_amount': remaining_amount
|
922
|
-
}
|
923
|
-
|
924
|
-
elif is_super_connector_response_format(data):
|
925
|
-
# Handle Super Connector API response format
|
926
|
-
patient_info = extract_super_connector_patient_info(data)
|
927
|
-
remaining_amount = extract_super_connector_remaining_amount(data)
|
928
|
-
insurance_info = extract_super_connector_insurance_info(data)
|
929
|
-
policy_status = extract_super_connector_policy_status(data)
|
930
|
-
|
931
|
-
patient_name = "{} {} {}".format(
|
932
|
-
patient_info['firstName'],
|
933
|
-
patient_info['middleName'],
|
934
|
-
patient_info['lastName']
|
935
|
-
).strip()
|
936
|
-
|
937
|
-
return {
|
938
|
-
'patient_id': patient_id,
|
939
|
-
'patient_name': patient_name,
|
940
|
-
'dob': dob,
|
941
|
-
'member_id': member_id,
|
942
|
-
'payer_id': insurance_info['payerId'],
|
943
|
-
'service_date_display': service_date,
|
944
|
-
'service_date_sort': datetime.min, # Will be enhanced later
|
945
|
-
'status': 'Processed',
|
946
|
-
'insurance_type': insurance_info['insuranceType'],
|
947
|
-
'policy_status': policy_status,
|
948
|
-
'remaining_amount': remaining_amount
|
949
|
-
}
|
950
|
-
|
951
|
-
else:
|
952
|
-
# Unknown response format - log for debugging
|
953
|
-
MediLink_ConfigLoader.log("Unknown response format in convert_eligibility_to_enhanced_format", level="WARNING")
|
954
|
-
MediLink_ConfigLoader.log("Response structure: {}".format(json.dumps(data, indent=2)), level="DEBUG")
|
955
|
-
return None
|
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
|
956
665
|
|
957
666
|
# Function to extract required fields and display in a tabular format
|
958
667
|
def display_eligibility_info(data, dob, member_id, output_file, patient_id="", service_date=""):
|
@@ -978,6 +687,17 @@ def display_eligibility_info(data, dob, member_id, output_file, patient_id="", s
|
|
978
687
|
LEGACY_MODE = False
|
979
688
|
DEBUG_MODE = False
|
980
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
|
+
|
981
701
|
# Main Execution Flow
|
982
702
|
if __name__ == "__main__":
|
983
703
|
print("\n" + "=" * 80)
|
@@ -990,33 +710,43 @@ if __name__ == "__main__":
|
|
990
710
|
print("\nSelect operation mode:")
|
991
711
|
print("1. Legacy Mode (Default) - Single API calls, consolidated output")
|
992
712
|
print("2. Debug Mode - Dual API calls with validation reports")
|
993
|
-
print("3.
|
713
|
+
print("3. Payer Probe Debug Mode - Multi-payer probing for troubleshooting")
|
714
|
+
print("4. Exit")
|
994
715
|
|
995
|
-
mode_choice = input("\nEnter your choice (1-
|
716
|
+
mode_choice = input("\nEnter your choice (1-4) [Default: 1]: ").strip()
|
996
717
|
if not mode_choice:
|
997
718
|
mode_choice = "1"
|
998
719
|
|
999
|
-
if mode_choice == "
|
720
|
+
if mode_choice == "4":
|
1000
721
|
print("\nExiting. Thank you for using MediLink Deductible Tool!")
|
1001
722
|
sys.exit(0)
|
1002
|
-
elif mode_choice not in ["1", "2"]:
|
723
|
+
elif mode_choice not in ["1", "2", "3"]:
|
1003
724
|
print("Invalid choice. Using Legacy Mode (Default).")
|
1004
725
|
mode_choice = "1"
|
1005
726
|
|
1006
727
|
# Set mode flags
|
1007
728
|
LEGACY_MODE = (mode_choice == "1")
|
1008
729
|
DEBUG_MODE = (mode_choice == "2")
|
730
|
+
DEBUG_MODE_PAYER_PROBE = (mode_choice == "3")
|
1009
731
|
|
1010
732
|
if LEGACY_MODE:
|
1011
733
|
print("\nRunning in LEGACY MODE")
|
1012
734
|
print("- Single API calls (Legacy API only)")
|
1013
735
|
print("- Progressive output during processing")
|
1014
736
|
print("- Consolidated output file at the end")
|
1015
|
-
|
737
|
+
print("- Crosswalk-based payer ID resolution (O(N) complexity)")
|
738
|
+
elif DEBUG_MODE:
|
1016
739
|
print("\nRunning in DEBUG MODE")
|
1017
740
|
print("- Dual API calls (Legacy + Super Connector)")
|
1018
741
|
print("- Validation reports and comparisons")
|
1019
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")
|
1020
750
|
|
1021
751
|
while True:
|
1022
752
|
print("\nChoose an option:")
|
@@ -1047,55 +777,151 @@ if __name__ == "__main__":
|
|
1047
777
|
print("Batch processing cancelled.")
|
1048
778
|
continue
|
1049
779
|
|
1050
|
-
# PERFORMANCE
|
1051
|
-
#
|
1052
|
-
#
|
1053
|
-
|
1054
|
-
#
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
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
|
+
|
1058
801
|
errors = []
|
1059
802
|
validation_reports = []
|
1060
803
|
processed_count = 0
|
1061
804
|
validation_files_created = [] # Track validation files that were actually created
|
1062
805
|
eligibility_results = [] # Collect all results for enhanced display
|
1063
|
-
|
806
|
+
printed_messages = set() # Initialize a set to track printed messages
|
807
|
+
|
1064
808
|
for dob, member_id in patients:
|
1065
809
|
processed_count += 1
|
1066
810
|
print("Processing patient {}/{}: Member ID {}, DOB {}".format(
|
1067
811
|
processed_count, len(patients), member_id, dob))
|
1068
812
|
|
1069
|
-
#
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
813
|
+
# Get payer ID for this patient
|
814
|
+
if DEBUG_MODE_PAYER_PROBE:
|
815
|
+
# DEBUG MODE: Use multi-payer probing (original O(PxN) logic)
|
816
|
+
patient_processed = False
|
817
|
+
for payer_id in payer_ids:
|
818
|
+
try:
|
819
|
+
run_validation = DEBUG_MODE
|
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)
|
821
|
+
if eligibility_data is not None:
|
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)
|
849
|
+
patient_processed = True
|
850
|
+
|
851
|
+
if DEBUG_MODE:
|
852
|
+
validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
|
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)
|
858
|
+
validation_files_created.append(validation_file_path)
|
859
|
+
|
860
|
+
break # Stop trying other payer_ids
|
861
|
+
except Exception as e:
|
862
|
+
continue
|
863
|
+
|
864
|
+
if not patient_processed:
|
865
|
+
error_msg = "No successful payer_id found for patient (DEBUG MODE)"
|
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))
|
1099
925
|
|
1100
926
|
# Display results using enhanced table
|
1101
927
|
if eligibility_results:
|
@@ -1103,6 +929,56 @@ if __name__ == "__main__":
|
|
1103
929
|
display_enhanced_deductible_table(eligibility_results, context="post_api")
|
1104
930
|
print("=" * 80)
|
1105
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
|
+
|
1106
982
|
# Write results to file for legacy compatibility
|
1107
983
|
output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
|
1108
984
|
with open(output_file_path, 'w') as output_file:
|
@@ -1122,15 +998,13 @@ if __name__ == "__main__":
|
|
1122
998
|
result['remaining_amount'][:14])
|
1123
999
|
output_file.write(table_row + "\n")
|
1124
1000
|
|
1125
|
-
#
|
1001
|
+
# Write enhanced error summary to file
|
1126
1002
|
if errors:
|
1127
1003
|
error_msg = "\nErrors encountered during API calls:\n"
|
1128
1004
|
output_file.write(error_msg)
|
1129
|
-
print(error_msg)
|
1130
1005
|
for error in errors:
|
1131
1006
|
error_details = "DOB: {}, Member ID: {}, Error: {}\n".format(error[0], error[1], error[2])
|
1132
1007
|
output_file.write(error_details)
|
1133
|
-
print(error_details)
|
1134
1008
|
|
1135
1009
|
# Ask if user wants to open the report
|
1136
1010
|
open_report = input("\nBatch processing complete! Open the eligibility report? (Y/N): ").strip().lower()
|
@@ -1142,6 +1016,7 @@ if __name__ == "__main__":
|
|
1142
1016
|
print("\n" + "=" * 80)
|
1143
1017
|
print("VALIDATION SUMMARY")
|
1144
1018
|
print("=" * 80)
|
1019
|
+
validation_files_created = list(set(validation_files_created)) # Dedupe
|
1145
1020
|
if validation_files_created:
|
1146
1021
|
print("Validation reports generated: {} files".format(len(validation_files_created)))
|
1147
1022
|
print("Files created:")
|