medicafe 0.250930.0__tar.gz → 0.251014.0__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.250930.0 → medicafe-0.251014.0}/MediBot/__init__.py +1 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/__init__.py +1 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/api_core.py +151 -28
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/deductible_utils.py +21 -9
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/graphql_utils.py +143 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_ClaimStatus.py +11 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Deductible.py +1 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Deductible_Validator.py +2 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Display_Utils.py +3 -2
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/__init__.py +1 -1
- {medicafe-0.250930.0/medicafe.egg-info → medicafe-0.251014.0}/PKG-INFO +1 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0/medicafe.egg-info}/PKG-INFO +1 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/setup.py +1 -1
- {medicafe-0.250930.0 → medicafe-0.251014.0}/LICENSE +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MANIFEST.in +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Notepad_Utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_Preprocessor_lib.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_UI.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_debug.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/clear_cache.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/crash_diagnostic.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/f_drive_diagnostic.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/full_debug_suite.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/process_csvs.bat +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/update_json.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/__main__.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/api_factory.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/api_utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/core_utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/error_reporter.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/logging_config.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/smart_import.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediCafe/submission_index.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Charges.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_main.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/test.py +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/MediLink/webapp.html +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/README.md +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/medicafe.egg-info/SOURCES.txt +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250930.0 → medicafe-0.251014.0}/setup.cfg +0 -0
@@ -52,6 +52,16 @@ except ImportError:
|
|
52
52
|
import graphql_utils as MediLink_GraphQL
|
53
53
|
except ImportError:
|
54
54
|
# Create a dummy module if graphql_utils is not available
|
55
|
+
# BUG (minimally fixed): When graphql_utils is unavailable, DummyGraphQL
|
56
|
+
# lacked OPTUMAI claims inquiry helpers used by get_claim_summary_by_provider.
|
57
|
+
# Minimal fix implemented elsewhere: a capability guard disables OPTUMAI when
|
58
|
+
# these methods are missing so fallback to UHCAPI proceeds without AttributeError.
|
59
|
+
# Recommended next steps:
|
60
|
+
# - Provide explicit no-op stubs here that raise NotImplementedError for clarity.
|
61
|
+
# - Add a small shim that always exposes required methods to decouple import shapes.
|
62
|
+
# - Add a config feature flag to enable/disable OPTUMAI claims inquiry.
|
63
|
+
# - Add environment-based guard (dev/test default off) and metrics counter for use.
|
64
|
+
# - Add unit tests for both capability-present and capability-absent paths.
|
55
65
|
class DummyGraphQL:
|
56
66
|
@staticmethod
|
57
67
|
def transform_eligibility_response(response):
|
@@ -155,8 +165,9 @@ def _get_default_endpoint_payer_ids(endpoint_name):
|
|
155
165
|
|
156
166
|
# OPTUMAI – augmented list (subject to growth once the API adds a payer-list endpoint)
|
157
167
|
optumai_payer_ids = [
|
158
|
-
|
159
|
-
"
|
168
|
+
# Supported Payer IDs per Optum Real Claims Inquiry (partial swagger)
|
169
|
+
"87726", "25463", "39026", "74227", "LIFE1", "WELM2", "06111",
|
170
|
+
"96385", "37602", "03432", "95467", "86050", "86047", "95378"
|
160
171
|
]
|
161
172
|
|
162
173
|
if endpoint_name == 'OPTUMAI':
|
@@ -968,30 +979,147 @@ def fetch_payer_name_from_api(*args, **kwargs):
|
|
968
979
|
raise ValueError(final_error_message)
|
969
980
|
|
970
981
|
def get_claim_summary_by_provider(client, tin, first_service_date, last_service_date, payer_id, get_standard_error='false', transaction_id=None, env=None):
|
971
|
-
|
982
|
+
"""
|
983
|
+
Unified Claims Inquiry that prefers OPTUMAI GraphQL searchClaim with
|
984
|
+
legacy response mapping, and falls back to legacy UHCAPI REST endpoint
|
985
|
+
to preserve current downstream flows and UI.
|
986
|
+
"""
|
987
|
+
# Verbose input logging
|
972
988
|
if DEBUG:
|
973
|
-
MediLink_ConfigLoader.log("=
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
MediLink_ConfigLoader.log("First Service Date: {}".format(first_service_date), level="INFO")
|
978
|
-
MediLink_ConfigLoader.log("Last Service Date: {}".format(last_service_date), level="INFO")
|
979
|
-
MediLink_ConfigLoader.log("Payer ID: {}".format(payer_id), level="INFO")
|
980
|
-
MediLink_ConfigLoader.log("Get Standard Error: {}".format(get_standard_error), level="INFO")
|
981
|
-
MediLink_ConfigLoader.log("Transaction ID: {}".format(transaction_id), level="INFO")
|
982
|
-
MediLink_ConfigLoader.log("Environment: {}".format(env), level="INFO")
|
983
|
-
MediLink_ConfigLoader.log("=" * 80, level="INFO")
|
984
|
-
|
985
|
-
endpoint_name = 'UHCAPI'
|
989
|
+
MediLink_ConfigLoader.log("Claims Inquiry inputs: tin={} start={} end={} payer={} tx={}"
|
990
|
+
.format(tin, first_service_date, last_service_date, payer_id, transaction_id),
|
991
|
+
level="INFO")
|
992
|
+
|
986
993
|
from MediCafe.core_utils import extract_medilink_config
|
987
994
|
medi = extract_medilink_config(client.config)
|
988
|
-
|
989
|
-
|
995
|
+
|
996
|
+
# Determine whether OPTUMAI is available/configured
|
997
|
+
endpoints_cfg = medi.get('endpoints', {}) if isinstance(medi, dict) else {}
|
998
|
+
optumai_cfg = endpoints_cfg.get('OPTUMAI', {}) if isinstance(endpoints_cfg, dict) else {}
|
999
|
+
uhcapi_cfg = endpoints_cfg.get('UHCAPI', {}) if isinstance(endpoints_cfg, dict) else {}
|
1000
|
+
|
1001
|
+
optumai_path = (optumai_cfg.get('additional_endpoints', {}) or {}).get('claims_inquiry')
|
1002
|
+
uhc_path = (uhcapi_cfg.get('additional_endpoints', {}) or {}).get('claim_summary_by_provider')
|
1003
|
+
|
1004
|
+
optumai_api_url = optumai_cfg.get('api_url')
|
1005
|
+
use_optumai = bool(optumai_api_url and optumai_path)
|
1006
|
+
# Capability guard: disable OPTUMAI if required helpers are unavailable
|
1007
|
+
if use_optumai:
|
1008
|
+
has_build = hasattr(MediLink_GraphQL, 'build_optumai_claims_inquiry_request')
|
1009
|
+
has_transform = hasattr(MediLink_GraphQL, 'transform_claims_inquiry_response_to_legacy')
|
1010
|
+
if not (has_build and has_transform):
|
1011
|
+
if DEBUG:
|
1012
|
+
MediLink_ConfigLoader.log(
|
1013
|
+
"Disabling OPTUMAI claims inquiry: missing GraphQL helpers (build/transform)",
|
1014
|
+
level="WARNING",
|
1015
|
+
console_output=CONSOLE_LOGGING
|
1016
|
+
)
|
1017
|
+
use_optumai = False
|
1018
|
+
|
1019
|
+
# Validate payer ID list based on endpoint we will attempt
|
1020
|
+
# BUG (minimally fixed): Previously a broad except masked invalid payer IDs and config issues.
|
1021
|
+
# Minimal fix: narrow exceptions and log a clear warning; proceed to maintain backward compatibility.
|
1022
|
+
# Recommended next steps:
|
1023
|
+
# - In non-production environments, fail fast on invalid payer IDs.
|
1024
|
+
# - Return a structured error object for invalid payers to avoid silent pass-through.
|
1025
|
+
# - Add unit tests covering valid/invalid payers and config load failures.
|
1026
|
+
try:
|
1027
|
+
target_endpoint = 'OPTUMAI' if use_optumai else 'UHCAPI'
|
1028
|
+
valid_payers = get_valid_payer_ids_for_endpoint(client, target_endpoint)
|
1029
|
+
if payer_id not in valid_payers:
|
1030
|
+
raise ValueError("Invalid payer_id: {} for endpoint {}. Must be one of: {}".format(
|
1031
|
+
payer_id, target_endpoint, ", ".join(valid_payers)))
|
1032
|
+
except (ValueError, KeyError) as e:
|
1033
|
+
MediLink_ConfigLoader.log(
|
1034
|
+
"Payer validation warning: {}".format(e),
|
1035
|
+
level="WARNING",
|
1036
|
+
console_output=CONSOLE_LOGGING
|
1037
|
+
)
|
1038
|
+
except Exception as e:
|
1039
|
+
MediLink_ConfigLoader.log(
|
1040
|
+
"Payer validation unexpected error: {}".format(e),
|
1041
|
+
level="ERROR",
|
1042
|
+
console_output=CONSOLE_LOGGING
|
1043
|
+
)
|
1044
|
+
|
1045
|
+
# Build OPTUMAI GraphQL request if configured
|
1046
|
+
if use_optumai:
|
1047
|
+
try:
|
1048
|
+
# Compose headers (providerTaxId is required per Optum Real API)
|
1049
|
+
headers = {
|
1050
|
+
'Content-Type': 'application/json',
|
1051
|
+
'Accept': 'application/json'
|
1052
|
+
}
|
1053
|
+
# Map billing_provider_tin to providerTaxId
|
1054
|
+
provider_tin = medi.get('billing_provider_tin')
|
1055
|
+
if provider_tin:
|
1056
|
+
headers['providerTaxId'] = str(provider_tin)
|
1057
|
+
# correlation id for tracing
|
1058
|
+
try:
|
1059
|
+
corr_id = 'mc-ci-{}'.format(int(time.time() * 1000))
|
1060
|
+
except Exception:
|
1061
|
+
corr_id = 'mc-ci-{}'.format(int(time.time()))
|
1062
|
+
headers['x-optum-consumer-correlation-id'] = corr_id
|
1063
|
+
|
1064
|
+
# Environment header for sandbox/stage if URL indicates
|
1065
|
+
try:
|
1066
|
+
api_url_lower = str(optumai_api_url).lower()
|
1067
|
+
if any(tag in api_url_lower for tag in ['sandbox', 'stg', 'stage', 'test']):
|
1068
|
+
headers['environment'] = 'sandbox'
|
1069
|
+
except Exception:
|
1070
|
+
pass
|
1071
|
+
|
1072
|
+
# Build searchClaimInput per partial swagger
|
1073
|
+
search_claim_input = {
|
1074
|
+
'payerId': str(payer_id)
|
1075
|
+
}
|
1076
|
+
# Map dates MM/DD/YYYY -> expected strings for GraphQL (examples show MM/DD/YYYY and also 01/01/2025)
|
1077
|
+
if first_service_date:
|
1078
|
+
search_claim_input['serviceStartDate'] = first_service_date
|
1079
|
+
if last_service_date:
|
1080
|
+
search_claim_input['serviceEndDate'] = last_service_date
|
1081
|
+
|
1082
|
+
# Note: our pagination param is nextPageToken header in swagger; we surface as transaction_id in legacy code
|
1083
|
+
# For subsequent pages, pass it in headers; GraphQL spec shows header, but our call builder only handles body.
|
1084
|
+
# We'll set header when calling API; the transformer maps nextPageToken -> transactionId for downstream.
|
1085
|
+
if transaction_id:
|
1086
|
+
headers['nextPageToken'] = transaction_id
|
1087
|
+
|
1088
|
+
# Build GraphQL body
|
1089
|
+
graphql_body = MediLink_GraphQL.build_optumai_claims_inquiry_request(search_claim_input)
|
1090
|
+
|
1091
|
+
# Make the call
|
1092
|
+
response = client.make_api_call('OPTUMAI', 'POST', optumai_path, params=None, data=graphql_body, headers=headers)
|
1093
|
+
|
1094
|
+
# Transform to legacy format
|
1095
|
+
transformed = MediLink_GraphQL.transform_claims_inquiry_response_to_legacy(response)
|
1096
|
+
status = transformed.get('statuscode') if isinstance(transformed, dict) else None
|
1097
|
+
if status == '200':
|
1098
|
+
# Add note so UI can message that Optum Real is active (non-breaking)
|
1099
|
+
try:
|
1100
|
+
transformed['data_source'] = 'OPTUMAI'
|
1101
|
+
except Exception:
|
1102
|
+
pass
|
1103
|
+
return transformed
|
1104
|
+
# If not 200, fall through to UHC fallback when permitted
|
1105
|
+
except Exception as e:
|
1106
|
+
# If capability guard failed upstream for any reason, handle missing attribute gracefully here too
|
1107
|
+
if isinstance(e, AttributeError):
|
1108
|
+
MediLink_ConfigLoader.log(
|
1109
|
+
"OPTUMAI disabled due to missing GraphQL helpers; falling back to UHCAPI.",
|
1110
|
+
level="WARNING",
|
1111
|
+
console_output=CONSOLE_LOGGING
|
1112
|
+
)
|
1113
|
+
else:
|
1114
|
+
MediLink_ConfigLoader.log("OPTUMAI Claims Inquiry failed: {}".format(e), level="WARNING")
|
1115
|
+
|
1116
|
+
# Fallback to existing UHC REST behavior to preserve current flows
|
1117
|
+
endpoint_name = 'UHCAPI'
|
1118
|
+
url_extension = uhc_path or ''
|
1119
|
+
|
990
1120
|
if DEBUG:
|
991
|
-
MediLink_ConfigLoader.log("
|
992
|
-
|
993
|
-
# Build headers according to official API documentation
|
994
|
-
# Note: Environment detection is now handled automatically by the API client
|
1121
|
+
MediLink_ConfigLoader.log("Falling back to UHCAPI claim_summary_by_provider path: {}".format(url_extension), level="INFO")
|
1122
|
+
|
995
1123
|
headers = {
|
996
1124
|
'tin': tin,
|
997
1125
|
'firstServiceDt': first_service_date,
|
@@ -1000,14 +1128,9 @@ def get_claim_summary_by_provider(client, tin, first_service_date, last_service_
|
|
1000
1128
|
'getStandardError': get_standard_error,
|
1001
1129
|
'Accept': 'application/json'
|
1002
1130
|
}
|
1003
|
-
|
1004
|
-
# Add transactionId if provided (for pagination)
|
1005
1131
|
if transaction_id:
|
1006
1132
|
headers['transactionId'] = transaction_id
|
1007
|
-
|
1008
|
-
if DEBUG:
|
1009
|
-
MediLink_ConfigLoader.log("Headers: {}".format(json.dumps(headers, indent=2)), level="INFO")
|
1010
|
-
|
1133
|
+
|
1011
1134
|
return client.make_api_call(endpoint_name, 'GET', url_extension, params=None, data=None, headers=headers)
|
1012
1135
|
|
1013
1136
|
def get_eligibility(client, payer_id, provider_last_name, search_option, date_of_birth, member_id, npi):
|
@@ -612,7 +612,8 @@ def extract_super_connector_insurance_info(eligibility_data):
|
|
612
612
|
if insurance_info:
|
613
613
|
return {
|
614
614
|
'insuranceType': insurance_info.get("planTypeDescription", ""),
|
615
|
-
|
615
|
+
# Prefer new insuranceTypeCode when available; fallback to productServiceCode
|
616
|
+
'insuranceTypeCode': insurance_info.get("insuranceTypeCode") or insurance_info.get("productServiceCode", ""),
|
616
617
|
'memberId': insurance_info.get("memberId", ""),
|
617
618
|
'payerId': insurance_info.get("payerId", "")
|
618
619
|
}
|
@@ -634,7 +635,8 @@ def extract_super_connector_insurance_info(eligibility_data):
|
|
634
635
|
|
635
636
|
return {
|
636
637
|
'insuranceType': insurance_type,
|
637
|
-
|
638
|
+
# Prefer insuranceTypeCode if present in extensions; fallback to productServiceCode
|
639
|
+
'insuranceTypeCode': detail.get("insuranceTypeCode") or detail.get("productServiceCode", ""),
|
638
640
|
'memberId': detail.get("memberId", ""),
|
639
641
|
'payerId': detail.get("payerId", "")
|
640
642
|
}
|
@@ -656,8 +658,10 @@ def extract_super_connector_insurance_info(eligibility_data):
|
|
656
658
|
elif "POS" in insurance_type:
|
657
659
|
insurance_type = "Point of Service (POS)"
|
658
660
|
|
659
|
-
# Get insurance type code from multiple possible locations
|
660
|
-
insurance_type_code = eligibility_data.get("
|
661
|
+
# Get insurance type code from multiple possible locations (prefer insuranceTypeCode)
|
662
|
+
insurance_type_code = eligibility_data.get("insuranceTypeCode", "")
|
663
|
+
if not insurance_type_code:
|
664
|
+
insurance_type_code = eligibility_data.get("productServiceCode", "")
|
661
665
|
if not insurance_type_code:
|
662
666
|
# Try to get from coverageTypes
|
663
667
|
coverage_types = eligibility_data.get("coverageTypes", [])
|
@@ -767,7 +771,8 @@ def convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id="",
|
|
767
771
|
'service_date_display': service_date,
|
768
772
|
'service_date_sort': datetime.min, # Will be enhanced later
|
769
773
|
'status': 'Processed',
|
770
|
-
|
774
|
+
# Prefer insurance type code over description for downstream validation/UI
|
775
|
+
'insurance_type': insurance_info.get('insuranceTypeCode') or insurance_info.get('insuranceType', ''),
|
771
776
|
'policy_status': policy_status,
|
772
777
|
'remaining_amount': remaining_amount,
|
773
778
|
'data_source': 'Legacy',
|
@@ -803,7 +808,8 @@ def convert_eligibility_to_enhanced_format(data, dob, member_id, patient_id="",
|
|
803
808
|
'service_date_display': service_date,
|
804
809
|
'service_date_sort': datetime.min, # Will be enhanced later
|
805
810
|
'status': 'Processed',
|
806
|
-
|
811
|
+
# Prefer insurance type code over description for downstream validation/UI
|
812
|
+
'insurance_type': insurance_info.get('insuranceTypeCode') or insurance_info.get('insuranceType', ''),
|
807
813
|
'policy_status': policy_status,
|
808
814
|
'remaining_amount': remaining_amount,
|
809
815
|
'data_source': 'OptumAI',
|
@@ -975,7 +981,13 @@ def merge_responses(optumai_data, legacy_data, dob, member_id):
|
|
975
981
|
merged['patient_name'] = "{} {}".format(selected.get('firstName', ''), selected.get('lastName', '')).strip()
|
976
982
|
merged['dob'] = selected.get('dateOfBirth', dob)
|
977
983
|
merged['member_id'] = selected.get('memberId', member_id)
|
978
|
-
|
984
|
+
# Prefer insurance type code from extensions when available
|
985
|
+
merged['insurance_type'] = (
|
986
|
+
selected.get('insuranceTypeCode') or
|
987
|
+
selected.get('productServiceCode') or
|
988
|
+
selected.get('insuranceType') or
|
989
|
+
selected.get('coverageType', '')
|
990
|
+
)
|
979
991
|
merged['policy_status'] = selected.get('policyStatus', 'Active')
|
980
992
|
merged['payer_id'] = selected.get('payerId', '')
|
981
993
|
# Extract deductible from plan levels if available
|
@@ -1008,8 +1020,8 @@ def merge_responses(optumai_data, legacy_data, dob, member_id):
|
|
1008
1020
|
merged['remaining_amount'] = extract_super_connector_remaining_amount(primary)
|
1009
1021
|
merged['policy_status'] = primary.get('policyStatus', '')
|
1010
1022
|
|
1011
|
-
#
|
1012
|
-
merged['insurance_type'] = ''
|
1023
|
+
# Prefer insurance type code directly from OptumAI response
|
1024
|
+
merged['insurance_type'] = primary.get('insuranceTypeCode') or primary.get('productServiceCode', '')
|
1013
1025
|
merged['data_source'] = 'OptumAI'
|
1014
1026
|
|
1015
1027
|
elif is_legacy_response_format(primary) and primary:
|
@@ -218,6 +218,56 @@ class GraphQLQueryBuilder:
|
|
218
218
|
}
|
219
219
|
}
|
220
220
|
|
221
|
+
# ------------------------------------------------------------------
|
222
|
+
# OPTUMAI Claims Inquiry (Real Claim Inquiry) - minimal searchClaim
|
223
|
+
# ------------------------------------------------------------------
|
224
|
+
@staticmethod
|
225
|
+
def get_optumai_claims_inquiry_query():
|
226
|
+
"""
|
227
|
+
Returns a minimal GraphQL query for searchClaim that includes fields
|
228
|
+
necessary to map into the existing legacy claims summary structure
|
229
|
+
(memberInfo, claimSummary, crosswalk data).
|
230
|
+
"""
|
231
|
+
return (
|
232
|
+
"query searchClaim($searchClaimInput: SearchClaimInput!) {\n"
|
233
|
+
" searchClaimResponse(input: $searchClaimInput) {\n"
|
234
|
+
" claims {\n"
|
235
|
+
" claimNumber\n"
|
236
|
+
" claimStatus\n"
|
237
|
+
" member { firstName lastName }\n"
|
238
|
+
" claimEvents { processedDate serviceStartDate }\n"
|
239
|
+
" claimLevelTotalAmount {\n"
|
240
|
+
" totalBilledChargeAmount\n"
|
241
|
+
" totalAllowedAmount\n"
|
242
|
+
" totalPaidAmount\n"
|
243
|
+
" totalPatientResponsibilityAmount\n"
|
244
|
+
" }\n"
|
245
|
+
" claimStatusCrosswalkData {\n"
|
246
|
+
" claim507Code claim507CodeDesc claim508Code claim508CodeDesc adjudicatedClaimSuffixCode\n"
|
247
|
+
" }\n"
|
248
|
+
" }\n"
|
249
|
+
" pagination { hasMoreRecords nextPageToken }\n"
|
250
|
+
" }\n"
|
251
|
+
"}"
|
252
|
+
)
|
253
|
+
|
254
|
+
@staticmethod
|
255
|
+
def build_optumai_claims_inquiry_request(search_claim_input):
|
256
|
+
"""
|
257
|
+
Build the GraphQL request body for searchClaim.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
search_claim_input: dict with keys matching SearchClaimInput (e.g.,
|
261
|
+
payerId (required), serviceStartDate, serviceEndDate, claimNumber, etc.)
|
262
|
+
"""
|
263
|
+
return {
|
264
|
+
"query": GraphQLQueryBuilder.get_optumai_claims_inquiry_query(),
|
265
|
+
"operationName": "searchClaim",
|
266
|
+
"variables": {
|
267
|
+
"searchClaimInput": search_claim_input or {}
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
221
271
|
class GraphQLResponseTransformer:
|
222
272
|
"""Transforms GraphQL responses to match REST API format"""
|
223
273
|
|
@@ -331,7 +381,10 @@ class GraphQLResponseTransformer:
|
|
331
381
|
'productCode': insurance_info.get('productCode'),
|
332
382
|
'lineOfBusiness': insurance_info.get('lineOfBusiness'),
|
333
383
|
'lineOfBusinessCode': insurance_info.get('lineOfBusinessCode'),
|
334
|
-
'coverageTypes': insurance_info.get('coverageTypes', [])
|
384
|
+
'coverageTypes': insurance_info.get('coverageTypes', []),
|
385
|
+
# Expose insurance type fields for downstream logic/UI
|
386
|
+
'insuranceTypeCode': insurance_info.get('insuranceTypeCode'),
|
387
|
+
'insuranceType': insurance_info.get('insuranceType')
|
335
388
|
})
|
336
389
|
|
337
390
|
# Safely extract associated IDs
|
@@ -480,6 +533,87 @@ class GraphQLResponseTransformer:
|
|
480
533
|
'rawGraphQLResponse': graphql_response
|
481
534
|
}
|
482
535
|
|
536
|
+
@staticmethod
|
537
|
+
def transform_claims_inquiry_response_to_legacy(graphql_response):
|
538
|
+
"""
|
539
|
+
Transform OPTUMAI searchClaim GraphQL response into the legacy
|
540
|
+
claims summary format expected by downstream code (e.g.,
|
541
|
+
MediLink_ClaimStatus.extract_claim_data).
|
542
|
+
"""
|
543
|
+
try:
|
544
|
+
# Handle GraphQL errors
|
545
|
+
if isinstance(graphql_response, dict) and 'errors' in graphql_response:
|
546
|
+
first_err = graphql_response['errors'][0] if graphql_response['errors'] else {}
|
547
|
+
code = (first_err.get('extensions', {}) or {}).get('code') or first_err.get('code') or 'GRAPHQL_ERROR'
|
548
|
+
msg = first_err.get('message') or first_err.get('description') or 'GraphQL error'
|
549
|
+
status = '401' if 'AUTH' in str(code).upper() else '500'
|
550
|
+
return {
|
551
|
+
'statuscode': status,
|
552
|
+
'message': '{}: {}'.format(code, msg),
|
553
|
+
'rawGraphQLResponse': graphql_response
|
554
|
+
}
|
555
|
+
|
556
|
+
data = (graphql_response or {}).get('data') or {}
|
557
|
+
search_resp = data.get('searchClaimResponse') or {}
|
558
|
+
claims = search_resp.get('claims') or []
|
559
|
+
pagination = search_resp.get('pagination') or {}
|
560
|
+
|
561
|
+
legacy_claims = []
|
562
|
+
for c in claims:
|
563
|
+
member = c.get('member') or {}
|
564
|
+
events = c.get('claimEvents') or {}
|
565
|
+
totals = c.get('claimLevelTotalAmount') or {}
|
566
|
+
xwalk_list = c.get('claimStatusCrosswalkData') or []
|
567
|
+
|
568
|
+
# Map crosswalk to legacy keys list
|
569
|
+
legacy_xwalk = []
|
570
|
+
for x in xwalk_list:
|
571
|
+
legacy_xwalk.append({
|
572
|
+
'clm507Cd': x.get('claim507Code'),
|
573
|
+
'clm507CdDesc': x.get('claim507CodeDesc'),
|
574
|
+
'clm508Cd': x.get('claim508Code'),
|
575
|
+
'clm508CdDesc': x.get('claim508CodeDesc'),
|
576
|
+
'clmIcnSufxCd': x.get('adjudicatedClaimSuffixCode')
|
577
|
+
})
|
578
|
+
|
579
|
+
legacy_claims.append({
|
580
|
+
'claimNumber': c.get('claimNumber'),
|
581
|
+
'claimStatus': c.get('claimStatus'),
|
582
|
+
'memberInfo': {
|
583
|
+
'ptntFn': member.get('firstName'),
|
584
|
+
'ptntLn': member.get('lastName')
|
585
|
+
},
|
586
|
+
'claimSummary': {
|
587
|
+
'processedDt': events.get('processedDate') or '',
|
588
|
+
'firstSrvcDt': events.get('serviceStartDate') or '',
|
589
|
+
'totalChargedAmt': (totals.get('totalBilledChargeAmount') or ''),
|
590
|
+
'totalAllowdAmt': (totals.get('totalAllowedAmount') or ''),
|
591
|
+
'totalPaidAmt': (totals.get('totalPaidAmount') or ''),
|
592
|
+
'totalPtntRespAmt': (totals.get('totalPatientResponsibilityAmount') or ''),
|
593
|
+
'clmXWalkData': legacy_xwalk
|
594
|
+
}
|
595
|
+
})
|
596
|
+
|
597
|
+
next_token = pagination.get('nextPageToken')
|
598
|
+
has_more = bool(pagination.get('hasMoreRecords'))
|
599
|
+
|
600
|
+
transformed = {
|
601
|
+
'statuscode': '200',
|
602
|
+
'message': 'Claims found' if legacy_claims else 'No claims found',
|
603
|
+
'claims': legacy_claims,
|
604
|
+
# Maintain legacy pagination signaling by mapping to 'transactionId'
|
605
|
+
'transactionId': next_token if has_more else None,
|
606
|
+
'rawGraphQLResponse': graphql_response
|
607
|
+
}
|
608
|
+
return transformed
|
609
|
+
except Exception as e:
|
610
|
+
print("Error transforming Claims Inquiry response: {}".format(str(e)))
|
611
|
+
return {
|
612
|
+
'statuscode': '500',
|
613
|
+
'message': 'Error processing Claims Inquiry response: {}'.format(str(e)),
|
614
|
+
'rawGraphQLResponse': graphql_response
|
615
|
+
}
|
616
|
+
|
483
617
|
# Convenience functions for easy access
|
484
618
|
|
485
619
|
def get_eligibility_query():
|
@@ -502,10 +636,18 @@ def build_optumai_enriched_request(variables):
|
|
502
636
|
"""Build enriched GraphQL request body for OPTUMAI"""
|
503
637
|
return GraphQLQueryBuilder.build_optumai_enriched_request(variables)
|
504
638
|
|
639
|
+
def build_optumai_claims_inquiry_request(search_claim_input):
|
640
|
+
"""Build GraphQL request body for OPTUMAI searchClaim"""
|
641
|
+
return GraphQLQueryBuilder.build_optumai_claims_inquiry_request(search_claim_input)
|
642
|
+
|
505
643
|
def transform_eligibility_response(graphql_response):
|
506
644
|
"""Transform GraphQL eligibility response to REST format"""
|
507
645
|
return GraphQLResponseTransformer.transform_eligibility_response(graphql_response)
|
508
646
|
|
647
|
+
def transform_claims_inquiry_response_to_legacy(graphql_response):
|
648
|
+
"""Transform OPTUMAI searchClaim response to legacy claims summary format"""
|
649
|
+
return GraphQLResponseTransformer.transform_claims_inquiry_response_to_legacy(graphql_response)
|
650
|
+
|
509
651
|
def get_sample_eligibility_request():
|
510
652
|
"""Get the sample GraphQL request from swagger documentation"""
|
511
653
|
return GraphQLQueryBuilder.get_sample_eligibility_request()
|
@@ -263,6 +263,17 @@ def process_claims_with_pagination(client, billing_provider_tin, start_date_str,
|
|
263
263
|
client, billing_provider_tin, start_date_str, end_date_str,
|
264
264
|
payer_id=payer_id, transaction_id=transaction_id
|
265
265
|
)
|
266
|
+
# Informational notice if new OPTUM Real endpoint is being used
|
267
|
+
try:
|
268
|
+
if isinstance(claim_summary, dict) and claim_summary.get('data_source') == 'OPTUMAI':
|
269
|
+
MediLink_ConfigLoader.log(
|
270
|
+
"Claims Inquiry via Optum Real endpoint (OPTUMAI) with legacy-compatible output.",
|
271
|
+
level="INFO"
|
272
|
+
)
|
273
|
+
if DEBUG:
|
274
|
+
print("[Info] Using Optum Real Claims Inquiry for payer {}".format(payer_id))
|
275
|
+
except Exception:
|
276
|
+
pass
|
266
277
|
|
267
278
|
# Extract claims from this page
|
268
279
|
claims = claim_summary.get('claims', [])
|
@@ -1027,7 +1027,7 @@ if __name__ == "__main__":
|
|
1027
1027
|
output_file_path = os.path.join(os.getenv('TEMP'), 'eligibility_report.txt')
|
1028
1028
|
with open(output_file_path, 'w') as output_file:
|
1029
1029
|
table_header = "{:<20} | {:<10} | {:<40} | {:<5} | {:<14} | {:<14}".format(
|
1030
|
-
"Patient Name", "DOB", "Insurance Type", "PayID", "Policy Status", "Remaining Amt")
|
1030
|
+
"Patient Name", "DOB", "Insurance Type Code", "PayID", "Policy Status", "Remaining Amt")
|
1031
1031
|
output_file.write(table_header + "\n")
|
1032
1032
|
output_file.write("-" * len(table_header) + "\n")
|
1033
1033
|
|
@@ -72,7 +72,8 @@ def extract_legacy_values_for_comparison(legacy_data):
|
|
72
72
|
|
73
73
|
# Extract insurance info
|
74
74
|
insurance_info = policy.get("insuranceInfo", {})
|
75
|
-
|
75
|
+
# Prefer standardized insuranceTypeCode for comparisons; keep description as secondary
|
76
|
+
comparison_values["insurance_type"] = insurance_info.get("insuranceTypeCode", "") or insurance_info.get("insuranceType", "")
|
76
77
|
comparison_values["insurance_typeCode"] = insurance_info.get("insuranceTypeCode", "")
|
77
78
|
comparison_values["insurance_memberId"] = insurance_info.get("memberId", "")
|
78
79
|
comparison_values["insurance_payerId"] = insurance_info.get("payerId", "")
|
@@ -66,10 +66,11 @@ def display_file_summary(index, summary):
|
|
66
66
|
summary.get('user_preferred_endpoint') or
|
67
67
|
summary.get('suggested_endpoint', 'AVAILITY'))
|
68
68
|
|
69
|
-
# Format insurance type for display -
|
69
|
+
# Format insurance type for display - prioritize code (SBR09/insuranceTypeCode)
|
70
70
|
if insurance_type and len(insurance_type) <= 3:
|
71
71
|
insurance_display = insurance_type
|
72
72
|
else:
|
73
|
+
# If description was provided instead of code, truncate respectfully
|
73
74
|
insurance_display = insurance_type[:3] if insurance_type else '--'
|
74
75
|
|
75
76
|
# Shorten source for compact display
|
@@ -384,7 +385,7 @@ def _display_table_header(col_widths, context):
|
|
384
385
|
"Member ID", col_widths['member_id'],
|
385
386
|
"Payer ID", col_widths['payer_id'],
|
386
387
|
"Service Date", col_widths['service_date'],
|
387
|
-
"Insurance Type", col_widths['insurance_type'],
|
388
|
+
"Insurance Type Code", col_widths['insurance_type'],
|
388
389
|
"Policy Status", col_widths['policy_status'],
|
389
390
|
"Remaining Amt", col_widths['remaining_amount']
|
390
391
|
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|