medicafe 0.250822.2__py3-none-any.whl → 0.250822.3__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_Crosswalk_Library.py +1 -1
- MediBot/__init__.py +1 -1
- MediCafe/__init__.py +1 -1
- MediCafe/api_core.py +116 -14
- MediCafe/core_utils.py +1 -3
- MediLink/MediLink_Deductible.py +239 -100
- MediLink/MediLink_Display_Utils.py +326 -2
- MediLink/__init__.py +1 -1
- {medicafe-0.250822.2.dist-info → medicafe-0.250822.3.dist-info}/METADATA +1 -1
- {medicafe-0.250822.2.dist-info → medicafe-0.250822.3.dist-info}/RECORD +14 -19
- 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.250822.3.dist-info}/LICENSE +0 -0
- {medicafe-0.250822.2.dist-info → medicafe-0.250822.3.dist-info}/WHEEL +0 -0
- {medicafe-0.250822.2.dist-info → medicafe-0.250822.3.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250822.2.dist-info → medicafe-0.250822.3.dist-info}/top_level.txt +0 -0
@@ -42,7 +42,7 @@ else:
|
|
42
42
|
load_and_parse_z_data = None
|
43
43
|
|
44
44
|
# Import API functions using centralized import pattern
|
45
|
-
MediLink_API_v3 = smart_import(['MediCafe.api_core'
|
45
|
+
MediLink_API_v3 = smart_import(['MediCafe.api_core'])
|
46
46
|
fetch_payer_name_from_api = getattr(MediLink_API_v3, 'fetch_payer_name_from_api', None) if MediLink_API_v3 else None
|
47
47
|
|
48
48
|
# Module-level cache to prevent redundant API calls
|
MediBot/__init__.py
CHANGED
MediCafe/__init__.py
CHANGED
MediCafe/api_core.py
CHANGED
@@ -118,10 +118,107 @@ except ImportError:
|
|
118
118
|
class TokenCache:
|
119
119
|
def __init__(self):
|
120
120
|
self.tokens = {}
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
121
|
+
|
122
|
+
# -----------------------------------------------------------------------------
|
123
|
+
# Endpoint-specific payer ID management (crosswalk-backed with hardcoded default)
|
124
|
+
# -----------------------------------------------------------------------------
|
125
|
+
# Intent:
|
126
|
+
# - Validate payer IDs against the endpoint actually being called.
|
127
|
+
# - Persist endpoint-specific payer ID lists into the crosswalk so they can be
|
128
|
+
# updated over time without changing code.
|
129
|
+
# - For OPTUMAI: use the augmented list (includes LIFE1, WELM2, etc.).
|
130
|
+
# - For UHCAPI (including its Super Connector fallback): strictly enforce the
|
131
|
+
# known-good UHC payer IDs only.
|
132
|
+
# - Future: OPTUMAI will expose a dedicated endpoint that returns its current
|
133
|
+
# valid payer list. When available, this function should fetch and refresh the
|
134
|
+
# crosswalk entry automatically (likely weekly/monthly), replacing the
|
135
|
+
# hardcoded default below. The UHCAPI Super Connector will eventually be
|
136
|
+
# deprecated; when removed, cleanup the UHC-specific paths accordingly.
|
137
|
+
|
138
|
+
try:
|
139
|
+
# Prefer using existing crosswalk persistence utilities
|
140
|
+
from MediBot.MediBot_Crosswalk_Utils import ensure_full_config_loaded, save_crosswalk
|
141
|
+
except Exception:
|
142
|
+
ensure_full_config_loaded = None
|
143
|
+
save_crosswalk = None
|
144
|
+
|
145
|
+
def _get_default_endpoint_payer_ids(endpoint_name):
|
146
|
+
"""
|
147
|
+
Return hardcoded default payer IDs for a given endpoint.
|
148
|
+
|
149
|
+
NOTE: Defaults are used when crosswalk does not yet contain a list.
|
150
|
+
"""
|
151
|
+
# UHC-only list – keep STRICT. Do not augment with non-UHC payers.
|
152
|
+
uhc_payer_ids = [
|
153
|
+
"87726", "03432", "96385", "95467", "86050", "86047", "95378", "06111", "37602"
|
154
|
+
]
|
155
|
+
|
156
|
+
# OPTUMAI – augmented list (subject to growth once the API adds a payer-list endpoint)
|
157
|
+
optumai_payer_ids = [
|
158
|
+
"87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400",
|
159
|
+
"03432", "86050", "86047", "95378", "95467", "LIFE1", "WELM2"
|
160
|
+
]
|
161
|
+
|
162
|
+
if endpoint_name == 'OPTUMAI':
|
163
|
+
return optumai_payer_ids
|
164
|
+
# Default to UHCAPI for any other endpoint name
|
165
|
+
return uhc_payer_ids
|
166
|
+
|
167
|
+
def get_valid_payer_ids_for_endpoint(client, endpoint_name):
|
168
|
+
"""
|
169
|
+
Resolve the valid payer IDs for a specific endpoint using crosswalk storage
|
170
|
+
with a safe fallback to hardcoded defaults.
|
171
|
+
|
172
|
+
Behavior:
|
173
|
+
- Attempts to read crosswalk['endpoint_payer_ids'][endpoint_name].
|
174
|
+
- If missing, initializes with hardcoded defaults and persists to crosswalk
|
175
|
+
(non-interactive) so that future sessions use the saved list.
|
176
|
+
- Future: For OPTUMAI, replace the hardcoded default by calling the API's
|
177
|
+
payer-list endpoint once available, then update the crosswalk.
|
178
|
+
"""
|
179
|
+
try:
|
180
|
+
# Load full config + crosswalk (non-destructive)
|
181
|
+
base_config = None
|
182
|
+
crosswalk = None
|
183
|
+
if ensure_full_config_loaded is not None:
|
184
|
+
base_config, crosswalk = ensure_full_config_loaded(
|
185
|
+
getattr(client, 'config', None),
|
186
|
+
getattr(client, 'crosswalk', None)
|
187
|
+
)
|
188
|
+
else:
|
189
|
+
# Fallback: attempt to load via MediLink_ConfigLoader directly
|
190
|
+
# If we reach this fallback, it means ensure_full_config_loaded is not available.
|
191
|
+
# This is unexpected in normal operation and should be alerted.
|
192
|
+
print("Warning: IN api_core, ensure_full_config_loaded is not available; falling back to MediLink_ConfigLoader.load_configuration().")
|
193
|
+
MediLink_ConfigLoader.log(
|
194
|
+
"Fallback: ensure_full_config_loaded not available in get_valid_payer_ids_for_endpoint; using MediLink_ConfigLoader.load_configuration().",
|
195
|
+
level="WARNING"
|
196
|
+
)
|
197
|
+
base_config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
198
|
+
|
199
|
+
# Extract any existing stored list
|
200
|
+
cw_ep = crosswalk.get('endpoint_payer_ids', {}) if isinstance(crosswalk, dict) else {}
|
201
|
+
existing = cw_ep.get(endpoint_name)
|
202
|
+
if isinstance(existing, list) and len(existing) > 0:
|
203
|
+
return existing
|
204
|
+
|
205
|
+
# Initialize from defaults and persist to crosswalk
|
206
|
+
defaults = _get_default_endpoint_payer_ids(endpoint_name)
|
207
|
+
if isinstance(crosswalk, dict):
|
208
|
+
if 'endpoint_payer_ids' not in crosswalk:
|
209
|
+
crosswalk['endpoint_payer_ids'] = {}
|
210
|
+
crosswalk['endpoint_payer_ids'][endpoint_name] = list(defaults)
|
211
|
+
|
212
|
+
# Persist without interactive prompts; ignore errors silently to avoid breaking flows
|
213
|
+
if save_crosswalk is not None:
|
214
|
+
try:
|
215
|
+
save_crosswalk(client, base_config, crosswalk, skip_api_operations=True)
|
216
|
+
except Exception:
|
217
|
+
pass
|
218
|
+
return defaults
|
219
|
+
except Exception:
|
220
|
+
# As a last resort, return a safe default for the endpoint
|
221
|
+
return _get_default_endpoint_payer_ids(endpoint_name)
|
125
222
|
|
126
223
|
class BaseAPIClient:
|
127
224
|
def __init__(self, config):
|
@@ -792,12 +889,14 @@ def get_eligibility_v3(client, payer_id, provider_last_name, search_option, date
|
|
792
889
|
if not all([client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi]):
|
793
890
|
raise ValueError("All required parameters must have values: client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi")
|
794
891
|
|
795
|
-
#
|
796
|
-
valid_payer_ids = ["87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400", "03432", "86050", "86047", "95378", "95467"]
|
797
|
-
if payer_id not in valid_payer_ids:
|
798
|
-
raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
|
799
|
-
|
892
|
+
# Endpoint is UHCAPI for this v3 REST call
|
800
893
|
endpoint_name = 'UHCAPI'
|
894
|
+
|
895
|
+
# Validate payer_id strictly against UHC list
|
896
|
+
valid_payer_ids = get_valid_payer_ids_for_endpoint(client, endpoint_name)
|
897
|
+
if payer_id not in valid_payer_ids:
|
898
|
+
raise ValueError("Invalid payer_id: {} for endpoint {}. Must be one of: {}".format(
|
899
|
+
payer_id, endpoint_name, ", ".join(valid_payer_ids)))
|
801
900
|
from MediCafe.core_utils import extract_medilink_config
|
802
901
|
medi = extract_medilink_config(client.config)
|
803
902
|
url_extension = medi.get('endpoints', {}).get(endpoint_name, {}).get('additional_endpoints', {}).get('eligibility_v3', '')
|
@@ -856,11 +955,6 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
856
955
|
if not all([client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi]):
|
857
956
|
raise ValueError("All required parameters must have values: client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi")
|
858
957
|
|
859
|
-
# Validate payer_id
|
860
|
-
valid_payer_ids = ["87726", "06111", "25463", "37602", "39026", "74227", "65088", "81400", "03432", "86050", "86047", "95378", "95467"]
|
861
|
-
if payer_id not in valid_payer_ids:
|
862
|
-
raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
|
863
|
-
|
864
958
|
# Prefer OPTUMAI endpoint if configured, otherwise fall back to legacy UHCAPI super connector
|
865
959
|
try:
|
866
960
|
endpoints_cfg = client.config['MediLink_Config']['endpoints']
|
@@ -889,6 +983,14 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
889
983
|
except Exception:
|
890
984
|
url_extension = None
|
891
985
|
|
986
|
+
# Validate payer_id against the selected endpoint's list
|
987
|
+
# - If OPTUMAI is used, allow the augmented list (includes LIFE1, WELM2, etc.).
|
988
|
+
# - If UHCAPI fallback is used, enforce strict UHC list only.
|
989
|
+
valid_payer_ids = get_valid_payer_ids_for_endpoint(client, endpoint_name)
|
990
|
+
if payer_id not in valid_payer_ids:
|
991
|
+
raise ValueError("Invalid payer_id: {} for endpoint {}. Must be one of: {}".format(
|
992
|
+
payer_id, endpoint_name, ", ".join(valid_payer_ids)))
|
993
|
+
|
892
994
|
if not url_extension:
|
893
995
|
raise ValueError("Eligibility endpoint not configured for {}".format(endpoint_name))
|
894
996
|
|
MediCafe/core_utils.py
CHANGED
@@ -499,9 +499,7 @@ def get_api_client_factory():
|
|
499
499
|
"""
|
500
500
|
# Try multiple import paths for factory
|
501
501
|
import_specs = [
|
502
|
-
('MediCafe.api_factory', 'APIClientFactory')
|
503
|
-
('MediLink.MediLink_API_Factory', 'APIClientFactory'), # Legacy fallback
|
504
|
-
('MediLink_API_Factory', 'APIClientFactory') # Legacy fallback
|
502
|
+
('MediCafe.api_factory', 'APIClientFactory')
|
505
503
|
]
|
506
504
|
|
507
505
|
APIClientFactory = import_with_alternatives(import_specs)
|
MediLink/MediLink_Deductible.py
CHANGED
@@ -50,6 +50,11 @@ 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
|
55
60
|
|
@@ -116,14 +121,15 @@ except ImportError as e:
|
|
116
121
|
|
117
122
|
# Function to check if the date format is correct
|
118
123
|
def validate_and_format_date(date_str):
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
"""
|
125
|
+
Enhanced date parsing that handles ambiguous formats intelligently.
|
126
|
+
For ambiguous formats like MM/DD vs DD/MM, uses heuristics to determine the most likely interpretation.
|
127
|
+
"""
|
128
|
+
import re
|
129
|
+
|
130
|
+
# First, try unambiguous formats (4-digit years, month names, etc.)
|
131
|
+
unambiguous_formats = [
|
122
132
|
'%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
133
|
'%d-%b-%Y', # 15-Jan-1990
|
128
134
|
'%d %b %Y', # 15 Jan 1990
|
129
135
|
'%b %d, %Y', # Jan 15, 1990
|
@@ -132,46 +138,114 @@ def validate_and_format_date(date_str):
|
|
132
138
|
'%B %d %Y', # January 15 1990
|
133
139
|
'%Y/%m/%d', # 1990/01/15
|
134
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
|
135
221
|
|
136
|
-
|
137
|
-
|
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
|
138
227
|
'%m-%d-%y', # 01-15-90
|
139
|
-
'%d/%m/%y', # 15/01/90
|
140
228
|
'%d-%m-%y', # 15-01-90
|
141
|
-
'%d %b %y', # 15 Jan 90
|
142
229
|
'%b %d, %y', # Jan 15, 90
|
143
230
|
'%b %d %y', # Jan 15 90
|
144
231
|
'%y/%m/%d', # 90/01/15
|
145
232
|
'%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
233
|
]
|
158
234
|
|
159
|
-
for fmt in
|
235
|
+
for fmt in remaining_formats:
|
160
236
|
try:
|
161
|
-
# For 2-digit years, assume 20th/21st century
|
162
237
|
if '%y' in fmt:
|
163
238
|
parsed_date = datetime.strptime(date_str, fmt)
|
164
|
-
# Handle year 00-99: assume 1950-2049 range
|
165
239
|
if parsed_date.year < 50:
|
166
240
|
parsed_date = parsed_date.replace(year=parsed_date.year + 2000)
|
167
241
|
elif parsed_date.year < 100:
|
168
242
|
parsed_date = parsed_date.replace(year=parsed_date.year + 1900)
|
169
|
-
|
243
|
+
return parsed_date.strftime('%Y-%m-%d')
|
170
244
|
else:
|
171
|
-
|
172
|
-
return formatted_date
|
245
|
+
return datetime.strptime(date_str, fmt).strftime('%Y-%m-%d')
|
173
246
|
except ValueError:
|
174
247
|
continue
|
248
|
+
|
175
249
|
return None
|
176
250
|
|
177
251
|
# Use latest core_utils configuration cache for better performance
|
@@ -203,7 +277,8 @@ if provider_last_name == 'Unknown':
|
|
203
277
|
MediLink_ConfigLoader.log("Warning: provider_last_name was not found in the configuration.", level="WARNING")
|
204
278
|
|
205
279
|
# Define the list of payer_id's to iterate over
|
206
|
-
payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602'] # United Healthcare.
|
280
|
+
payer_ids = ['87726', '03432', '96385', '95467', '86050', '86047', '95378', '06111', '37602'] # United Healthcare ONLY.
|
281
|
+
|
207
282
|
|
208
283
|
# Get the latest CSV
|
209
284
|
CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
|
@@ -222,10 +297,11 @@ summary_valid_rows = [
|
|
222
297
|
for row in valid_rows
|
223
298
|
]
|
224
299
|
|
225
|
-
#
|
226
|
-
|
227
|
-
|
228
|
-
|
300
|
+
# Display enhanced summary of valid rows using unified display philosophy
|
301
|
+
from MediLink_Display_Utils import display_enhanced_deductible_table
|
302
|
+
|
303
|
+
# Use the enhanced table display for pre-API context
|
304
|
+
display_enhanced_deductible_table(valid_rows, context="pre_api")
|
229
305
|
|
230
306
|
# List of patients with DOB and MemberID from CSV data with fallback
|
231
307
|
patients = [
|
@@ -274,6 +350,15 @@ def manual_deductible_lookup():
|
|
274
350
|
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
351
|
if eligibility_data:
|
276
352
|
found_data = True
|
353
|
+
|
354
|
+
# Convert to enhanced format and display
|
355
|
+
enhanced_result = convert_eligibility_to_enhanced_format(eligibility_data, formatted_dob, member_id)
|
356
|
+
if enhanced_result:
|
357
|
+
print("\n" + "=" * 60)
|
358
|
+
display_enhanced_deductible_table([enhanced_result], context="post_api",
|
359
|
+
title="Manual Lookup Result")
|
360
|
+
print("=" * 60)
|
361
|
+
|
277
362
|
# Generate unique output file for manual request
|
278
363
|
output_file_name = "eligibility_report_manual_{}_{}.txt".format(member_id, formatted_dob)
|
279
364
|
output_file_path = os.path.join(os.getenv('TEMP'), output_file_name)
|
@@ -282,8 +367,6 @@ def manual_deductible_lookup():
|
|
282
367
|
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
283
368
|
output_file.write(table_header + "\n")
|
284
369
|
output_file.write("-" * len(table_header) + "\n")
|
285
|
-
print(table_header)
|
286
|
-
print("-" * len(table_header))
|
287
370
|
display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
|
288
371
|
|
289
372
|
# Ask if user wants to open the report
|
@@ -799,10 +882,11 @@ def is_super_connector_response_format(data):
|
|
799
882
|
"""Determine if the response is in Super Connector format (has rawGraphQLResponse)"""
|
800
883
|
return data is not None and "rawGraphQLResponse" in data
|
801
884
|
|
802
|
-
# Function to
|
803
|
-
def
|
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"""
|
804
888
|
if data is None:
|
805
|
-
return
|
889
|
+
return None
|
806
890
|
|
807
891
|
# Determine which API response format we're dealing with
|
808
892
|
if is_legacy_response_format(data):
|
@@ -821,14 +905,21 @@ def display_eligibility_info(data, dob, member_id, output_file):
|
|
821
905
|
patient_info['firstName'],
|
822
906
|
patient_info['middleName'],
|
823
907
|
patient_info['lastName']
|
824
|
-
).strip()
|
908
|
+
).strip()
|
825
909
|
|
826
|
-
|
827
|
-
|
828
|
-
patient_name
|
829
|
-
|
830
|
-
|
831
|
-
|
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
|
+
}
|
832
923
|
|
833
924
|
elif is_super_connector_response_format(data):
|
834
925
|
# Handle Super Connector API response format
|
@@ -841,19 +932,47 @@ def display_eligibility_info(data, dob, member_id, output_file):
|
|
841
932
|
patient_info['firstName'],
|
842
933
|
patient_info['middleName'],
|
843
934
|
patient_info['lastName']
|
844
|
-
).strip()
|
935
|
+
).strip()
|
845
936
|
|
846
|
-
|
847
|
-
|
848
|
-
patient_name
|
849
|
-
|
850
|
-
|
851
|
-
|
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
|
+
}
|
852
950
|
|
853
951
|
else:
|
854
952
|
# Unknown response format - log for debugging
|
855
|
-
MediLink_ConfigLoader.log("Unknown response format in
|
953
|
+
MediLink_ConfigLoader.log("Unknown response format in convert_eligibility_to_enhanced_format", level="WARNING")
|
856
954
|
MediLink_ConfigLoader.log("Response structure: {}".format(json.dumps(data, indent=2)), level="DEBUG")
|
955
|
+
return None
|
956
|
+
|
957
|
+
# Function to extract required fields and display in a tabular format
|
958
|
+
def display_eligibility_info(data, dob, member_id, output_file, patient_id="", service_date=""):
|
959
|
+
"""Legacy display function - converts to enhanced format and displays"""
|
960
|
+
if data is None:
|
961
|
+
return
|
962
|
+
|
963
|
+
# Convert to enhanced format
|
964
|
+
enhanced_data = convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id, service_date)
|
965
|
+
if enhanced_data:
|
966
|
+
# Write to output file in legacy format for compatibility
|
967
|
+
table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
968
|
+
enhanced_data['patient_name'][:20],
|
969
|
+
enhanced_data['dob'],
|
970
|
+
enhanced_data['insurance_type'][:40],
|
971
|
+
enhanced_data['payer_id'][:5],
|
972
|
+
enhanced_data['policy_status'][:14],
|
973
|
+
enhanced_data['remaining_amount'][:14])
|
974
|
+
output_file.write(table_row + "\n")
|
975
|
+
print(table_row) # Print to console for progressive display
|
857
976
|
|
858
977
|
# Global mode flags (will be set in main)
|
859
978
|
LEGACY_MODE = False
|
@@ -928,60 +1047,80 @@ if __name__ == "__main__":
|
|
928
1047
|
print("Batch processing cancelled.")
|
929
1048
|
continue
|
930
1049
|
|
1050
|
+
# PERFORMANCE FIX: Optimize patient-payer processing to avoid O(PxN) complexity
|
1051
|
+
# Instead of nested loops, process each patient once and try payer_ids until success
|
1052
|
+
# TODO: We should be able to determine the correct payer_id for each patient ahead of time
|
1053
|
+
# by looking up their insurance information from the CSV data or crosswalk mapping.
|
1054
|
+
# This would eliminate the need to try multiple payer_ids per patient and make this O(N).
|
1055
|
+
# CLARIFICATION: In production, use the payer_id from the CSV/crosswalk as primary.
|
1056
|
+
# Retain multi-payer probing behind a DEBUG/DIAGNOSTIC feature toggle only.
|
1057
|
+
# Suggested flag: DEBUG_MODE_PAYER_PROBE = False (module-level), default False.
|
1058
|
+
errors = []
|
1059
|
+
validation_reports = []
|
1060
|
+
processed_count = 0
|
1061
|
+
validation_files_created = [] # Track validation files that were actually created
|
1062
|
+
eligibility_results = [] # Collect all results for enhanced display
|
1063
|
+
|
1064
|
+
for dob, member_id in patients:
|
1065
|
+
processed_count += 1
|
1066
|
+
print("Processing patient {}/{}: Member ID {}, DOB {}".format(
|
1067
|
+
processed_count, len(patients), member_id, dob))
|
1068
|
+
|
1069
|
+
# Try each payer_id for this patient until we get a successful response
|
1070
|
+
patient_processed = False
|
1071
|
+
for payer_id in payer_ids:
|
1072
|
+
try:
|
1073
|
+
# Run with validation enabled only in debug mode
|
1074
|
+
run_validation = DEBUG_MODE
|
1075
|
+
eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False)
|
1076
|
+
if eligibility_data is not None:
|
1077
|
+
# Convert to enhanced format and collect
|
1078
|
+
enhanced_result = convert_eligibility_to_enhanced_format(eligibility_data, dob, member_id)
|
1079
|
+
if enhanced_result:
|
1080
|
+
eligibility_results.append(enhanced_result)
|
1081
|
+
patient_processed = True
|
1082
|
+
|
1083
|
+
# Track validation file creation in debug mode
|
1084
|
+
if DEBUG_MODE:
|
1085
|
+
validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
|
1086
|
+
if os.path.exists(validation_file_path):
|
1087
|
+
validation_files_created.append(validation_file_path)
|
1088
|
+
print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
|
1089
|
+
|
1090
|
+
break # Stop trying other payer_ids for this patient once we get a response
|
1091
|
+
except Exception as e:
|
1092
|
+
# Continue trying other payer_ids
|
1093
|
+
continue
|
1094
|
+
|
1095
|
+
# If no payer_id worked for this patient, log the error
|
1096
|
+
if not patient_processed:
|
1097
|
+
error_msg = "No successful payer_id found for patient"
|
1098
|
+
errors.append((dob, member_id, error_msg))
|
1099
|
+
|
1100
|
+
# Display results using enhanced table
|
1101
|
+
if eligibility_results:
|
1102
|
+
print("\n" + "=" * 80)
|
1103
|
+
display_enhanced_deductible_table(eligibility_results, context="post_api")
|
1104
|
+
print("=" * 80)
|
1105
|
+
|
1106
|
+
# Write results to file for legacy compatibility
|
931
1107
|
output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
|
932
1108
|
with open(output_file_path, 'w') as output_file:
|
933
1109
|
table_header = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
934
1110
|
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
935
1111
|
output_file.write(table_header + "\n")
|
936
1112
|
output_file.write("-" * len(table_header) + "\n")
|
937
|
-
print(table_header)
|
938
|
-
print("-" * len(table_header))
|
939
|
-
|
940
|
-
# PERFORMANCE FIX: Optimize patient-payer processing to avoid O(PxN) complexity
|
941
|
-
# Instead of nested loops, process each patient once and try payer_ids until success
|
942
|
-
# TODO: We should be able to determine the correct payer_id for each patient ahead of time
|
943
|
-
# by looking up their insurance information from the CSV data or crosswalk mapping.
|
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
|
952
1113
|
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
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)
|
965
|
-
if eligibility_data is not None:
|
966
|
-
display_eligibility_info(eligibility_data, dob, member_id, output_file)
|
967
|
-
patient_processed = True
|
968
|
-
|
969
|
-
# Track validation file creation in debug mode
|
970
|
-
if DEBUG_MODE:
|
971
|
-
validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
|
972
|
-
if os.path.exists(validation_file_path):
|
973
|
-
validation_files_created.append(validation_file_path)
|
974
|
-
print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
|
975
|
-
|
976
|
-
break # Stop trying other payer_ids for this patient once we get a response
|
977
|
-
except Exception as e:
|
978
|
-
# Continue trying other payer_ids
|
979
|
-
continue
|
980
|
-
|
981
|
-
# If no payer_id worked for this patient, log the error
|
982
|
-
if not patient_processed:
|
983
|
-
error_msg = "No successful payer_id found for patient"
|
984
|
-
errors.append((dob, member_id, error_msg))
|
1114
|
+
# Write all results to file
|
1115
|
+
for result in eligibility_results:
|
1116
|
+
table_row = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
1117
|
+
result['patient_name'][:20],
|
1118
|
+
result['dob'],
|
1119
|
+
result['insurance_type'][:40],
|
1120
|
+
result['payer_id'][:5],
|
1121
|
+
result['policy_status'][:14],
|
1122
|
+
result['remaining_amount'][:14])
|
1123
|
+
output_file.write(table_row + "\n")
|
985
1124
|
|
986
1125
|
# Display errors if any
|
987
1126
|
if errors:
|