feat: add BCBS MA eligibility check with OTP flow
- New Selenium worker (fresh Chrome per run, no persistent session) login → OTP modal → eTools → ConnectCenter → Verification → New Eligibility Request → fill form (NPI, member ID, DOB) → Expand All → CDP PDF back to app - Backend route fetches BCBS_MA credentials + provider NPI from settings - Frontend OTP modal with 6-digit code entry - BCBS MA added to insurance credentials dropdown in settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ 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 helpers_bcbs_ma_eligibility as hbcbs_ma
|
||||
|
||||
# Import startup session-clear functions
|
||||
from ddma_browser_manager import clear_ddma_session_on_startup
|
||||
@@ -547,6 +548,48 @@ async def cca_eligibility(request: Request):
|
||||
return {"status": "started", "session_id": sid}
|
||||
|
||||
|
||||
async def _bcbs_ma_worker_wrapper(sid: str, data: dict, url: str):
|
||||
"""Background worker for BCBS MA eligibility — fresh browser, always OTP."""
|
||||
global active_jobs, waiting_jobs
|
||||
async with semaphore:
|
||||
async with lock:
|
||||
waiting_jobs -= 1
|
||||
active_jobs += 1
|
||||
try:
|
||||
await hbcbs_ma.start_bcbs_ma_run(sid, data, url)
|
||||
finally:
|
||||
async with lock:
|
||||
active_jobs -= 1
|
||||
|
||||
|
||||
@app.post("/bcbs-ma-eligibility")
|
||||
async def bcbs_ma_eligibility(request: Request):
|
||||
"""
|
||||
Starts a BCBS MA eligibility session in the background.
|
||||
Fresh Chrome each time — no persistent session (OTP always required).
|
||||
Body: { "data": { memberId, dateOfBirth, firstName, lastName, bcbsMaUsername, bcbsMaPassword } }
|
||||
Returns: { status: "started", session_id: "<uuid>" }
|
||||
"""
|
||||
global waiting_jobs
|
||||
|
||||
body = await request.json()
|
||||
data = body.get("data", {})
|
||||
|
||||
sid = hbcbs_ma.make_session_entry()
|
||||
hbcbs_ma.sessions[sid]["type"] = "bcbs_ma_eligibility"
|
||||
hbcbs_ma.sessions[sid]["last_activity"] = time.time()
|
||||
|
||||
async with lock:
|
||||
waiting_jobs += 1
|
||||
|
||||
asyncio.create_task(_bcbs_ma_worker_wrapper(
|
||||
sid, data,
|
||||
url="https://provider.bluecrossma.com/ProviderHome/portal/"
|
||||
))
|
||||
|
||||
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
|
||||
@@ -782,6 +825,8 @@ async def submit_otp(request: Request):
|
||||
res = huniteddh_claim.submit_otp(sid, otp)
|
||||
elif sid in htuftssco_claim.sessions:
|
||||
res = htuftssco_claim.submit_otp(sid, otp)
|
||||
elif sid in hbcbs_ma.sessions:
|
||||
res = hbcbs_ma.submit_otp(sid, otp)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="session not found")
|
||||
|
||||
@@ -813,6 +858,8 @@ async def session_status(sid: str):
|
||||
s = huniteddh_claim.get_session_status(sid)
|
||||
elif sid in htuftssco_claim.sessions:
|
||||
s = htuftssco_claim.get_session_status(sid)
|
||||
elif sid in hbcbs_ma.sessions:
|
||||
s = hbcbs_ma.get_session_status(sid)
|
||||
else:
|
||||
s = {"status": "not_found"}
|
||||
if s.get("status") == "not_found":
|
||||
|
||||
199
apps/SeleniumService/helpers_bcbs_ma_eligibility.py
Normal file
199
apps/SeleniumService/helpers_bcbs_ma_eligibility.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Dict, Any
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
|
||||
from selenium_BCBS_MA_eligibilityCheckWorker import AutomationBCBSMAEligibilityCheck
|
||||
|
||||
# In-memory session store
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
SESSION_OTP_TIMEOUT = int(os.getenv("SESSION_OTP_TIMEOUT", "120")) # seconds
|
||||
|
||||
|
||||
def make_session_entry() -> str:
|
||||
sid = str(uuid.uuid4())
|
||||
sessions[sid] = {
|
||||
"status": "created", # created → running → waiting_for_otp → completed / error
|
||||
"created_at": time.time(),
|
||||
"last_activity": time.time(),
|
||||
"bot": None,
|
||||
"driver": None,
|
||||
"otp_event": asyncio.Event(),
|
||||
"otp_value": None,
|
||||
"result": None,
|
||||
"message": None,
|
||||
"type": None,
|
||||
}
|
||||
return sid
|
||||
|
||||
|
||||
async def _remove_session_later(sid: str, delay: int = 30):
|
||||
await asyncio.sleep(delay)
|
||||
sessions.pop(sid, None)
|
||||
print(f"[helpers_bcbs_ma] cleaned session {sid}")
|
||||
|
||||
|
||||
async def start_bcbs_ma_run(sid: str, data: dict, url: str):
|
||||
"""
|
||||
Full BCBS MA eligibility workflow for one session.
|
||||
Always fresh Chrome — no persistent session because BCBS MA always
|
||||
requires OTP (the prefix changes each login).
|
||||
|
||||
OTP handling:
|
||||
a) Accept OTP submitted from the app via /submit-otp (sets otp_value)
|
||||
b) Poll the browser directly to detect user entry in the open window
|
||||
"""
|
||||
s = sessions.get(sid)
|
||||
if not s:
|
||||
return {"status": "error", "message": "session not found"}
|
||||
|
||||
s["status"] = "running"
|
||||
s["last_activity"] = time.time()
|
||||
bot = None
|
||||
|
||||
try:
|
||||
bot = AutomationBCBSMAEligibilityCheck({"data": data})
|
||||
bot.config_driver()
|
||||
s["bot"] = bot
|
||||
s["driver"] = bot.driver
|
||||
|
||||
# ── Login ──────────────────────────────────────────────────────────────
|
||||
try:
|
||||
login_result = bot.login(url)
|
||||
except WebDriverException as e:
|
||||
s["status"] = "error"
|
||||
s["message"] = f"WebDriver error during login: {e}"
|
||||
return {"status": "error", "message": s["message"]}
|
||||
except Exception as e:
|
||||
s["status"] = "error"
|
||||
s["message"] = f"Login failed: {e}"
|
||||
return {"status": "error", "message": s["message"]}
|
||||
|
||||
if isinstance(login_result, str) and login_result.startswith("ERROR"):
|
||||
s["status"] = "error"
|
||||
s["message"] = login_result
|
||||
return {"status": "error", "message": login_result}
|
||||
|
||||
# ── OTP required (always for BCBS MA) ─────────────────────────────────
|
||||
if isinstance(login_result, str) and login_result == "OTP_REQUIRED":
|
||||
s["status"] = "waiting_for_otp"
|
||||
s["message"] = "OTP required — enter the 6-digit code from the BCBS MA email"
|
||||
s["last_activity"] = time.time()
|
||||
|
||||
driver = bot.driver
|
||||
login_success = False
|
||||
|
||||
print(f"[BCBS MA] Waiting for OTP (up to {SESSION_OTP_TIMEOUT}s)...")
|
||||
for poll in range(SESSION_OTP_TIMEOUT):
|
||||
await asyncio.sleep(1)
|
||||
s["last_activity"] = time.time()
|
||||
|
||||
try:
|
||||
# a) App submitted OTP
|
||||
otp_value = s.get("otp_value")
|
||||
if otp_value:
|
||||
print(f"[BCBS MA OTP poll {poll+1}] Submitting OTP from app...")
|
||||
otp_result = bot.submit_otp_step(otp_value)
|
||||
s["otp_value"] = None
|
||||
if isinstance(otp_result, str) and otp_result == "SUCCESS":
|
||||
login_success = True
|
||||
break
|
||||
elif isinstance(otp_result, str) and otp_result.startswith("ERROR"):
|
||||
print(f"[BCBS MA OTP] submit_otp_step returned: {otp_result}")
|
||||
# Don't abort yet — let the poll loop check the browser state
|
||||
|
||||
# b) Detect success by browser URL
|
||||
current_url = driver.current_url.lower()
|
||||
print(f"[BCBS MA OTP poll {poll+1}/{SESSION_OTP_TIMEOUT}] URL: {current_url[:70]}")
|
||||
|
||||
if "authsvc" not in current_url and "/mga/sps/" not in current_url:
|
||||
# Left the OTP page — login completed (user entered OTP in browser)
|
||||
print("[BCBS MA OTP] Browser left OTP page — login successful")
|
||||
login_success = True
|
||||
break
|
||||
|
||||
except Exception as poll_err:
|
||||
print(f"[BCBS MA OTP poll {poll+1}] Error: {poll_err}")
|
||||
|
||||
if not login_success:
|
||||
s["status"] = "error"
|
||||
s["message"] = "OTP timeout — login not completed in time"
|
||||
return {"status": "error", "message": s["message"]}
|
||||
|
||||
s["status"] = "running"
|
||||
s["message"] = "Login successful after OTP"
|
||||
print("[BCBS MA] OTP accepted, proceeding to eligibility search...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# ── Already logged in (no OTP path) ──────────────────────────────────
|
||||
elif isinstance(login_result, str) and login_result == "SUCCESS":
|
||||
print("[BCBS MA] Login succeeded without OTP")
|
||||
s["status"] = "running"
|
||||
|
||||
# ── Step 1: search member ──────────────────────────────────────────────
|
||||
step1_result = bot.step1()
|
||||
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
||||
s["status"] = "error"
|
||||
s["message"] = step1_result
|
||||
return {"status": "error", "message": step1_result}
|
||||
|
||||
# ── Step 2: extract + PDF ──────────────────────────────────────────────
|
||||
step2_result = bot.step2()
|
||||
if isinstance(step2_result, dict) and step2_result.get("status") == "success":
|
||||
s["status"] = "completed"
|
||||
s["result"] = step2_result
|
||||
s["message"] = "completed"
|
||||
asyncio.create_task(_remove_session_later(sid, 30))
|
||||
return step2_result
|
||||
else:
|
||||
s["status"] = "error"
|
||||
s["message"] = step2_result.get("message", "unknown error") if isinstance(step2_result, dict) else str(step2_result)
|
||||
return {"status": "error", "message": s["message"]}
|
||||
|
||||
except Exception as e:
|
||||
s["status"] = "error"
|
||||
s["message"] = f"worker exception: {e}"
|
||||
print(f"[helpers_bcbs_ma] Unexpected error: {e}")
|
||||
return {"status": "error", "message": s["message"]}
|
||||
|
||||
finally:
|
||||
# Always close the disposable browser
|
||||
try:
|
||||
if bot:
|
||||
bot.close_driver()
|
||||
except Exception:
|
||||
pass
|
||||
s["driver"] = None
|
||||
|
||||
|
||||
def submit_otp(sid: str, otp: str) -> Dict[str, Any]:
|
||||
"""Called by /submit-otp to hand OTP to the polling loop."""
|
||||
s = sessions.get(sid)
|
||||
if not s:
|
||||
return {"status": "error", "message": "session not found"}
|
||||
if s.get("status") != "waiting_for_otp":
|
||||
return {"status": "error", "message": f"session not waiting for OTP (state={s.get('status')})"}
|
||||
s["otp_value"] = otp
|
||||
s["last_activity"] = time.time()
|
||||
try:
|
||||
s["otp_event"].set()
|
||||
except Exception:
|
||||
pass
|
||||
return {"status": "ok", "message": "otp accepted"}
|
||||
|
||||
|
||||
def get_session_status(sid: str) -> Dict[str, Any]:
|
||||
s = sessions.get(sid)
|
||||
if not s:
|
||||
return {"status": "not_found"}
|
||||
return {
|
||||
"session_id": sid,
|
||||
"status": s.get("status"),
|
||||
"message": s.get("message"),
|
||||
"created_at": s.get("created_at"),
|
||||
"last_activity": s.get("last_activity"),
|
||||
"result": s.get("result") if s.get("status") == "completed" else None,
|
||||
}
|
||||
462
apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py
Normal file
462
apps/SeleniumService/selenium_BCBS_MA_eligibilityCheckWorker.py
Normal file
@@ -0,0 +1,462 @@
|
||||
import os
|
||||
import time
|
||||
import base64
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException, WebDriverException
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
|
||||
|
||||
DOWNLOAD_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "downloads", "bcbs_ma"))
|
||||
|
||||
|
||||
def _fresh_driver() -> webdriver.Chrome:
|
||||
"""Create a disposable Chrome instance for a single BCBS MA session."""
|
||||
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
||||
options = Options()
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
options.add_experimental_option("prefs", {
|
||||
"download.default_directory": DOWNLOAD_DIR,
|
||||
"download.prompt_for_download": False,
|
||||
"plugins.always_open_pdf_externally": True,
|
||||
})
|
||||
headless = os.getenv("SELENIUM_HEADLESS", "false").lower() == "true"
|
||||
if headless:
|
||||
options.add_argument("--headless=new")
|
||||
|
||||
try:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
service = Service(ChromeDriverManager().install())
|
||||
except Exception:
|
||||
service = Service()
|
||||
|
||||
driver = webdriver.Chrome(service=service, options=options)
|
||||
driver.maximize_window()
|
||||
return driver
|
||||
|
||||
|
||||
class AutomationBCBSMAEligibilityCheck:
|
||||
"""
|
||||
BCBS MA Provider Central eligibility check.
|
||||
|
||||
No persistent session — fresh Chrome every run because BCBS MA
|
||||
always requires OTP on new login (the OTP prefix changes each time).
|
||||
|
||||
Flow: login(url) → OTP_REQUIRED → [caller polls for OTP] → submit_otp_step(otp)
|
||||
→ step1() [search member] → step2() [extract + PDF]
|
||||
"""
|
||||
|
||||
LOGIN_URL = "https://provider.bluecrossma.com/ProviderHome/portal/"
|
||||
|
||||
def __init__(self, data: dict):
|
||||
raw = data.get("data", data)
|
||||
self.member_id = raw.get("memberId", "")
|
||||
self.dob = raw.get("dateOfBirth", "") # YYYY-MM-DD from frontend
|
||||
self.first_name = raw.get("firstName", "")
|
||||
self.last_name = raw.get("lastName", "")
|
||||
self.username = raw.get("bcbsMaUsername", "")
|
||||
self.password = raw.get("bcbsMaPassword", "")
|
||||
self.provider_npi = raw.get("providerNpi", "")
|
||||
self.driver: webdriver.Chrome | None = None
|
||||
|
||||
# ── Driver lifecycle ──────────────────────────────────────────────────────
|
||||
|
||||
def config_driver(self):
|
||||
self.driver = _fresh_driver()
|
||||
|
||||
def close_driver(self):
|
||||
try:
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
except Exception:
|
||||
pass
|
||||
self.driver = None
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _wait(self, timeout=15):
|
||||
return WebDriverWait(self.driver, timeout)
|
||||
|
||||
def _dob_mmddyyyy(self) -> str:
|
||||
"""Convert YYYY-MM-DD → MM/DD/YYYY."""
|
||||
try:
|
||||
parts = self.dob.split("-")
|
||||
return f"{parts[1]}/{parts[2]}/{parts[0]}"
|
||||
except Exception:
|
||||
return self.dob
|
||||
|
||||
# ── Step: Login ───────────────────────────────────────────────────────────
|
||||
|
||||
def login(self, url: str = "") -> str:
|
||||
"""
|
||||
Navigate to BCBS MA Provider Central, fill credentials, click Log in.
|
||||
Returns:
|
||||
"OTP_REQUIRED" – redirected to MFA challenge page (/mga/sps/authsvc)
|
||||
"SUCCESS" – landed on dashboard without OTP
|
||||
"ERROR:..." – something went wrong
|
||||
"""
|
||||
target = url or self.LOGIN_URL
|
||||
try:
|
||||
print(f"[BCBS MA login] Navigating to {target}")
|
||||
self.driver.get(target)
|
||||
|
||||
# Wait for username field: id="txtUsername0"
|
||||
username_input = self._wait(20).until(
|
||||
EC.presence_of_element_located((By.ID, "txtUsername0"))
|
||||
)
|
||||
username_input.clear()
|
||||
username_input.send_keys(self.username)
|
||||
print("[BCBS MA login] Username filled")
|
||||
|
||||
# Password field: id="txtPassword"
|
||||
password_input = self._wait(10).until(
|
||||
EC.presence_of_element_located((By.ID, "txtPassword"))
|
||||
)
|
||||
password_input.clear()
|
||||
password_input.send_keys(self.password)
|
||||
print("[BCBS MA login] Password filled")
|
||||
|
||||
# Log in button: id="ns_Z7_09ME1282N8N3B0QGV9ND6N20G2_loginSubmit"
|
||||
login_btn = self._wait(10).until(
|
||||
EC.element_to_be_clickable((By.ID, "ns_Z7_09ME1282N8N3B0QGV9ND6N20G2_loginSubmit"))
|
||||
)
|
||||
login_btn.click()
|
||||
print("[BCBS MA login] Log in clicked, waiting for response...")
|
||||
time.sleep(3)
|
||||
|
||||
return self._detect_post_login_state()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BCBS MA login] Error: {e}")
|
||||
return f"ERROR: login failed: {e}"
|
||||
|
||||
def _detect_post_login_state(self) -> str:
|
||||
"""Check current URL/DOM to decide what happened after login."""
|
||||
for _ in range(6):
|
||||
time.sleep(1)
|
||||
url = self.driver.current_url.lower()
|
||||
print(f"[BCBS MA] post-login URL: {url[:80]}")
|
||||
|
||||
if "authsvc" in url or "/mga/sps/" in url:
|
||||
print("[BCBS MA] OTP page detected")
|
||||
return "OTP_REQUIRED"
|
||||
|
||||
if "providerhome" in url or "portal" in url:
|
||||
# Check if we're on a real portal page (not login form)
|
||||
try:
|
||||
self.driver.find_element(By.XPATH,
|
||||
"//*[contains(@href,'eTools') or contains(text(),'eTools') or "
|
||||
"contains(@href,'eligibility') or contains(text(),'Eligibility')]"
|
||||
)
|
||||
print("[BCBS MA] Dashboard detected — logged in without OTP")
|
||||
return "SUCCESS"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("[BCBS MA] Could not determine post-login state")
|
||||
return "OTP_REQUIRED" # default assumption for BCBS MA
|
||||
|
||||
# ── Step: Submit OTP ──────────────────────────────────────────────────────
|
||||
|
||||
def submit_otp_step(self, otp: str) -> str:
|
||||
"""
|
||||
Enter the 6-digit OTP into the BCBS MA verification page and click Submit.
|
||||
OTP input: id="otppswd", name="otp.user.otp"
|
||||
Submit btn: id="submitButton" (starts disabled, enables after OTP typed)
|
||||
Returns "SUCCESS" or "ERROR:..."
|
||||
"""
|
||||
try:
|
||||
print(f"[BCBS MA OTP] Submitting OTP: {otp}")
|
||||
|
||||
# OTP input field: id="otppswd"
|
||||
otp_input = self._wait(15).until(
|
||||
EC.presence_of_element_located((By.ID, "otppswd"))
|
||||
)
|
||||
otp_input.clear()
|
||||
otp_input.send_keys(otp)
|
||||
print("[BCBS MA OTP] OTP entered")
|
||||
|
||||
# Submit button starts disabled — wait for it to become clickable
|
||||
submit_btn = self._wait(10).until(
|
||||
EC.element_to_be_clickable((By.ID, "submitButton"))
|
||||
)
|
||||
submit_btn.click()
|
||||
print("[BCBS MA OTP] Submit clicked, waiting for dashboard...")
|
||||
time.sleep(4)
|
||||
|
||||
# Wait for dashboard
|
||||
for _ in range(15):
|
||||
time.sleep(1)
|
||||
url = self.driver.current_url.lower()
|
||||
print(f"[BCBS MA OTP] URL: {url[:80]}")
|
||||
if "authsvc" not in url and "/mga/sps/" not in url:
|
||||
print("[BCBS MA OTP] Left OTP page — login successful")
|
||||
return "SUCCESS"
|
||||
|
||||
return "ERROR: OTP page still visible after submission"
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BCBS MA OTP] Error: {e}")
|
||||
return f"ERROR: OTP submission failed: {e}"
|
||||
|
||||
# ── Step 1: Navigate to ConnectCenter → New Eligibility Request ──────────
|
||||
|
||||
def step1(self) -> str:
|
||||
"""
|
||||
After OTP login:
|
||||
1. Click eTools menu
|
||||
2. Click ConnectCenter in the dropdown
|
||||
3. Click Go Now on the ConnectCenter launch page
|
||||
4. Click Continue in the popup (opens ConnectCenter in new tab)
|
||||
5. Switch to new tab, click Verification
|
||||
6. Click New Eligibility Request in dropdown
|
||||
Returns "SUCCESS" (on Eligibility Identifier page) or "ERROR:..."
|
||||
"""
|
||||
try:
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
# 1. Click eTools
|
||||
print("[BCBS MA step1] Clicking eTools...")
|
||||
etools = self._wait(15).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//span[text()='eTools'] | //a[.//span[text()='eTools']]"
|
||||
))
|
||||
)
|
||||
etools.click()
|
||||
print("[BCBS MA step1] eTools clicked, waiting for dropdown...")
|
||||
time.sleep(2)
|
||||
|
||||
# 2. Click ConnectCenter link in the dropdown
|
||||
print("[BCBS MA step1] Clicking ConnectCenter...")
|
||||
connect_center = self._wait(15).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//a[contains(@href,'connectcenter')]"
|
||||
))
|
||||
)
|
||||
connect_center.click()
|
||||
print("[BCBS MA step1] ConnectCenter clicked, waiting for page to load...")
|
||||
time.sleep(3)
|
||||
|
||||
# 3. Click Go Now on the ConnectCenter page
|
||||
print("[BCBS MA step1] Clicking Go Now...")
|
||||
go_now = self._wait(20).until(
|
||||
EC.element_to_be_clickable((By.ID, "GoNow-2c338de9-6a2f-4d71-b5fb-a6e4ae14be80"))
|
||||
)
|
||||
go_now.click()
|
||||
time.sleep(2)
|
||||
print("[BCBS MA step1] Go Now clicked — popup opened, pressing Enter to continue...")
|
||||
|
||||
# 4. Press Enter to auto-confirm the popup
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
ActionChains(self.driver).send_keys(Keys.ENTER).perform()
|
||||
time.sleep(4)
|
||||
print("[BCBS MA step1] Enter pressed — continuing to ConnectCenter")
|
||||
|
||||
# 5. Switch to the new tab that ConnectCenter opened
|
||||
self._wait(10).until(lambda d: len(d.window_handles) > 1)
|
||||
self.driver.switch_to.window(self.driver.window_handles[-1])
|
||||
print(f"[BCBS MA step1] Switched to new tab: {self.driver.current_url[:60]}")
|
||||
time.sleep(3)
|
||||
|
||||
# 6. Click Verification menu
|
||||
print("[BCBS MA step1] Clicking Verification...")
|
||||
verification = self._wait(15).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//a[text()='Verification' or normalize-space(text())='Verification']"
|
||||
))
|
||||
)
|
||||
verification.click()
|
||||
time.sleep(2)
|
||||
print("[BCBS MA step1] Verification clicked")
|
||||
|
||||
# 7. Click New Eligibility Request in dropdown
|
||||
print("[BCBS MA step1] Clicking New Eligibility Request...")
|
||||
new_elig = self._wait(10).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//a[@ng-click='newEligibility();']"
|
||||
))
|
||||
)
|
||||
new_elig.click()
|
||||
time.sleep(3)
|
||||
print("[BCBS MA step1] New Eligibility Request clicked — on Eligibility Identifier page")
|
||||
|
||||
return "SUCCESS"
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BCBS MA step1] Error: {e}")
|
||||
return f"ERROR: step1 failed: {e}"
|
||||
|
||||
# ── Step 2: Fill Eligibility Identifier form and get results ─────────────
|
||||
|
||||
def step2(self) -> dict:
|
||||
"""
|
||||
On the Eligibility Identifier page:
|
||||
1. Enter provider NPI → click Find Provider
|
||||
2. Select payer search option: Member ID, Subscriber Date Of Birth (value=2)
|
||||
3. Select service type: Dental Care [35] (value=33)
|
||||
4. Select place of service: OFFICE [11] (value=30)
|
||||
5. Enter member ID
|
||||
6. Enter date of birth (MM/DD/YYYY)
|
||||
7. Click Submit → wait for results page → PDF
|
||||
"""
|
||||
from selenium.webdriver.support.ui import Select
|
||||
|
||||
try:
|
||||
print("[BCBS MA step2] Filling Eligibility Identifier form...")
|
||||
|
||||
# 1. Enter provider NPI
|
||||
provider_id_input = self._wait(15).until(
|
||||
EC.presence_of_element_located((By.ID, "providerID"))
|
||||
)
|
||||
provider_id_input.clear()
|
||||
provider_id_input.send_keys(self.provider_npi)
|
||||
print(f"[BCBS MA step2] Provider NPI entered: {self.provider_npi}")
|
||||
|
||||
# Click Find Provider
|
||||
find_provider = self._wait(10).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//button[@ng-click='searchProviders()']"
|
||||
))
|
||||
)
|
||||
find_provider.click()
|
||||
print("[BCBS MA step2] Find Provider clicked, waiting...")
|
||||
time.sleep(3)
|
||||
|
||||
# 2. Payer search options → value="2": Member ID, Subscriber Date Of Birth
|
||||
payer_select = self._wait(10).until(
|
||||
EC.presence_of_element_located((By.ID, "payerSearchOptions"))
|
||||
)
|
||||
Select(payer_select).select_by_value("2")
|
||||
print("[BCBS MA step2] Payer search option selected: Member ID + DOB")
|
||||
time.sleep(1)
|
||||
|
||||
# 3. Service type → value="33": Dental Care [35]
|
||||
service_select = self._wait(10).until(
|
||||
EC.presence_of_element_located((By.ID, "serviceType"))
|
||||
)
|
||||
Select(service_select).select_by_value("33")
|
||||
print("[BCBS MA step2] Service type selected: Dental Care [35]")
|
||||
|
||||
# 4. Place of service → value="30": OFFICE [11]
|
||||
pos_select = self._wait(10).until(
|
||||
EC.presence_of_element_located((By.ID, "placeOfService"))
|
||||
)
|
||||
Select(pos_select).select_by_value("30")
|
||||
print("[BCBS MA step2] Place of service selected: OFFICE [11]")
|
||||
|
||||
# 5. Member ID — field name is subscriberMedicaidID
|
||||
member_input = self._wait(10).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//input[@name='subscriberMedicaidID' or @id='subscriberMedicaidID']"
|
||||
))
|
||||
)
|
||||
member_input.clear()
|
||||
member_input.send_keys(self.member_id)
|
||||
print(f"[BCBS MA step2] Member ID entered: {self.member_id}")
|
||||
|
||||
# 6. Date of birth — id="subscriberDateOfBirth", placeholder="mm/dd/yyyy"
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
dob_formatted = self._dob_mmddyyyy()
|
||||
dob_input = self._wait(10).until(
|
||||
EC.presence_of_element_located((By.ID, "subscriberDateOfBirth"))
|
||||
)
|
||||
# Double-click to focus, then type directly via ActionChains
|
||||
ActionChains(self.driver).double_click(dob_input).perform()
|
||||
time.sleep(0.3)
|
||||
ActionChains(self.driver).send_keys(dob_formatted).perform()
|
||||
print(f"[BCBS MA step2] DOB typed: {dob_formatted}")
|
||||
time.sleep(1)
|
||||
|
||||
# 7. Click Submit
|
||||
submit_btn = self._wait(10).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//button[@ng-click='submit()' and @type='submit']"
|
||||
))
|
||||
)
|
||||
submit_btn.click()
|
||||
print("[BCBS MA step2] Submit clicked, waiting for results...")
|
||||
time.sleep(5)
|
||||
|
||||
# Wait for results page to load
|
||||
self._wait(20).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//*[contains(text(),'Eligible') or contains(text(),'Not Eligible') or "
|
||||
"contains(text(),'Active') or contains(text(),'Inactive') or "
|
||||
"contains(text(),'Coverage') or contains(text(),'Benefit')]"
|
||||
))
|
||||
)
|
||||
print("[BCBS MA step2] Results page loaded")
|
||||
|
||||
# Click Expand All to expand all eligibility sections before PDFing
|
||||
try:
|
||||
expand_all = self._wait(10).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//h6[normalize-space(text())='Expand All']"
|
||||
))
|
||||
)
|
||||
expand_all.click()
|
||||
print("[BCBS MA step2] Expand All clicked, waiting for sections to expand...")
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
print(f"[BCBS MA step2] Expand All not found or failed: {e}")
|
||||
|
||||
# Extract eligibility status
|
||||
page_text = self.driver.find_element(By.TAG_NAME, "body").text
|
||||
text_lower = page_text.lower()
|
||||
if "not eligible" in text_lower or "inactive" in text_lower or "terminated" in text_lower:
|
||||
eligibility = "Not Eligible"
|
||||
elif "eligible" in text_lower or "active" in text_lower or "covered" in text_lower:
|
||||
eligibility = "Eligible"
|
||||
else:
|
||||
eligibility = "Unknown"
|
||||
|
||||
patient_name = f"{self.first_name} {self.last_name}".strip()
|
||||
print(f"[BCBS MA step2] Eligibility: {eligibility}")
|
||||
|
||||
# PDF the results page via CDP
|
||||
pdf_base64 = ""
|
||||
pdf_path = None
|
||||
try:
|
||||
result = self.driver.execute_cdp_cmd("Page.printToPDF", {
|
||||
"printBackground": True,
|
||||
"paperWidth": 8.5,
|
||||
"paperHeight": 11,
|
||||
"marginTop": 0.5,
|
||||
"marginBottom": 0.5,
|
||||
"marginLeft": 0.5,
|
||||
"marginRight": 0.5,
|
||||
})
|
||||
pdf_data = result.get("data", "")
|
||||
if pdf_data:
|
||||
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
||||
filename = f"bcbs_ma_eligibility_{self.member_id}_{int(time.time())}.pdf"
|
||||
pdf_path = os.path.join(DOWNLOAD_DIR, filename)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(base64.b64decode(pdf_data))
|
||||
pdf_base64 = pdf_data
|
||||
print(f"[BCBS MA step2] PDF saved: {pdf_path}")
|
||||
except Exception as e:
|
||||
print(f"[BCBS MA step2] PDF generation failed: {e}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"eligibility": eligibility,
|
||||
"patientName": patient_name,
|
||||
"memberId": self.member_id,
|
||||
"insurerName": "BCBS MA",
|
||||
"pdfBase64": pdf_base64,
|
||||
"pdf_path": pdf_path,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[BCBS MA step2] Error: {e}")
|
||||
return {"status": "error", "message": f"step2 failed: {e}"}
|
||||
Reference in New Issue
Block a user