feat: add United Ucard eligibility check with OTP + persistent session

Adds a full login/OTP/search/capture flow for United Ucard (OneHealthcareID
-> UHC Dental provider portal), mirroring the United SCO/DDMA pattern:
persistent Chrome profile, OTP polling, member eligibility search, and PDF
capture saved to the patient's Eligibility folder.

Also fixes a validation bug in the browser-safe patient schema where
email/address/city/zipCode/gender/phone were optional but not nullable,
rejecting patient records with null values on those fields during edits
(e.g. "Expected string, received null").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 22:44:43 -04:00
co-authored by Claude Sonnet 5
parent e8889c0ff8
commit f30016e674
14 changed files with 2226 additions and 17 deletions
+58
View File
@@ -17,6 +17,7 @@ import time
import helpers_ddma_eligibility as hddma
import helpers_deltains_eligibility as hdeltains
import helpers_unitedsco_eligibility as hunitedsco
import helpers_uniteducard_eligibility as huniteducard
import helpers_dentaquest_eligibility as hdentaquest
import helpers_cca_eligibility as hcca
import helpers_cca_claim as hcca_claim
@@ -32,6 +33,7 @@ import helpers_bcbs_ma_eligibility as hbcbs_ma
from ddma_browser_manager import clear_ddma_session_on_startup
from deltains_browser_manager import clear_deltains_session_on_startup
from unitedsco_browser_manager import clear_unitedsco_session_on_startup
from uniteducard_browser_manager import clear_uniteducard_session_on_startup
from dentaquest_browser_manager import clear_dentaquest_session_on_startup
from cca_browser_manager import clear_cca_session_on_startup
@@ -45,6 +47,7 @@ print("=" * 50)
clear_ddma_session_on_startup()
clear_deltains_session_on_startup()
clear_unitedsco_session_on_startup()
clear_uniteducard_session_on_startup()
clear_dentaquest_session_on_startup()
clear_cca_session_on_startup()
print("=" * 50)
@@ -497,6 +500,47 @@ async def unitedsco_eligibility(request: Request):
return {"status": "started", "session_id": sid}
async def _uniteducard_worker_wrapper(sid: str, data: dict, url: str):
"""Background worker for United Ucard — acquires semaphore, updates counters."""
global active_jobs, waiting_jobs
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
await huniteducard.start_uniteducard_run(sid, data, url)
finally:
async with lock:
active_jobs -= 1
@app.post("/uniteducard-eligibility")
async def uniteducard_eligibility(request: Request):
"""
Starts a United Ucard (OneHealthcareID) eligibility session in the background.
Body: { "data": { ... } }
Returns: { status: "started", session_id: "<uuid>" }
"""
global waiting_jobs
body = await request.json()
data = body.get("data", {})
sid = huniteducard.make_session_entry()
huniteducard.sessions[sid]["type"] = "uniteducard_eligibility"
huniteducard.sessions[sid]["last_activity"] = time.time()
async with lock:
waiting_jobs += 1
asyncio.create_task(_uniteducard_worker_wrapper(
sid, data,
url="https://www.uhcdental.com/content/provider/dental.html?state=abc"
))
return {"status": "started", "session_id": sid}
async def _dentaquest_worker_wrapper(sid: str, data: dict, url: str):
"""Background worker for DentaQuest (Tufts SCO) — acquires semaphore, updates counters."""
global active_jobs, waiting_jobs
@@ -932,6 +976,8 @@ async def submit_otp(request: Request):
res = hdeltains.submit_otp(sid, otp)
elif sid in hunitedsco.sessions:
res = hunitedsco.submit_otp(sid, otp)
elif sid in huniteducard.sessions:
res = huniteducard.submit_otp(sid, otp)
elif sid in hdentaquest.sessions:
res = hdentaquest.submit_otp(sid, otp)
elif sid in hddma_claim.sessions:
@@ -963,6 +1009,8 @@ async def session_status(sid: str):
s = hdeltains.get_session_status(sid)
elif sid in hunitedsco.sessions:
s = hunitedsco.get_session_status(sid)
elif sid in huniteducard.sessions:
s = huniteducard.get_session_status(sid)
elif sid in hdentaquest.sessions:
s = hdentaquest.get_session_status(sid)
elif sid in hcca.sessions:
@@ -1022,6 +1070,16 @@ async def clear_unitedsco_session_endpoint():
return {"status": "error", "message": str(e)}
@app.post("/clear-uniteducard-session")
async def clear_uniteducard_session_endpoint():
"""Clears the United Ucard browser session. Call when credentials are deleted or changed."""
try:
clear_uniteducard_session_on_startup()
return {"status": "success", "message": "United Ucard session cleared"}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.post("/clear-cca-session")
async def clear_cca_session_endpoint():
"""Clears the CCA browser session. Call when credentials are deleted or changed."""