feat: wire Tufts SCO to DentaQuest portal and fix insurance credential dropdown
- Add /dentaquest-eligibility endpoint in Python agent (Tufts SCO uses providers.dentaquest.com) - Add backend route, processor, and service client for Tufts SCO (separate from UnitedSCO/DentalHub) - Fix Tufts SCO button to post to new tuftssco route instead of unitedsco - Fix credential field names: dentaquestUsername/Password (was tuftsscoUsername/Password) - Fix socket event: listen for selenium:dentaquest_session_started (was unitedsco) - Fix error visibility: keep session alive 30s on error so backend reads real message - Replace free-text Site Key field with dropdown to prevent key mismatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,14 @@ import time
|
||||
import helpers_ddma_eligibility as hddma
|
||||
import helpers_deltains_eligibility as hdeltains
|
||||
import helpers_unitedsco_eligibility as hunitedsco
|
||||
import helpers_dentaquest_eligibility as hdentaquest
|
||||
import helpers_cca_eligibility as hcca
|
||||
|
||||
# Import startup session-clear functions
|
||||
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 dentaquest_browser_manager import clear_dentaquest_session_on_startup
|
||||
from cca_browser_manager import clear_cca_session_on_startup
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@@ -31,6 +33,7 @@ print("=" * 50)
|
||||
clear_ddma_session_on_startup()
|
||||
clear_deltains_session_on_startup()
|
||||
clear_unitedsco_session_on_startup()
|
||||
clear_dentaquest_session_on_startup()
|
||||
clear_cca_session_on_startup()
|
||||
print("=" * 50)
|
||||
print("SESSION CLEAR COMPLETE")
|
||||
@@ -341,6 +344,47 @@ async def unitedsco_eligibility(request: Request):
|
||||
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
|
||||
async with semaphore:
|
||||
async with lock:
|
||||
waiting_jobs -= 1
|
||||
active_jobs += 1
|
||||
try:
|
||||
await hdentaquest.start_dentaquest_run(sid, data, url)
|
||||
finally:
|
||||
async with lock:
|
||||
active_jobs -= 1
|
||||
|
||||
|
||||
@app.post("/dentaquest-eligibility")
|
||||
async def dentaquest_eligibility(request: Request):
|
||||
"""
|
||||
Starts a DentaQuest (Tufts SCO) 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 = hdentaquest.make_session_entry()
|
||||
hdentaquest.sessions[sid]["type"] = "dentaquest_eligibility"
|
||||
hdentaquest.sessions[sid]["last_activity"] = time.time()
|
||||
|
||||
async with lock:
|
||||
waiting_jobs += 1
|
||||
|
||||
asyncio.create_task(_dentaquest_worker_wrapper(
|
||||
sid, data,
|
||||
url="https://providers.dentaquest.com/"
|
||||
))
|
||||
|
||||
return {"status": "started", "session_id": sid}
|
||||
|
||||
|
||||
async def _cca_worker_wrapper(sid: str, data: dict, url: str):
|
||||
"""Background worker for CCA — acquires semaphore, updates counters. No OTP."""
|
||||
global active_jobs, waiting_jobs
|
||||
@@ -401,6 +445,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 hdentaquest.sessions:
|
||||
res = hdentaquest.submit_otp(sid, otp)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="session not found")
|
||||
|
||||
@@ -418,6 +464,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 hdentaquest.sessions:
|
||||
s = hdentaquest.get_session_status(sid)
|
||||
elif sid in hcca.sessions:
|
||||
s = hcca.get_session_status(sid)
|
||||
else:
|
||||
|
||||
@@ -36,41 +36,34 @@ def make_session_entry() -> str:
|
||||
|
||||
async def cleanup_session(sid: str, message: str | None = None):
|
||||
"""
|
||||
Close driver (if any), wake OTP waiter, set final state, and remove session entry.
|
||||
Idempotent: safe to call multiple times.
|
||||
Set final error state and wake OTP waiter. Schedules session removal after a delay
|
||||
so the backend can read the actual error message before the session disappears.
|
||||
"""
|
||||
s = sessions.get(sid)
|
||||
if not s:
|
||||
return
|
||||
try:
|
||||
# Ensure final state
|
||||
try:
|
||||
if s.get("status") not in ("completed", "error", "not_found"):
|
||||
s["status"] = "error"
|
||||
if message:
|
||||
s["message"] = message
|
||||
except Exception:
|
||||
pass
|
||||
if s.get("status") not in ("completed", "error", "not_found"):
|
||||
s["status"] = "error"
|
||||
if message:
|
||||
s["message"] = message
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Wake any OTP waiter (so awaiting coroutines don't hang)
|
||||
try:
|
||||
ev = s.get("otp_event")
|
||||
if ev and not ev.is_set():
|
||||
ev.set()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
ev = s.get("otp_event")
|
||||
if ev and not ev.is_set():
|
||||
ev.set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# NOTE: Do NOT quit driver - keep browser alive for next patient
|
||||
# Browser manager handles the persistent browser instance
|
||||
|
||||
finally:
|
||||
# Remove session entry from map
|
||||
sessions.pop(sid, None)
|
||||
# Keep session for 30s so backend can read the error, then remove
|
||||
asyncio.create_task(_remove_session_later(sid, 30))
|
||||
|
||||
|
||||
async def _remove_session_later(sid: str, delay: int = 20):
|
||||
async def _remove_session_later(sid: str, delay: int = 30):
|
||||
await asyncio.sleep(delay)
|
||||
await cleanup_session(sid)
|
||||
sessions.pop(sid, None)
|
||||
|
||||
|
||||
async def start_dentaquest_run(sid: str, data: dict, url: str):
|
||||
|
||||
@@ -335,32 +335,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Warning: Could not fill DOB: {e}")
|
||||
|
||||
# 3. Fill First Name if provided
|
||||
if self.firstName:
|
||||
try:
|
||||
first_name_input = wait.until(EC.presence_of_element_located(
|
||||
(By.XPATH, '//input[@placeholder="First name - 1 char minimum" or contains(@placeholder,"first name") or contains(@name,"firstName")]')
|
||||
))
|
||||
first_name_input.clear()
|
||||
first_name_input.send_keys(self.firstName)
|
||||
print(f"[DDMA step1] Entered First Name: {self.firstName}")
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Warning: Could not fill First Name: {e}")
|
||||
|
||||
# 4. Fill Last Name if provided
|
||||
if self.lastName:
|
||||
try:
|
||||
last_name_input = wait.until(EC.presence_of_element_located(
|
||||
(By.XPATH, '//input[@placeholder="Last name - 2 char minimum" or contains(@placeholder,"last name") or contains(@name,"lastName")]')
|
||||
))
|
||||
last_name_input.clear()
|
||||
last_name_input.send_keys(self.lastName)
|
||||
print(f"[DDMA step1] Entered Last Name: {self.lastName}")
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
print(f"[DDMA step1] Warning: Could not fill Last Name: {e}")
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
# Click Search button
|
||||
|
||||
Reference in New Issue
Block a user