medicafe 0.250820.7__py3-none-any.whl → 0.250822.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MediBot/MediBot.py +16 -0
- MediBot/MediBot_Charges.py +133 -0
- MediBot/MediBot_UI.py +42 -1
- MediBot/__init__.py +1 -1
- MediBot/update_medicafe.py +66 -24
- MediCafe/__init__.py +1 -1
- MediCafe/api_core.py +164 -7
- MediCafe/graphql_utils.py +66 -1
- MediCafe/smart_import.py +2 -1
- MediLink/MediLink_Charges.py +517 -0
- MediLink/MediLink_Deductible.py +91 -45
- MediLink/MediLink_Deductible_Validator.py +40 -3
- MediLink/__init__.py +1 -1
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/METADATA +1 -1
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/RECORD +19 -18
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/LICENSE +0 -0
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/WHEEL +0 -0
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250820.7.dist-info → medicafe-0.250822.1.dist-info}/top_level.txt +0 -0
MediBot/MediBot.py
CHANGED
@@ -863,6 +863,22 @@ if __name__ == "__main__":
|
|
863
863
|
if _ac():
|
864
864
|
_ac().set_pause_status(True)
|
865
865
|
_ = manage_script_pause(csv_data, error_message, reverse_mapping)
|
866
|
+
# Prototype charges enrichment - optional and safe-failed to avoid disrupting existing workflow
|
867
|
+
if e_state.config.get('ENABLE_CHARGES_ENRICHMENT', False):
|
868
|
+
try:
|
869
|
+
from MediCafe.smart_import import get_components
|
870
|
+
MediBot_Charges = get_components('medibot_charges')
|
871
|
+
|
872
|
+
field_mapping = MediBot_Preprocessor_lib.field_mapping # Ensure defined for enrichment
|
873
|
+
|
874
|
+
csv_data, field_mapping, reverse_mapping, fixed_values = MediBot_Charges.enrich_with_charges(
|
875
|
+
csv_data, field_mapping, reverse_mapping, fixed_values
|
876
|
+
)
|
877
|
+
except Exception as e:
|
878
|
+
MediLink_ConfigLoader.log("Charges enrichment failed (prototype): {}. Continuing without enrichment.".format(e), level="WARNING")
|
879
|
+
print("Warning: Charges feature skipped due to error. Proceeding with standard flow.")
|
880
|
+
else:
|
881
|
+
MediLink_ConfigLoader.log("Charges enrichment disabled in config. Skipping.", level="INFO")
|
866
882
|
data_entry_loop(csv_data, MediBot_Preprocessor_lib.field_mapping, reverse_mapping, fixed_values)
|
867
883
|
cleanup()
|
868
884
|
else:
|
MediBot/MediBot_Charges.py
CHANGED
@@ -0,0 +1,133 @@
|
|
1
|
+
"""MediBot_Charges.py
|
2
|
+
|
3
|
+
Overview
|
4
|
+
--------
|
5
|
+
This module provides helper functions for MediBot.py to calculate and bundle charges for entry into Medisoft, based on patient data from MediBot (e.g., full patient details plus user-input anesthesia minutes). It focuses on ophthalmology clinics performing cataract surgeries, where patients typically undergo procedures on both eyes as separate events but require price bundling for consistent billing. Charges are generally calculated as piecewise step-functions based on anesthesia minutes (aligned with 837p units/minutes), exhibiting linear mx+b behavior.
|
6
|
+
|
7
|
+
The module enriches patient data for backend use in MediBot.py, potentially including flags/tags for billing readiness (e.g., indicating prior surgeries, calculated charges, or bundling status). It does not handle UI display directly—that's for MediBot_UI.py or similar. Output feeds into MediLink for generating 837p files; consider enriching data in 837p-compatible format (e.g., Loop syntax for charges/minutes) for easier conversion to human-readable UI. See MediLink_Charges_Integration_Analysis.md for integration details and safety considerations.
|
8
|
+
|
9
|
+
Key Features
|
10
|
+
------------
|
11
|
+
- Supports batch processing of up to 200 patients (typical: ~20), though serial processing is preferred for user input. Add a "save partial" flag to mitigate risks of inconsistent enriched data if batch fails midway.
|
12
|
+
- Implements price bundling to balance charges across multiple procedures (e.g., both eyes), ensuring even total costs per eye to meet patient expectations. Bundling is not affected by unmet deductibles (which instead flag claims as Pending for Deductible to delay sending).
|
13
|
+
- Flags "bundling_pending" by defaulting to expect a second procedure within 30 days from first date of service (process refund if expired). Speculate partly on diagnosis codes (e.g., ICD-10 H25.xx for bilateral cataracts), but note variability (e.g., glaucoma in one eye or prior surgery elsewhere) makes this imperfect—err on expecting return visits.
|
14
|
+
- Uses tiered pricing schedules from MediLink_Charges.py (loaded via configuration or defaults):
|
15
|
+
- Private insurance:
|
16
|
+
- $450 for 1-15 minutes
|
17
|
+
- $540 for 16-34 minutes
|
18
|
+
- $580 for 35-59 minutes (maximum allowed duration; cap at 59 if exceeded)
|
19
|
+
- Medicare: Placeholder tiers (e.g., $300 for 1-30, $350 for 31-45, $400 for 46-59); override via configuration if different from private.
|
20
|
+
- Integrates with historical transaction data (e.g., via MediSoft's MATRAN file, details TBD) for lookups, especially for patients with prior surgeries to support bundling. Never alters MATRAN data—print user notifications for any needed modifications (e.g., "Edit MATRAN for Patient ID: [ID] - Adjust prior charge from [old] to [new] for bundling"). For MediCafe-owned records, adjustments are fine.
|
21
|
+
|
22
|
+
Limitations and Assumptions
|
23
|
+
---------------------------
|
24
|
+
- Bundling for commercial insurance depends on deductibles, claim submission timing (e.g., within 90 days of service), refund timing, and policy conditions. This is not fully specified but aims to minimize patient invoices due to non-payment risks post-surgery. Unmet deductibles flag claims but do not alter bundling.
|
25
|
+
- Does not synchronize perfectly with procedure timing (date of service) or insurance events; bundling may fail if conditions like deductibles are unmet. Over-flagging "bundling_pending" could lead to unnecessary prep—monitor via logs.
|
26
|
+
- Assumes serial processing: User inputs minutes per patient, module enriches data, repeat. Batch processing is inefficient without all data (e.g., minutes) available upfront. If batch fails midway, use "save partial" flag for recovery; no automatic rollback.
|
27
|
+
- Medicare handling uses placeholders; assumes similarity to private insurance unless configured otherwise.
|
28
|
+
- For missing historical data: Notify user (print, similar to MAINS failures; minimal logging to avoid DEBUG/INFO clutter [[memory:4184036]]), use any existing first/second visit data; prompt manually if none available (e.g., display table-like line from enhanced patient table, user fills 'minutes' column, line progresses with added charges and pre-loaded deductible data if available).
|
29
|
+
- For durations >59 minutes: Cap at 59, log minimal INFO warning (no HIPAA data), and print notify for user confirmation (rare case, likely typo).
|
30
|
+
- No direct risk to existing functionality, but bundling logic could affect billing accuracy if assumptions about insurance conditions are incorrect (e.g., over-bundling risks claim rejections; user errors in manual MATRAN edits could break audits if notifications aren't precise).
|
31
|
+
|
32
|
+
Usage
|
33
|
+
-----
|
34
|
+
Intended as a data enricher for MediBot.py to automate Medisoft charge entry. Example flow:
|
35
|
+
1. Receive patient data from MediBot.py.
|
36
|
+
2. User inputs anesthesia minutes (possibly via enhanced patient table in MediBot_UI.py; extend display_enhanced_patient_table() with serial pauses/prompts).
|
37
|
+
3. Enrich data with calculated/bundled charges, flags (e.g., bundling_pending, Pending for Deductible), and 837p-compatible fields.
|
38
|
+
4. Pass enriched data back for MediSoft entry or 837p generation. For prior billed charges, lookup without alteration; notify user for manual MATRAN edits if needed.
|
39
|
+
|
40
|
+
Integration Notes
|
41
|
+
-----------------
|
42
|
+
- Integrate with MediBot_UI.py for displaying an enhanced patient table, user input for minutes (table-like prompts where user fills 'minutes' column, line progresses), and historical lookups (e.g., for prior charges in bundling). Handle UI pauses serially; use "save partial" flag on errors.
|
43
|
+
- Run serially per patient for real-time enrichment during input.
|
44
|
+
- Future: Specify how to display bundled charges in UI and handle MATRAN file parsing for transactions (read-only).
|
45
|
+
|
46
|
+
Compatibility
|
47
|
+
-------------
|
48
|
+
Always ensure XP SP3 + Python 3.4.4 + ASCII-only environment compatibility. Avoid f-strings and include inline commentary where assumptions may differ (e.g., Medicare handling).
|
49
|
+
|
50
|
+
Note: This module is a helper; place any additional helper functions in a separate .py file per preferences.
|
51
|
+
"""
|
52
|
+
|
53
|
+
from MediCafe.smart_import import get_components
|
54
|
+
from MediCafe.core_utils import get_config_loader_with_fallback
|
55
|
+
|
56
|
+
# Import MediLink_Charges via smart_import (ensures no circular imports)
|
57
|
+
MediLink_Charges = get_components('medilink_charges')
|
58
|
+
MediLink_ConfigLoader = get_config_loader_with_fallback() # Using centralized config loader for consistency with MediBot.py
|
59
|
+
|
60
|
+
# Prototype function to enrich data with charges (called from MediBot.py TODO)
|
61
|
+
def enrich_with_charges(csv_data, field_mapping, reverse_mapping, fixed_values):
|
62
|
+
# Inline commentary: This function enriches csv_data with charges using MediLink_Charges.
|
63
|
+
# Assumes serial processing; add "save partial" flag for batch recovery.
|
64
|
+
# No alterations to MATRAN - read-only lookups with user notifications.
|
65
|
+
# XP/Python 3.4.4 compatible: No f-strings, ASCII-only.
|
66
|
+
|
67
|
+
enriched_data = []
|
68
|
+
for row in csv_data:
|
69
|
+
# Get anesthesia minutes (prototype: assume from user input via UI prompt)
|
70
|
+
minutes = get_anesthesia_minutes(row, reverse_mapping) # Placeholder call to UI prompt
|
71
|
+
|
72
|
+
# Cap minutes at 59
|
73
|
+
if minutes > 59:
|
74
|
+
minutes = 59
|
75
|
+
print("Capped duration to 59 minutes for patient ID: {}".format(row.get(reverse_mapping.get('Patient ID #2', ''), 'Unknown')))
|
76
|
+
# Minimal log (no HIPAA data)
|
77
|
+
if MediLink_ConfigLoader:
|
78
|
+
MediLink_ConfigLoader.log("Capped duration to 59 minutes", level="INFO")
|
79
|
+
|
80
|
+
# Determine insurance type (prototype: from row)
|
81
|
+
insurance_type = 'medicare' if 'MEDICARE' in row.get(reverse_mapping.get('Primary Insurance', ''), '').upper() else 'private'
|
82
|
+
|
83
|
+
# Calculate charge using MediLink_Charges
|
84
|
+
charge_info = MediLink_Charges.calculate_procedure_charge(
|
85
|
+
minutes=minutes,
|
86
|
+
insurance_type=insurance_type,
|
87
|
+
procedure_code=row.get(reverse_mapping.get('Procedure Code', ''), '66984'), # Default cataract code
|
88
|
+
service_date=datetime.strptime(row.get('Surgery Date', ''), '%m-%d-%Y'),
|
89
|
+
patient_id=row.get(reverse_mapping.get('Patient ID #2', ''), '')
|
90
|
+
)
|
91
|
+
|
92
|
+
# Bundling and flagging (prototype: check for multi-eye)
|
93
|
+
prior_charges = lookup_historical_charges(charge_info.patient_id) # Read-only MATRAN lookup
|
94
|
+
if prior_charges:
|
95
|
+
bundled = MediLink_Charges.bundle_bilateral_charges([charge_info] + prior_charges)
|
96
|
+
# Notify user for MATRAN edits if needed (no alterations)
|
97
|
+
print("Edit MATRAN for Patient ID: {} - Adjust prior charge from {} to {} for bundling".format(
|
98
|
+
charge_info.patient_id, prior_charges[0].base_charge, bundled[0].adjusted_charge))
|
99
|
+
else:
|
100
|
+
# Flag bundling_pending: Expect second within 30 days, based on diagnosis
|
101
|
+
diagnosis = row.get(reverse_mapping.get('Diagnosis Code', ''), '')
|
102
|
+
if 'H25' in diagnosis.upper(): # Speculative bilateral check
|
103
|
+
charge_info.bundling_group = 'bundling_pending' # Refund if expires after 30 days
|
104
|
+
|
105
|
+
# Enrich row (prototype: add fields)
|
106
|
+
row['Calculated Charge'] = str(charge_info.adjusted_charge)
|
107
|
+
row['Minutes'] = charge_info.minutes
|
108
|
+
# Add flags (e.g., Pending for Deductible - prototype check)
|
109
|
+
if check_deductible_unmet(row): # Placeholder
|
110
|
+
row['Status'] = 'Pending for Deductible'
|
111
|
+
|
112
|
+
enriched_data.append(row)
|
113
|
+
|
114
|
+
return enriched_data, field_mapping, reverse_mapping, fixed_values # As per TODO
|
115
|
+
|
116
|
+
# Prototype helpers (add more as needed)
|
117
|
+
def get_anesthesia_minutes(row, reverse_mapping):
|
118
|
+
# Inline: Use interactive table from MediBot_UI for input.
|
119
|
+
from MediBot_UI import display_enhanced_patient_table
|
120
|
+
# Wrap single row as list (prototype)
|
121
|
+
patient_info = [(row.get('Surgery Date', ''), row.get(reverse_mapping.get('Patient Name', ''), 'Unknown'),
|
122
|
+
row.get(reverse_mapping.get('Patient ID #2', ''), ''), row.get('Diagnosis Code', ''), row)]
|
123
|
+
enriched_info = display_enhanced_patient_table(patient_info, "Enter Minutes for Patient", interactive=True)
|
124
|
+
return enriched_info[0][4]['Minutes'] # Extract from enriched dict in tuple
|
125
|
+
|
126
|
+
def lookup_historical_charges(patient_id):
|
127
|
+
# Inline: Read-only MATRAN lookup (details TBD). Never alter.
|
128
|
+
# If missing, prompt user via table-like UI.
|
129
|
+
return [] # Prototype: Return list of prior ChargeInfo
|
130
|
+
|
131
|
+
def check_deductible_unmet(row):
|
132
|
+
# Inline: Prototype check; doesn't affect bundling.
|
133
|
+
return True # Placeholder
|
MediBot/MediBot_UI.py
CHANGED
@@ -28,7 +28,7 @@ except ImportError:
|
|
28
28
|
from MediCafe.core_utils import create_config_cache
|
29
29
|
_get_config, (_config_cache, _crosswalk_cache) = create_config_cache()
|
30
30
|
|
31
|
-
def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
31
|
+
def display_enhanced_patient_table(patient_info, title, show_line_numbers=True, interactive=False):
|
32
32
|
"""
|
33
33
|
Display an enhanced patient table with multiple surgery dates and diagnosis codes.
|
34
34
|
|
@@ -189,6 +189,47 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
|
189
189
|
primary_format = " {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
|
190
190
|
print(primary_format.format(formatted_date, patient_id, patient_name, display_diagnosis))
|
191
191
|
|
192
|
+
if interactive:
|
193
|
+
for i in range(len(patient_info)):
|
194
|
+
# Redraw table up to current row (prototype: simple print)
|
195
|
+
print("\n" + title)
|
196
|
+
print("|" + "-" * 80 + "|") # ASCII table border
|
197
|
+
print("| Date | Name | ID | Diagnosis | Minutes | Deductible | Charge | Flags |")
|
198
|
+
print("|" + "-" * 80 + "|")
|
199
|
+
|
200
|
+
# Print previous rows (enriched)
|
201
|
+
for j in range(i):
|
202
|
+
r = patient_info[j]
|
203
|
+
print("| {} | {} | {} | {} | {} | {} | {} | {} |".format(
|
204
|
+
r[0], r[1][:10], r[2], r[3], r.get('Minutes', ''), r.get('Deductible', 'N/A'), r.get('Charge', ''), r.get('Flags', '')))
|
205
|
+
|
206
|
+
# Prompt for current row
|
207
|
+
current = patient_info[i]
|
208
|
+
print("| {} | {} | {} | {} | [Input] | {} | | |".format(
|
209
|
+
current[0], current[1][:10], current[2], current[3], current.get('Deductible', 'N/A'))) # Highlight Minutes column
|
210
|
+
|
211
|
+
while True:
|
212
|
+
try:
|
213
|
+
minutes_str = raw_input("Enter Minutes (1-59): ")
|
214
|
+
minutes = int(minutes_str)
|
215
|
+
if minutes > 59:
|
216
|
+
minutes = 59
|
217
|
+
confirm = raw_input("Capped to 59. Proceed? (Y/N): ").upper()
|
218
|
+
if confirm != 'Y': continue
|
219
|
+
if 1 <= minutes <= 59: break
|
220
|
+
print("Invalid: 1-59 only.")
|
221
|
+
except ValueError:
|
222
|
+
print("Invalid: Enter number.")
|
223
|
+
|
224
|
+
# Enrich and update row
|
225
|
+
enriched_row = enrich_single_row(current, minutes, patient_info[i][4]) # Pass original row dict
|
226
|
+
patient_info[i] = (enriched_row['Surgery Date'], enriched_row['Patient Name'], enriched_row['Patient ID #2'], enriched_row['Diagnosis Code'], enriched_row) # Update tuple
|
227
|
+
|
228
|
+
# Redraw full table after update
|
229
|
+
# (Repeat print logic above with all rows up to i)
|
230
|
+
|
231
|
+
return patient_info # Enriched
|
232
|
+
|
192
233
|
# Function to check if a specific key is pressed
|
193
234
|
def _get_vk_codes():
|
194
235
|
"""Get VK codes from config."""
|
MediBot/__init__.py
CHANGED
MediBot/update_medicafe.py
CHANGED
@@ -268,9 +268,17 @@ def verify_post_install(package, expected_version):
|
|
268
268
|
|
269
269
|
|
270
270
|
def upgrade_package(package):
|
271
|
-
#
|
272
|
-
strategies
|
271
|
+
# Default to using existing system packages: skip dependencies first
|
272
|
+
# Light strategies: --no-deps to avoid heavy reinstall of dependencies
|
273
|
+
light_strategies = [
|
274
|
+
['install', '--upgrade', '--no-deps', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check'],
|
275
|
+
['install', '--upgrade', '--no-deps', package, '--no-cache-dir', '--disable-pip-version-check']
|
276
|
+
]
|
277
|
+
|
278
|
+
# Heavy strategies: allow dependencies as a last resort
|
279
|
+
heavy_strategies = [
|
273
280
|
['install', '--upgrade', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check'],
|
281
|
+
['install', '--upgrade', package, '--no-cache-dir', '--disable-pip-version-check'],
|
274
282
|
['install', '--upgrade', '--force-reinstall', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check'],
|
275
283
|
['install', '--upgrade', '--force-reinstall', '--ignore-installed', '--user', package + '[binary]', '--no-cache-dir', '--disable-pip-version-check']
|
276
284
|
]
|
@@ -279,9 +287,11 @@ def upgrade_package(package):
|
|
279
287
|
if not latest_before:
|
280
288
|
print_status('WARNING', 'Unable to determine latest version from PyPI; proceeding with blind install')
|
281
289
|
|
282
|
-
|
290
|
+
print_status('INFO', 'Using existing system packages when possible (skipping dependencies)')
|
291
|
+
|
292
|
+
for idx, parts in enumerate(light_strategies):
|
283
293
|
attempt = idx + 1
|
284
|
-
print_section("
|
294
|
+
print_section("Light attempt {}".format(attempt))
|
285
295
|
cmd = [sys.executable, '-m', 'pip'] + parts
|
286
296
|
print_status('INFO', 'Running: {} -m pip {}'.format(sys.executable, ' '.join(parts)))
|
287
297
|
code, out, err = run_pip_install(cmd)
|
@@ -305,26 +315,58 @@ def upgrade_package(package):
|
|
305
315
|
print(tail)
|
306
316
|
except Exception:
|
307
317
|
pass
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
318
|
+
print_status('WARNING', 'pip returned non-zero exit code ({})'.format(code))
|
319
|
+
|
320
|
+
# If we reached here, light attempts did not succeed conclusively
|
321
|
+
# Check command-line flags for non-interactive approval
|
322
|
+
auto_yes = False
|
323
|
+
for arg in sys.argv[1:]:
|
324
|
+
if arg.strip().lower() in ('--aggressive', '--yes-deps', '--full-deps'):
|
325
|
+
auto_yes = True
|
326
|
+
break
|
327
|
+
|
328
|
+
proceed_heavy = auto_yes
|
329
|
+
if not proceed_heavy:
|
330
|
+
print_section('Confirmation required')
|
331
|
+
print_status('INFO', 'Light update did not complete. A full dependency reinstall may take a long time.')
|
332
|
+
print_status('INFO', 'Proceeding will uninstall/reinstall related packages as needed (e.g., requests, lxml, msal).')
|
333
|
+
try:
|
334
|
+
answer = input('Proceed with full dependency update? (y/N): ').strip().lower()
|
335
|
+
except Exception:
|
336
|
+
answer = ''
|
337
|
+
proceed_heavy = (answer == 'y' or answer == 'yes')
|
338
|
+
|
339
|
+
if not proceed_heavy:
|
340
|
+
print_status('INFO', 'User declined full dependency update. Keeping existing dependencies.')
|
341
|
+
return False
|
342
|
+
|
343
|
+
print_section('Full dependency update')
|
344
|
+
print_status('INFO', 'Running heavy update with dependencies (this may take several minutes)')
|
345
|
+
|
346
|
+
for idx, parts in enumerate(heavy_strategies):
|
347
|
+
attempt = idx + 1
|
348
|
+
print_section('Heavy attempt {}'.format(attempt))
|
349
|
+
cmd = [sys.executable, '-m', 'pip'] + parts
|
350
|
+
print_status('INFO', 'Running: {} -m pip {}'.format(sys.executable, ' '.join(parts)))
|
351
|
+
code, out, err = run_pip_install(cmd)
|
352
|
+
if code == 0:
|
353
|
+
ok, installed = verify_post_install(package, latest_before)
|
354
|
+
if ok:
|
355
|
+
print_status('SUCCESS', 'Installed version: {}'.format(installed))
|
356
|
+
return True
|
357
|
+
else:
|
358
|
+
print_status('WARNING', 'Install returned success but version not updated yet{}'.format(
|
359
|
+
'' if not installed else ' (detected {})'.format(installed)))
|
360
|
+
else:
|
361
|
+
combined = err or out
|
362
|
+
if combined:
|
363
|
+
try:
|
364
|
+
lines3 = combined.strip().splitlines()
|
365
|
+
tail3 = '\n'.join(lines3[-15:])
|
366
|
+
if tail3:
|
367
|
+
print(tail3)
|
368
|
+
except Exception:
|
369
|
+
pass
|
328
370
|
print_status('WARNING', 'pip returned non-zero exit code ({})'.format(code))
|
329
371
|
|
330
372
|
return False
|
MediCafe/__init__.py
CHANGED
MediCafe/api_core.py
CHANGED
@@ -861,8 +861,50 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
861
861
|
if payer_id not in valid_payer_ids:
|
862
862
|
raise ValueError("Invalid payer_id: {}. Must be one of: {}".format(payer_id, ", ".join(valid_payer_ids)))
|
863
863
|
|
864
|
-
|
865
|
-
|
864
|
+
# Prefer OPTUMAI endpoint if configured, otherwise fall back to legacy UHCAPI super connector
|
865
|
+
try:
|
866
|
+
endpoints_cfg = client.config['MediLink_Config']['endpoints']
|
867
|
+
except Exception:
|
868
|
+
endpoints_cfg = {}
|
869
|
+
|
870
|
+
endpoint_name = None
|
871
|
+
url_extension = None
|
872
|
+
|
873
|
+
try:
|
874
|
+
optumai_cfg = endpoints_cfg.get('OPTUMAI', {})
|
875
|
+
optumai_additional = optumai_cfg.get('additional_endpoints', {}) if isinstance(optumai_cfg, dict) else {}
|
876
|
+
optumai_url = optumai_additional.get('eligibility_optumai')
|
877
|
+
if optumai_cfg and optumai_cfg.get('api_url') and optumai_url:
|
878
|
+
endpoint_name = 'OPTUMAI'
|
879
|
+
url_extension = optumai_url
|
880
|
+
except Exception:
|
881
|
+
# Safe ignore; will fall back below
|
882
|
+
pass
|
883
|
+
|
884
|
+
if not endpoint_name:
|
885
|
+
# Fallback to legacy UHCAPI super connector path for backward compatibility
|
886
|
+
endpoint_name = 'UHCAPI'
|
887
|
+
try:
|
888
|
+
url_extension = endpoints_cfg.get(endpoint_name, {}).get('additional_endpoints', {}).get('eligibility_super_connector')
|
889
|
+
except Exception:
|
890
|
+
url_extension = None
|
891
|
+
|
892
|
+
if not url_extension:
|
893
|
+
raise ValueError("Eligibility endpoint not configured for {}".format(endpoint_name))
|
894
|
+
|
895
|
+
# Debug/trace: indicate which endpoint/path is being used for the Super Connector call
|
896
|
+
try:
|
897
|
+
MediLink_ConfigLoader.log(
|
898
|
+
"Super Connector eligibility using endpoint '{}' with path '{}'".format(endpoint_name, url_extension),
|
899
|
+
level="INFO",
|
900
|
+
console_output=CONSOLE_LOGGING
|
901
|
+
)
|
902
|
+
except Exception:
|
903
|
+
pass
|
904
|
+
try:
|
905
|
+
print("[Eligibility] Using endpoint: {} (path: {})".format(endpoint_name, url_extension))
|
906
|
+
except Exception:
|
907
|
+
pass
|
866
908
|
|
867
909
|
# Get provider TIN from config (using existing billing_provider_tin)
|
868
910
|
from MediCafe.core_utils import extract_medilink_config
|
@@ -870,6 +912,19 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
870
912
|
provider_tin = medi.get('billing_provider_tin')
|
871
913
|
if not provider_tin:
|
872
914
|
raise ValueError("Provider TIN not found in configuration")
|
915
|
+
# Normalize provider TIN to 9-digit numeric string
|
916
|
+
try:
|
917
|
+
provider_tin_str = ''.join([ch for ch in str(provider_tin) if ch.isdigit()])
|
918
|
+
if len(provider_tin_str) == 9:
|
919
|
+
provider_tin = provider_tin_str
|
920
|
+
else:
|
921
|
+
MediLink_ConfigLoader.log(
|
922
|
+
"Warning: Provider TIN '{}' is not 9 digits after normalization".format(provider_tin),
|
923
|
+
level="WARNING",
|
924
|
+
console_output=CONSOLE_LOGGING
|
925
|
+
)
|
926
|
+
except Exception:
|
927
|
+
pass
|
873
928
|
|
874
929
|
# Construct GraphQL query variables using the consolidated module
|
875
930
|
graphql_variables = MediLink_GraphQL.build_eligibility_variables(
|
@@ -896,8 +951,17 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
896
951
|
MediLink_ConfigLoader.log("Using SAMPLE DATA from swagger documentation", level="INFO")
|
897
952
|
else:
|
898
953
|
# Build GraphQL request with actual data using consolidated module
|
899
|
-
|
900
|
-
|
954
|
+
# OPTUMAI now uses an enriched query aligned to production schema
|
955
|
+
try:
|
956
|
+
if endpoint_name == 'OPTUMAI' and hasattr(MediLink_GraphQL, 'build_optumai_enriched_request'):
|
957
|
+
graphql_body = MediLink_GraphQL.build_optumai_enriched_request(graphql_variables)
|
958
|
+
MediLink_ConfigLoader.log("Using OPTUMAI ENRICHED GraphQL request", level="INFO")
|
959
|
+
else:
|
960
|
+
graphql_body = MediLink_GraphQL.build_eligibility_request(graphql_variables)
|
961
|
+
MediLink_ConfigLoader.log("Using CONSTRUCTED DATA with consolidated GraphQL module", level="INFO")
|
962
|
+
except Exception:
|
963
|
+
graphql_body = MediLink_GraphQL.build_eligibility_request(graphql_variables)
|
964
|
+
MediLink_ConfigLoader.log("Fallback to standard GraphQL request body", level="WARNING")
|
901
965
|
|
902
966
|
# Compare with sample data for debugging
|
903
967
|
sample_data = MediLink_GraphQL.get_sample_eligibility_request()
|
@@ -924,8 +988,27 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
924
988
|
headers = {
|
925
989
|
'Content-Type': 'application/json',
|
926
990
|
'Accept': 'application/json',
|
927
|
-
'tin': str(provider_tin) # Ensure TIN is a string
|
991
|
+
'tin': str(provider_tin) # Ensure TIN is a string (used for legacy UHC super connector)
|
928
992
|
}
|
993
|
+
|
994
|
+
# OPTUMAI requires 'providertaxid' header (mapped from billing_provider_tin)
|
995
|
+
try:
|
996
|
+
if endpoint_name == 'OPTUMAI' and provider_tin:
|
997
|
+
# OPTUMAI expects providerTaxId; remove legacy 'tin' header to avoid confusion
|
998
|
+
if 'tin' in headers:
|
999
|
+
try:
|
1000
|
+
del headers['tin']
|
1001
|
+
except Exception:
|
1002
|
+
pass
|
1003
|
+
headers['providerTaxId'] = str(provider_tin)
|
1004
|
+
# Add trace header for observability (optional per spec)
|
1005
|
+
try:
|
1006
|
+
corr_id = 'mc-{}'.format(int(time.time() * 1000))
|
1007
|
+
except Exception:
|
1008
|
+
corr_id = 'mc-{}'.format(int(time.time()))
|
1009
|
+
headers['x-optum-consumer-correlation-id'] = corr_id
|
1010
|
+
except Exception:
|
1011
|
+
pass
|
929
1012
|
|
930
1013
|
# Only add env header when using sample data
|
931
1014
|
if USE_SAMPLE_DATA:
|
@@ -937,12 +1020,86 @@ def get_eligibility_super_connector(client, payer_id, provider_last_name, search
|
|
937
1020
|
# Log the final headers being sent
|
938
1021
|
MediLink_ConfigLoader.log("Final headers being sent: {}".format(json.dumps(headers, indent=2)), level="DEBUG")
|
939
1022
|
|
940
|
-
# Make the GraphQL API call
|
941
|
-
|
1023
|
+
# Make the GraphQL API call with enhanced error diagnostics for endpoint failures
|
1024
|
+
try:
|
1025
|
+
response = client.make_api_call(endpoint_name, 'POST', url_extension, params=None, data=graphql_body, headers=headers)
|
1026
|
+
except Exception as e:
|
1027
|
+
# Best-effort diagnostics without exposing secrets or PHI
|
1028
|
+
try:
|
1029
|
+
status = getattr(getattr(e, 'response', None), 'status_code', None)
|
1030
|
+
diag = "Eligibility request to {}{} failed".format(
|
1031
|
+
endpoint_name and (endpoint_name + " ") or "", url_extension)
|
1032
|
+
if status is not None:
|
1033
|
+
diag += " with status {}".format(status)
|
1034
|
+
MediLink_ConfigLoader.log(diag, level="ERROR", console_output=CONSOLE_LOGGING)
|
1035
|
+
try:
|
1036
|
+
print("[Eligibility] Request failed (status: {}). See logs for details.".format(status))
|
1037
|
+
except Exception:
|
1038
|
+
pass
|
1039
|
+
except Exception:
|
1040
|
+
pass
|
1041
|
+
|
1042
|
+
# Progressive hardening: allow disabling fallback to UHC via config
|
1043
|
+
try:
|
1044
|
+
disable_fallback = False
|
1045
|
+
try:
|
1046
|
+
disable_fallback = bool(medi.get('optumai_disable_fallback', False))
|
1047
|
+
except Exception:
|
1048
|
+
disable_fallback = False
|
1049
|
+
if not disable_fallback and endpoint_name == 'OPTUMAI':
|
1050
|
+
fallback_url = endpoints_cfg.get('UHCAPI', {}).get('additional_endpoints', {}).get('eligibility_super_connector')
|
1051
|
+
if fallback_url:
|
1052
|
+
try:
|
1053
|
+
MediLink_ConfigLoader.log(
|
1054
|
+
"OPTUMAI call failed. Falling back to UHCAPI path '{}'".format(fallback_url),
|
1055
|
+
level="WARNING",
|
1056
|
+
console_output=CONSOLE_LOGGING
|
1057
|
+
)
|
1058
|
+
except Exception:
|
1059
|
+
pass
|
1060
|
+
try:
|
1061
|
+
print("[Eligibility] Fallback to UHCAPI (path: {})".format(fallback_url))
|
1062
|
+
except Exception:
|
1063
|
+
pass
|
1064
|
+
response = client.make_api_call('UHCAPI', 'POST', fallback_url, params=None, data=graphql_body, headers=headers)
|
1065
|
+
else:
|
1066
|
+
raise
|
1067
|
+
else:
|
1068
|
+
raise
|
1069
|
+
except Exception:
|
1070
|
+
# Propagate original error if fallback not possible or also fails
|
1071
|
+
raise
|
942
1072
|
|
943
1073
|
# Transform GraphQL response to match REST API format
|
944
1074
|
# This ensures the calling code doesn't know the difference
|
945
1075
|
transformed_response = MediLink_GraphQL.transform_eligibility_response(response)
|
1076
|
+
|
1077
|
+
# Post-transform sanity: if non-200, emit brief diagnostics to aid validation sessions
|
1078
|
+
try:
|
1079
|
+
sc_status = transformed_response.get('statuscode') if isinstance(transformed_response, dict) else None
|
1080
|
+
if sc_status and sc_status != '200':
|
1081
|
+
msg = transformed_response.get('message')
|
1082
|
+
MediLink_ConfigLoader.log(
|
1083
|
+
"Super Connector transformed response status: {} msg: {}".format(sc_status, msg),
|
1084
|
+
level="INFO",
|
1085
|
+
console_output=CONSOLE_LOGGING
|
1086
|
+
)
|
1087
|
+
raw_errs = None
|
1088
|
+
try:
|
1089
|
+
raw = transformed_response.get('rawGraphQLResponse', {})
|
1090
|
+
raw_errs = raw.get('errors')
|
1091
|
+
except Exception:
|
1092
|
+
raw_errs = None
|
1093
|
+
if raw_errs:
|
1094
|
+
try:
|
1095
|
+
first_err = raw_errs[0]
|
1096
|
+
code = first_err.get('code') or first_err.get('extensions', {}).get('code')
|
1097
|
+
desc = first_err.get('description') or first_err.get('message')
|
1098
|
+
print("[Eligibility] GraphQL error code={} desc={}".format(code, desc))
|
1099
|
+
except Exception:
|
1100
|
+
pass
|
1101
|
+
except Exception:
|
1102
|
+
pass
|
946
1103
|
|
947
1104
|
return transformed_response
|
948
1105
|
|
MediCafe/graphql_utils.py
CHANGED
@@ -7,7 +7,17 @@ Handles query templates, query building, and response transformations
|
|
7
7
|
import json
|
8
8
|
|
9
9
|
class GraphQLQueryBuilder:
|
10
|
-
"""Builder class for constructing GraphQL queries for Super Connector API
|
10
|
+
"""Builder class for constructing GraphQL queries for Super Connector API
|
11
|
+
|
12
|
+
Note on ID card images (idCardImages):
|
13
|
+
- Intentionally excluded from all active queries to avoid fetching large
|
14
|
+
image payloads and to reduce PHI handling risk in standard flows.
|
15
|
+
- If a future feature requires downloading insurance ID cards, add this
|
16
|
+
selection to the desired query:
|
17
|
+
idCardImages { side content contentType }
|
18
|
+
and ensure the implementation does NOT log or persist raw image content.
|
19
|
+
Gate any new behavior behind an explicit, opt-in feature flag.
|
20
|
+
"""
|
11
21
|
|
12
22
|
@staticmethod
|
13
23
|
def get_eligibility_query():
|
@@ -161,6 +171,53 @@ class GraphQLQueryBuilder:
|
|
161
171
|
}
|
162
172
|
}
|
163
173
|
|
174
|
+
# ------------------------------------------------------------------
|
175
|
+
# OPTUMAI compatibility: minimal query that avoids fields not present
|
176
|
+
# ------------------------------------------------------------------
|
177
|
+
@staticmethod
|
178
|
+
def get_optumai_minimal_query():
|
179
|
+
"""
|
180
|
+
Returns a minimal GraphQL query for OPTUMAI endpoint that avoids
|
181
|
+
fields reported as undefined by the schema.
|
182
|
+
"""
|
183
|
+
return """query Query($input: EligibilityInput!) {\r\n checkEligibility(input: $input) {\r\n eligibility {\r\n eligibilityInfo {\r\n trnId\r\n member {\r\n memberId\r\n firstName\r\n lastName\r\n middleName\r\n dateOfBirth\r\n gender\r\n relationshipCode\r\n dependentSequenceNumber\r\n individualRelationship { code description }\r\n relationshipType { code description }\r\n }\r\n insuranceInfo {\r\n policyStatus\r\n planTypeDescription\r\n payerId\r\n }\r\n planLevels { level }\r\n }\r\n }\r\n }\r\n}"""
|
184
|
+
|
185
|
+
@staticmethod
|
186
|
+
def build_optumai_minimal_request(variables):
|
187
|
+
"""
|
188
|
+
Builds the minimal GraphQL request body for OPTUMAI.
|
189
|
+
"""
|
190
|
+
return {
|
191
|
+
"query": GraphQLQueryBuilder.get_optumai_minimal_query(),
|
192
|
+
"variables": {
|
193
|
+
"input": variables
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
# ------------------------------------------------------------------
|
198
|
+
# OPTUMAI compatibility: enriched query with commonly used fields
|
199
|
+
# ------------------------------------------------------------------
|
200
|
+
@staticmethod
|
201
|
+
def get_optumai_enriched_query():
|
202
|
+
"""
|
203
|
+
Returns an enriched GraphQL query for OPTUMAI that includes
|
204
|
+
eligibilityInfo core fields plus planLevels details that our
|
205
|
+
downstream logic commonly uses. Avoids legacy-only fields.
|
206
|
+
"""
|
207
|
+
return """query Query($input: EligibilityInput!) {\r\n checkEligibility(input: $input) {\r\n eligibility {\r\n eligibilityInfo {\r\n trnId\r\n member {\r\n memberId\r\n firstName\r\n lastName\r\n middleName\r\n dateOfBirth\r\n gender\r\n relationshipCode\r\n dependentSequenceNumber\r\n individualRelationship { code description }\r\n relationshipType { code description }\r\n }\r\n insuranceInfo {\r\n policyNumber\r\n eligibilityStartDate\r\n eligibilityEndDate\r\n planStartDate\r\n planEndDate\r\n policyStatus\r\n planTypeDescription\r\n payerId\r\n lineOfBusinessCode\r\n coverageType\r\n insuranceTypeCode\r\n insuranceType\r\n stateOfIssueCode\r\n productType\r\n productId\r\n productCode\r\n }\r\n associatedIds {\r\n alternateId\r\n medicaidRecipientId\r\n exchangeMemberId\r\n alternateSubscriberId\r\n hicNumber\r\n mbiNumber\r\n subscriberMemberFacingIdentifier\r\n survivingSpouseId\r\n subscriberId\r\n memberReplacementId\r\n legacyMemberId\r\n healthInsuranceExchangeId\r\n }\r\n planLevels {\r\n level\r\n family {\r\n networkStatus\r\n planAmount\r\n planAmountFrequency\r\n remainingAmount\r\n }\r\n individual {\r\n networkStatus\r\n planAmount\r\n planAmountFrequency\r\n remainingAmount\r\n }\r\n }\r\n }\r\n providerNetwork { status tier }\r\n }\r\n }\r\n}"""
|
208
|
+
|
209
|
+
@staticmethod
|
210
|
+
def build_optumai_enriched_request(variables):
|
211
|
+
"""
|
212
|
+
Builds the enriched GraphQL request body for OPTUMAI.
|
213
|
+
"""
|
214
|
+
return {
|
215
|
+
"query": GraphQLQueryBuilder.get_optumai_enriched_query(),
|
216
|
+
"variables": {
|
217
|
+
"input": variables
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
164
221
|
class GraphQLResponseTransformer:
|
165
222
|
"""Transforms GraphQL responses to match REST API format"""
|
166
223
|
|
@@ -437,6 +494,14 @@ def build_eligibility_request(variables):
|
|
437
494
|
"""Build complete GraphQL request body with working format"""
|
438
495
|
return GraphQLQueryBuilder.build_eligibility_request(variables)
|
439
496
|
|
497
|
+
def build_optumai_minimal_request(variables):
|
498
|
+
"""Build minimal GraphQL request body for OPTUMAI"""
|
499
|
+
return GraphQLQueryBuilder.build_optumai_minimal_request(variables)
|
500
|
+
|
501
|
+
def build_optumai_enriched_request(variables):
|
502
|
+
"""Build enriched GraphQL request body for OPTUMAI"""
|
503
|
+
return GraphQLQueryBuilder.build_optumai_enriched_request(variables)
|
504
|
+
|
440
505
|
def transform_eligibility_response(graphql_response):
|
441
506
|
"""Transform GraphQL eligibility response to REST format"""
|
442
507
|
return GraphQLResponseTransformer.transform_eligibility_response(graphql_response)
|