Files
DentalManagementMH06/apps/SeleniumService/agent.py
Gitead f5ec4a1480 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>
2026-04-17 00:59:24 -04:00

544 lines
17 KiB
Python
Executable File

from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import uvicorn
import asyncio
from selenium_claimSubmitWorker import AutomationMassHealthClaimsLogin
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
from selenium_preAuthWorker import AutomationMassHealthPreAuth
import os
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
load_dotenv()
# Clear sessions on startup so fresh login is required after PC restart.
print("=" * 50)
print("SELENIUM AGENT STARTING - CLEARING SESSIONS")
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")
print("=" * 50)
app = FastAPI()
# Allow 1 selenium session at a time
semaphore = asyncio.Semaphore(1)
# Manual counters to track active & queued jobs
active_jobs = 0
waiting_jobs = 0
lock = asyncio.Lock() # To safely update counters
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Replace with your frontend domain for security
allow_methods=["*"],
allow_headers=["*"],
)
# Mount static files for serving PDFs (must be after middleware)
DOWNLOAD_DIR = os.path.join(os.path.dirname(__file__), "downloads")
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
app.mount("/downloads", StaticFiles(directory=DOWNLOAD_DIR), name="downloads")
# Endpoint: 1 — Start the automation of submitting Claim.
@app.post("/claimsubmit")
async def start_workflow(request: Request):
global active_jobs, waiting_jobs
data = await request.json()
async with lock:
waiting_jobs += 1
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
bot = AutomationMassHealthClaimsLogin(data)
# result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
result = bot.run()
if result.get("status") != "success":
return {"status": "error", "message": result.get("message")}
# Convert pdf_path to pdf_url
if result.get("pdf_path"):
filename = os.path.basename(result["pdf_path"])
port = os.getenv("PORT", "5002")
url_host = os.getenv("HOST", "localhost")
result["pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}"
print(f"DEBUG: Generated pdf_url = {result['pdf_url']}")
return result
except Exception as e:
return {"status": "error", "message": str(e)}
finally:
async with lock:
active_jobs -= 1
# Endpoint: 2 — Start the automation of cheking eligibility
@app.post("/eligibility-check")
async def start_workflow(request: Request):
global active_jobs, waiting_jobs
data = await request.json()
async with lock:
waiting_jobs += 1
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
bot = AutomationMassHealthEligibilityCheck(data)
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
if result.get("status") != "success":
return {"status": "error", "message": result.get("message")}
# Convert pdf_path to pdf_url
if result.get("pdf_path"):
filename = os.path.basename(result["pdf_path"])
port = os.getenv("PORT", "5002")
url_host = os.getenv("HOST", "localhost")
result["pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}"
print(f"DEBUG: Generated pdf_url = {result['pdf_url']}")
return result
except Exception as e:
return {"status": "error", "message": str(e)}
finally:
async with lock:
active_jobs -= 1
# Endpoint: 2.1 — Start the automation for Claims login (open browser and log in)
@app.post("/claims-login")
async def start_claims_login(request: Request):
global active_jobs, waiting_jobs
data = await request.json()
async with lock:
waiting_jobs += 1
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
bot = AutomationMassHealthClaimsLogin(data)
result = bot.run()
if result.get("status") != "success":
return {"status": "error", "message": result.get("message")}
return result
except Exception as e:
return {"status": "error", "message": str(e)}
finally:
async with lock:
active_jobs -= 1
# Endpoint: 3 — Start the automation of cheking claim status
@app.post("/claim-status-check")
async def start_workflow(request: Request):
global active_jobs, waiting_jobs
data = await request.json()
async with lock:
waiting_jobs += 1
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
bot = AutomationMassHealthClaimStatusCheck(data)
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
if result.get("status") != "success":
return {"status": "error", "message": result.get("message")}
return result
except Exception as e:
return {"status": "error", "message": str(e)}
finally:
async with lock:
active_jobs -= 1
# Endpoint: 4 — Start the automation of cheking claim pre auth
@app.post("/claim-pre-auth")
async def start_workflow(request: Request):
global active_jobs, waiting_jobs
data = await request.json()
async with lock:
waiting_jobs += 1
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
bot = AutomationMassHealthPreAuth(data)
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
if result.get("status") != "success":
return {"status": "error", "message": result.get("message")}
return result
except Exception as e:
return {"status": "error", "message": str(e)}
finally:
async with lock:
active_jobs -= 1
# Endpoint:5 - DDMA eligibility (background, OTP)
async def _ddma_worker_wrapper(sid: str, data: dict, url: str):
"""
Background worker that:
- acquires semaphore (to keep 1 selenium at a time),
- updates active/queued counters,
- runs the DDMA flow via helpers.start_ddma_run.
"""
global active_jobs, waiting_jobs
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
await hddma.start_ddma_run(sid, data, url)
finally:
async with lock:
active_jobs -= 1
@app.post("/ddma-eligibility")
async def ddma_eligibility(request: Request):
"""
Starts a DDMA eligibility session in the background.
Body: { "data": { ... }, "url"?: string }
Returns: { status: "started", session_id: "<uuid>" }
"""
global waiting_jobs
body = await request.json()
data = body.get("data", {})
# create session
sid = hddma.make_session_entry()
hddma.sessions[sid]["type"] = "ddma_eligibility"
hddma.sessions[sid]["last_activity"] = time.time()
async with lock:
waiting_jobs += 1
# run in background (queued under semaphore)
asyncio.create_task(_ddma_worker_wrapper(sid, data, url="https://providers.deltadentalma.com/onboarding/start/"))
return {"status": "started", "session_id": sid}
async def _deltains_worker_wrapper(sid: str, data: dict, url: str):
"""Background worker for DeltaIns — acquires semaphore, updates counters."""
global active_jobs, waiting_jobs
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
await hdeltains.start_deltains_run(sid, data, url)
finally:
async with lock:
active_jobs -= 1
@app.post("/deltains-eligibility")
async def deltains_eligibility(request: Request):
"""
Starts a DeltaIns 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 = hdeltains.make_session_entry()
hdeltains.sessions[sid]["type"] = "deltains_eligibility"
hdeltains.sessions[sid]["last_activity"] = time.time()
async with lock:
waiting_jobs += 1
asyncio.create_task(_deltains_worker_wrapper(
sid, data,
url="https://www.deltadentalins.com/ciam/login?TARGET=%2Fprovider-tools%2Fv2"
))
return {"status": "started", "session_id": sid}
async def _unitedsco_worker_wrapper(sid: str, data: dict, url: str):
"""Background worker for UnitedSCO — acquires semaphore, updates counters."""
global active_jobs, waiting_jobs
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
await hunitedsco.start_unitedsco_run(sid, data, url)
finally:
async with lock:
active_jobs -= 1
@app.post("/unitedsco-eligibility")
async def unitedsco_eligibility(request: Request):
"""
Starts a UnitedSCO 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 = hunitedsco.make_session_entry()
hunitedsco.sessions[sid]["type"] = "unitedsco_eligibility"
hunitedsco.sessions[sid]["last_activity"] = time.time()
async with lock:
waiting_jobs += 1
asyncio.create_task(_unitedsco_worker_wrapper(
sid, data,
url="https://app.dentalhub.com/app/login"
))
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
async with semaphore:
async with lock:
waiting_jobs -= 1
active_jobs += 1
try:
await hcca.start_cca_run(sid, data, url)
finally:
async with lock:
active_jobs -= 1
@app.post("/cca-eligibility")
async def cca_eligibility(request: Request):
"""
Starts a CCA eligibility session in the background (no OTP).
Body: { "data": { ... } }
Returns: { status: "started", session_id: "<uuid>" }
"""
global waiting_jobs
body = await request.json()
data = body.get("data", {})
sid = hcca.make_session_entry()
hcca.sessions[sid]["type"] = "cca_eligibility"
hcca.sessions[sid]["last_activity"] = time.time()
async with lock:
waiting_jobs += 1
asyncio.create_task(_cca_worker_wrapper(
sid, data,
url="https://pwp.sciondental.com/PWP/Landing"
))
return {"status": "started", "session_id": sid}
@app.post("/submit-otp")
async def submit_otp(request: Request):
"""
Body: { "session_id": "<sid>", "otp": "123456" }
Tries each session store in order (CCA has no OTP but included for completeness).
"""
body = await request.json()
sid = body.get("session_id")
otp = body.get("otp")
if not sid or not otp:
raise HTTPException(status_code=400, detail="session_id and otp required")
# Try each session store in order
if sid in hddma.sessions:
res = hddma.submit_otp(sid, otp)
elif sid in hdeltains.sessions:
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")
if res.get("status") == "error":
raise HTTPException(status_code=400, detail=res.get("message"))
return res
@app.get("/session/{sid}/status")
async def session_status(sid: str):
# Try each session store in order
if sid in hddma.sessions:
s = hddma.get_session_status(sid)
elif sid in hdeltains.sessions:
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:
s = {"status": "not_found"}
if s.get("status") == "not_found":
raise HTTPException(status_code=404, detail="session not found")
return s
# ── Session management endpoints ─────────────────────────────────────────────
@app.post("/clear-ddma-session")
async def clear_ddma_session_endpoint():
"""Clears the DDMA browser session. Call when credentials are deleted or changed."""
try:
clear_ddma_session_on_startup()
return {"status": "success", "message": "DDMA session cleared"}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.post("/clear-deltains-session")
async def clear_deltains_session_endpoint():
"""Clears the DeltaIns browser session. Call when credentials are deleted or changed."""
try:
clear_deltains_session_on_startup()
return {"status": "success", "message": "DeltaIns session cleared"}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.post("/clear-unitedsco-session")
async def clear_unitedsco_session_endpoint():
"""Clears the UnitedSCO browser session. Call when credentials are deleted or changed."""
try:
clear_unitedsco_session_on_startup()
return {"status": "success", "message": "UnitedSCO 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."""
try:
clear_cca_session_on_startup()
return {"status": "success", "message": "CCA session cleared"}
except Exception as e:
return {"status": "error", "message": str(e)}
# ✅ Health Check Endpoint
@app.get("/")
async def health_check():
return {
"status": "ok",
"service": "Selenium Service",
"message": "Service is running"
}
# ✅ Status Endpoint
@app.get("/status")
async def get_status():
async with lock:
return {
"active_jobs": active_jobs,
"queued_jobs": waiting_jobs,
"status": "busy" if active_jobs > 0 or waiting_jobs > 0 else "idle"
}
if __name__ == "__main__":
host = os.getenv("HOST", "0.0.0.0") # Default to 0.0.0.0 to accept connections from all interfaces
port = int(os.getenv("PORT", "5002")) # Default to 5002
print(f"Starting Selenium service on {host}:{port}")
uvicorn.run(app, host=host, port=port)