medicafe 0.250822.1__tar.gz → 0.250822.3__tar.gz
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.
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Crosswalk_Library.py +1 -1
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/__init__.py +1 -1
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/__init__.py +1 -1
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/api_core.py +116 -14
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/core_utils.py +1 -3
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Deductible.py +264 -75
- medicafe-0.250822.3/MediLink/MediLink_Display_Utils.py +419 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/__init__.py +1 -1
- {medicafe-0.250822.1/medicafe.egg-info → medicafe-0.250822.3}/PKG-INFO +1 -1
- {medicafe-0.250822.1 → medicafe-0.250822.3/medicafe.egg-info}/PKG-INFO +1 -1
- {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/SOURCES.txt +0 -5
- {medicafe-0.250822.1 → medicafe-0.250822.3}/setup.py +1 -1
- medicafe-0.250822.1/MediCafe/api_core_backup.py +0 -428
- medicafe-0.250822.1/MediLink/MediLink_Display_Utils.py +0 -95
- medicafe-0.250822.1/MediLink/insurance_type_integration_test.py +0 -361
- medicafe-0.250822.1/MediLink/test_cob_library.py +0 -436
- medicafe-0.250822.1/MediLink/test_timing.py +0 -59
- medicafe-0.250822.1/MediLink/test_validation.py +0 -127
- {medicafe-0.250822.1 → medicafe-0.250822.3}/LICENSE +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MANIFEST.in +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Notepad_Utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_Preprocessor_lib.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_UI.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_debug.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/clear_cache.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/crash_diagnostic.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/f_drive_diagnostic.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/full_debug_suite.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/process_csvs.bat +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/update_json.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/__main__.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/api_factory.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/api_utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/graphql_utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/logging_config.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/smart_import.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediCafe/submission_index.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Charges.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_main.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/test.py +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/MediLink/webapp.html +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/README.md +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250822.1 → medicafe-0.250822.3}/setup.cfg +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
|
@@ -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
|
|
@@ -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)
|
@@ -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,12 +121,131 @@ 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
|
-
|
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 = [
|
132
|
+
'%Y-%m-%d', # 1990-01-15
|
133
|
+
'%d-%b-%Y', # 15-Jan-1990
|
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:
|
120
146
|
try:
|
121
|
-
|
122
|
-
|
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')
|
123
156
|
except ValueError:
|
124
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:
|
236
|
+
try:
|
237
|
+
if '%y' in fmt:
|
238
|
+
parsed_date = datetime.strptime(date_str, fmt)
|
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')
|
246
|
+
except ValueError:
|
247
|
+
continue
|
248
|
+
|
125
249
|
return None
|
126
250
|
|
127
251
|
# Use latest core_utils configuration cache for better performance
|
@@ -153,7 +277,8 @@ if provider_last_name == 'Unknown':
|
|
153
277
|
MediLink_ConfigLoader.log("Warning: provider_last_name was not found in the configuration.", level="WARNING")
|
154
278
|
|
155
279
|
# Define the list of payer_id's to iterate over
|
156
|
-
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
|
+
|
157
282
|
|
158
283
|
# Get the latest CSV
|
159
284
|
CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
|
@@ -172,10 +297,11 @@ summary_valid_rows = [
|
|
172
297
|
for row in valid_rows
|
173
298
|
]
|
174
299
|
|
175
|
-
#
|
176
|
-
|
177
|
-
|
178
|
-
|
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")
|
179
305
|
|
180
306
|
# List of patients with DOB and MemberID from CSV data with fallback
|
181
307
|
patients = [
|
@@ -224,6 +350,15 @@ def manual_deductible_lookup():
|
|
224
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)
|
225
351
|
if eligibility_data:
|
226
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
|
+
|
227
362
|
# Generate unique output file for manual request
|
228
363
|
output_file_name = "eligibility_report_manual_{}_{}.txt".format(member_id, formatted_dob)
|
229
364
|
output_file_path = os.path.join(os.getenv('TEMP'), output_file_name)
|
@@ -232,8 +367,6 @@ def manual_deductible_lookup():
|
|
232
367
|
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
233
368
|
output_file.write(table_header + "\n")
|
234
369
|
output_file.write("-" * len(table_header) + "\n")
|
235
|
-
print(table_header)
|
236
|
-
print("-" * len(table_header))
|
237
370
|
display_eligibility_info(eligibility_data, formatted_dob, member_id, output_file)
|
238
371
|
|
239
372
|
# Ask if user wants to open the report
|
@@ -749,10 +882,11 @@ def is_super_connector_response_format(data):
|
|
749
882
|
"""Determine if the response is in Super Connector format (has rawGraphQLResponse)"""
|
750
883
|
return data is not None and "rawGraphQLResponse" in data
|
751
884
|
|
752
|
-
# Function to
|
753
|
-
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"""
|
754
888
|
if data is None:
|
755
|
-
return
|
889
|
+
return None
|
756
890
|
|
757
891
|
# Determine which API response format we're dealing with
|
758
892
|
if is_legacy_response_format(data):
|
@@ -771,14 +905,21 @@ def display_eligibility_info(data, dob, member_id, output_file):
|
|
771
905
|
patient_info['firstName'],
|
772
906
|
patient_info['middleName'],
|
773
907
|
patient_info['lastName']
|
774
|
-
).strip()
|
908
|
+
).strip()
|
775
909
|
|
776
|
-
|
777
|
-
|
778
|
-
patient_name
|
779
|
-
|
780
|
-
|
781
|
-
|
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
|
+
}
|
782
923
|
|
783
924
|
elif is_super_connector_response_format(data):
|
784
925
|
# Handle Super Connector API response format
|
@@ -791,19 +932,47 @@ def display_eligibility_info(data, dob, member_id, output_file):
|
|
791
932
|
patient_info['firstName'],
|
792
933
|
patient_info['middleName'],
|
793
934
|
patient_info['lastName']
|
794
|
-
).strip()
|
935
|
+
).strip()
|
795
936
|
|
796
|
-
|
797
|
-
|
798
|
-
patient_name
|
799
|
-
|
800
|
-
|
801
|
-
|
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
|
+
}
|
802
950
|
|
803
951
|
else:
|
804
952
|
# Unknown response format - log for debugging
|
805
|
-
MediLink_ConfigLoader.log("Unknown response format in
|
953
|
+
MediLink_ConfigLoader.log("Unknown response format in convert_eligibility_to_enhanced_format", level="WARNING")
|
806
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
|
807
976
|
|
808
977
|
# Global mode flags (will be set in main)
|
809
978
|
LEGACY_MODE = False
|
@@ -878,60 +1047,80 @@ if __name__ == "__main__":
|
|
878
1047
|
print("Batch processing cancelled.")
|
879
1048
|
continue
|
880
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
|
881
1107
|
output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
|
882
1108
|
with open(output_file_path, 'w') as output_file:
|
883
1109
|
table_header = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
884
1110
|
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
885
1111
|
output_file.write(table_header + "\n")
|
886
1112
|
output_file.write("-" * len(table_header) + "\n")
|
887
|
-
print(table_header)
|
888
|
-
print("-" * len(table_header))
|
889
|
-
|
890
|
-
# PERFORMANCE FIX: Optimize patient-payer processing to avoid O(PxN) complexity
|
891
|
-
# Instead of nested loops, process each patient once and try payer_ids until success
|
892
|
-
# TODO: We should be able to determine the correct payer_id for each patient ahead of time
|
893
|
-
# by looking up their insurance information from the CSV data or crosswalk mapping.
|
894
|
-
# This would eliminate the need to try multiple payer_ids per patient and make this O(N).
|
895
|
-
# CLARIFICATION: In production, use the payer_id from the CSV/crosswalk as primary.
|
896
|
-
# Retain multi-payer probing behind a DEBUG/DIAGNOSTIC feature toggle only.
|
897
|
-
# Suggested flag: DEBUG_MODE_PAYER_PROBE = False (module-level), default False.
|
898
|
-
errors = []
|
899
|
-
validation_reports = []
|
900
|
-
processed_count = 0
|
901
|
-
validation_files_created = [] # Track validation files that were actually created
|
902
1113
|
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
run_validation = DEBUG_MODE
|
914
|
-
eligibility_data = get_eligibility_info(client, payer_id, provider_last_name, dob, member_id, npi, run_validation=run_validation, is_manual_lookup=False)
|
915
|
-
if eligibility_data is not None:
|
916
|
-
display_eligibility_info(eligibility_data, dob, member_id, output_file)
|
917
|
-
patient_processed = True
|
918
|
-
|
919
|
-
# Track validation file creation in debug mode
|
920
|
-
if DEBUG_MODE:
|
921
|
-
validation_file_path = os.path.join(os.getenv('TEMP'), 'validation_report_{}_{}.txt'.format(member_id, dob))
|
922
|
-
if os.path.exists(validation_file_path):
|
923
|
-
validation_files_created.append(validation_file_path)
|
924
|
-
print(" Validation report created: {}".format(os.path.basename(validation_file_path)))
|
925
|
-
|
926
|
-
break # Stop trying other payer_ids for this patient once we get a response
|
927
|
-
except Exception as e:
|
928
|
-
# Continue trying other payer_ids
|
929
|
-
continue
|
930
|
-
|
931
|
-
# If no payer_id worked for this patient, log the error
|
932
|
-
if not patient_processed:
|
933
|
-
error_msg = "No successful payer_id found for patient"
|
934
|
-
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")
|
935
1124
|
|
936
1125
|
# Display errors if any
|
937
1126
|
if errors:
|