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_MH_eligibilityHistoryCheckWorker import AutomationMassHealthEligibilityHistoryCheck from selenium_CMSP_eligibilityHistoryRemainingCheckWorker import AutomationCMSPEligibilityHistoryRemainingCheck from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck from selenium_preAuthWorker import AutomationMassHealthPreAuth from selenium_MHPaymentCheckWorker import AutomationMassHealthPaymentCheck 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 helpers_cca_claim as hcca_claim import helpers_cca_preauth as hcca_preauth import helpers_ddma_claim as hddma_claim import helpers_uniteddh_claim as huniteddh_claim import helpers_tuftssco_claim as htuftssco_claim # 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: 2a — MH Eligibility + Service History @app.post("/mh-eligibility-history-check") async def mh_eligibility_history_check(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 = AutomationMassHealthEligibilityHistoryCheck(data) result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login") if result.get("status") == "error": return {"status": "error", "message": result.get("message")} port = os.getenv("PORT", "5002") url_host = os.getenv("HOST", "localhost") if result.get("pdf_path"): filename = os.path.basename(result["pdf_path"]) result["pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}" if result.get("history_pdf_path"): filename = os.path.basename(result["history_pdf_path"]) result["history_pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}" return result except Exception as e: return {"status": "error", "message": str(e)} finally: async with lock: active_jobs -= 1 # Endpoint: 2b — CMSP Eligibility + Service History + Accumulator (Remaining) @app.post("/cmsp-eligibility-history-remaining-check") async def cmsp_eligibility_history_remaining_check(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 = AutomationCMSPEligibilityHistoryRemainingCheck(data) result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login") if result.get("status") == "error": return {"status": "error", "message": result.get("message")} port = os.getenv("PORT", "5002") url_host = os.getenv("HOST", "localhost") for key, url_key in [ ("pdf_path", "pdf_url"), ("history_pdf_path", "history_pdf_url"), ("accumulator_pdf_path", "accumulator_pdf_url"), ]: if result.get(key): filename = os.path.basename(result[key]) result[url_key] = f"http://{url_host}:{port}/downloads/{filename}" 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: 5 — Check MassHealth payment for a given claim number @app.post("/mh-payment-check") async def mh_payment_check(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 = AutomationMassHealthPaymentCheck(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")} # Convert pdf_path to pdf_url so the frontend can fetch it 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: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: "" } """ 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: "" } """ 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: "" } """ 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: "" } """ 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: "" } """ 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} async def _cca_claim_worker_wrapper(sid: str, data: dict, url: str): """Background worker for CCA claim submission.""" global active_jobs, waiting_jobs async with semaphore: async with lock: waiting_jobs -= 1 active_jobs += 1 try: await hcca_claim.start_cca_claim_run(sid, data, url) finally: async with lock: active_jobs -= 1 @app.post("/cca-claim") async def cca_claim(request: Request): """ Starts a CCA claim submission session in the background. Logs in, navigates Claims > Submit Claims, opens claim entry page. Body: { "claim": { "cca_username": "...", "cca_password": "...", ... } } Returns: { status: "started", session_id: "" } """ global waiting_jobs body = await request.json() sid = hcca_claim.make_session_entry() hcca_claim.sessions[sid]["type"] = "cca_claim" hcca_claim.sessions[sid]["last_activity"] = time.time() async with lock: waiting_jobs += 1 asyncio.create_task(_cca_claim_worker_wrapper( sid, body, url="https://pwp.sciondental.com/PWP/Landing" )) return {"status": "started", "session_id": sid} async def _ddma_claim_worker_wrapper(sid: str, data: dict, url: str): """Background worker for DDMA claim submission.""" global active_jobs, waiting_jobs async with semaphore: async with lock: waiting_jobs -= 1 active_jobs += 1 try: await hddma_claim.start_ddma_claim_run(sid, data, url) finally: async with lock: active_jobs -= 1 @app.post("/ddma-claim") async def ddma_claim(request: Request): """ Starts a DDMA claim submission session in the background. Logs in, searches patient, opens Member Information page, clicks Create claim, fills service date and procedure code. Body: { "claim": { "massddmaUsername": "...", "massddmaPassword": "...", ... } } Returns: { status: "started", session_id: "" } """ global waiting_jobs body = await request.json() sid = hddma_claim.make_session_entry() hddma_claim.sessions[sid]["type"] = "ddma_claim" hddma_claim.sessions[sid]["last_activity"] = time.time() async with lock: waiting_jobs += 1 asyncio.create_task(_ddma_claim_worker_wrapper( sid, body, url="https://providers.deltadentalma.com/onboarding/start/" )) return {"status": "started", "session_id": sid} async def _uniteddh_claim_worker_wrapper(sid: str, data: dict, url: str): """Background worker for United/DentalHub claim submission.""" global active_jobs, waiting_jobs async with semaphore: async with lock: waiting_jobs -= 1 active_jobs += 1 try: await huniteddh_claim.start_uniteddh_claim_run(sid, data, url) finally: async with lock: active_jobs -= 1 @app.post("/uniteddh-claim") async def uniteddh_claim(request: Request): """ Starts a United/DentalHub claim submission session in the background. Logs in, searches patient, opens Member Information page, clicks Create claim, fills service date and procedure codes. Body: { "claim": { "uniteddhUsername": "...", "uniteddhPassword": "...", ... } } Returns: { status: "started", session_id: "" } """ global waiting_jobs body = await request.json() sid = huniteddh_claim.make_session_entry() huniteddh_claim.sessions[sid]["type"] = "uniteddh_claim" huniteddh_claim.sessions[sid]["last_activity"] = time.time() async with lock: waiting_jobs += 1 asyncio.create_task(_uniteddh_claim_worker_wrapper( sid, body, url="https://app.dentalhub.com/app/login" )) return {"status": "started", "session_id": sid} async def _tuftssco_claim_worker_wrapper(sid: str, data: dict, url: str): """Background worker for Tufts SCO (DentaQuest) claim submission.""" global active_jobs, waiting_jobs async with semaphore: async with lock: waiting_jobs -= 1 active_jobs += 1 try: await htuftssco_claim.start_tuftssco_claim_run(sid, data, url) finally: async with lock: active_jobs -= 1 @app.post("/tuftssco-claim") async def tuftssco_claim(request: Request): """ Starts a Tufts SCO (DentaQuest) claim submission session in the background. Logs in, searches patient, opens Member Information page, clicks Create claim, fills service date and procedure codes. Body: { "claim": { "dentaquestUsername": "...", "dentaquestPassword": "...", ... } } Returns: { status: "started", session_id: "" } """ global waiting_jobs body = await request.json() sid = htuftssco_claim.make_session_entry() htuftssco_claim.sessions[sid]["type"] = "tuftssco_claim" htuftssco_claim.sessions[sid]["last_activity"] = time.time() async with lock: waiting_jobs += 1 asyncio.create_task(_tuftssco_claim_worker_wrapper( sid, body, url="https://providers.dentaquest.com/" )) return {"status": "started", "session_id": sid} async def _cca_preauth_worker_wrapper(sid: str, data: dict, url: str): """Background worker for CCA pre-authorization submission.""" global active_jobs, waiting_jobs async with semaphore: async with lock: waiting_jobs -= 1 active_jobs += 1 try: await hcca_preauth.start_cca_preauth_run(sid, data, url) finally: async with lock: active_jobs -= 1 @app.post("/cca-preauth") async def cca_preauth(request: Request): """ Starts a CCA pre-authorization session in the background. Logs in, navigates to Authorization Entry, fills the form and submits. Body: { "claim": { "cca_username": "...", "cca_password": "...", ... } } Returns: { status: "started", session_id: "" } """ global waiting_jobs body = await request.json() sid = hcca_preauth.make_session_entry() hcca_preauth.sessions[sid]["type"] = "cca_preauth" hcca_preauth.sessions[sid]["last_activity"] = time.time() async with lock: waiting_jobs += 1 asyncio.create_task(_cca_preauth_worker_wrapper( sid, body, 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": "", "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) elif sid in hddma_claim.sessions: res = hddma_claim.submit_otp(sid, otp) elif sid in huniteddh_claim.sessions: res = huniteddh_claim.submit_otp(sid, otp) elif sid in htuftssco_claim.sessions: res = htuftssco_claim.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) elif sid in hcca_claim.sessions: s = hcca_claim.get_session_status(sid) elif sid in hcca_preauth.sessions: s = hcca_preauth.get_session_status(sid) elif sid in hddma_claim.sessions: s = hddma_claim.get_session_status(sid) elif sid in huniteddh_claim.sessions: s = huniteddh_claim.get_session_status(sid) elif sid in htuftssco_claim.sessions: s = htuftssco_claim.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)