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>
633 lines
30 KiB
Python
633 lines
30 KiB
Python
"""
|
|
United Ucard (OneHealthcareID) eligibility check worker.
|
|
|
|
Scaffolded from selenium_UnitedSCO_eligibilityCheckWorker.py — same shape
|
|
(config_driver / login / step1 / step2 / main_workflow) so it plugs into the
|
|
same helpers_*_eligibility.py + agent.py session/OTP pattern.
|
|
|
|
Login flow (confirmed against the live site, 2026-09-13):
|
|
0. https://www.uhcdental.com/content/provider/dental.html?state=abc (public site)
|
|
span.header-signin-btn ("Sign In") -> a.sign-in-button-link ("Provider Login")
|
|
which OIDC-redirects to identity.onehealthcareid.com (or straight back to the
|
|
dashboard if there's already a valid SSO session)
|
|
1. https://identity.onehealthcareid.com/oneapp/index.html#/login
|
|
input#username -> button#btnLogin ("Continue")
|
|
2. #/signin/password
|
|
input#login-pwd -> button#btnLogin ("Continue")
|
|
3. #/rba/options ("Verify Your Identity")
|
|
button#textMsg ("Via Text Message")
|
|
4. #/rba/options/text ("Access Code")
|
|
input#otpBox -> button#continuebtn ("Continue")
|
|
5. Lands on the UHC Dental provider dashboard:
|
|
https://secure.uhcdental.com/.../postloginhomescreen.html?...
|
|
|
|
step1()/step2() (the actual eligibility search + PDF capture, once logged in)
|
|
are left as TODO stubs — fill these in once we know the exact DOM for the
|
|
member search + eligibility result pages on the dashboard.
|
|
"""
|
|
from selenium.common.exceptions import WebDriverException, TimeoutException
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
import time
|
|
import os
|
|
import re
|
|
import base64
|
|
|
|
from uniteducard_browser_manager import get_browser_manager
|
|
|
|
|
|
class AutomationUnitedUcardEligibilityCheck:
|
|
def __init__(self, data):
|
|
self.headless = False
|
|
self.driver = None
|
|
|
|
self.data = data.get("data", {}) if isinstance(data, dict) else {}
|
|
|
|
# Flatten values for convenience
|
|
self.memberId = self.data.get("memberId", "")
|
|
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
|
self.firstName = self.data.get("firstName", "")
|
|
self.lastName = self.data.get("lastName", "")
|
|
self.uniteducard_username = self.data.get("uniteducardUsername", "")
|
|
self.uniteducard_password = self.data.get("uniteducardPassword", "")
|
|
self.payment_group_id = self.data.get("paymentGroupId", "")
|
|
|
|
# Use browser manager's download dir
|
|
self.download_dir = get_browser_manager().download_dir
|
|
os.makedirs(self.download_dir, exist_ok=True)
|
|
|
|
def config_driver(self):
|
|
# Use persistent browser from manager (keeps device trust tokens)
|
|
self.driver = get_browser_manager().get_driver(self.headless)
|
|
|
|
def _force_logout(self):
|
|
"""Force logout by clearing cookies for the One Healthcare ID domain."""
|
|
try:
|
|
print("[UnitedUcard login] Forcing logout due to credential change...")
|
|
browser_manager = get_browser_manager()
|
|
|
|
try:
|
|
self.driver.delete_all_cookies()
|
|
print("[UnitedUcard login] Cleared all cookies")
|
|
except Exception as e:
|
|
print(f"[UnitedUcard login] Error clearing cookies: {e}")
|
|
|
|
browser_manager.clear_credentials_hash()
|
|
print("[UnitedUcard login] Logout complete")
|
|
return True
|
|
except Exception as e:
|
|
print(f"[UnitedUcard login] Error during forced logout: {e}")
|
|
return False
|
|
|
|
def login(self, url):
|
|
"""
|
|
Log in via https://www.uhcdental.com/content/provider/dental.html (Sign In ->
|
|
Provider Login), which OIDC-redirects through identity.onehealthcareid.com.
|
|
|
|
Returns one of:
|
|
"ALREADY_LOGGED_IN" / "SUCCESS" - logged in, caller should proceed to step1
|
|
"OTP_REQUIRED" - caller should poll for OTP entry
|
|
"ERROR: <msg>" - login failed
|
|
"""
|
|
wait = WebDriverWait(self.driver, 30)
|
|
browser_manager = get_browser_manager()
|
|
|
|
try:
|
|
# Check if credentials have changed - if so, force logout first
|
|
if self.uniteducard_username and browser_manager.credentials_changed(self.uniteducard_username):
|
|
self._force_logout()
|
|
self.driver.get(url)
|
|
time.sleep(2)
|
|
|
|
# Navigate to the public UHC Dental provider portal (NOT directly to
|
|
# identity.onehealthcareid.com) — Sign In -> Provider Login triggers
|
|
# the OIDC redirect to the identity login page.
|
|
self.driver.get(url)
|
|
time.sleep(3)
|
|
|
|
current_url = self.driver.current_url
|
|
print(f"[UnitedUcard login] Current URL: {current_url}")
|
|
|
|
# If we've already landed on the UHC Dental provider dashboard
|
|
# (secure.uhcdental.com/.../postloginhomescreen.html), a prior
|
|
# session is still valid.
|
|
if "secure.uhcdental.com" in current_url.lower():
|
|
print("[UnitedUcard login] Already logged in - on UHC Dental dashboard")
|
|
return "ALREADY_LOGGED_IN"
|
|
|
|
# ── Step 0: public site -> Sign In -> Provider Login ────────────
|
|
# Confirmed against live site (2026-09-13):
|
|
# <span class="header-signin-btn">Sign In</span> (opens dropdown)
|
|
# <a class="sign-in-button-link" href="https://identity.onehealthcareid.com/
|
|
# oidc/authorize?client_id=DBP92444&response_type=code&scope=openid+profile+email&
|
|
# redirect_uri=https://secure.uhcdental.com/content/dental-benefits-provider/
|
|
# en/secure/postloginhomescreen.html">Provider Login</a>
|
|
if "onehealthcareid.com" not in current_url.lower():
|
|
try:
|
|
signin_btn = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//span[contains(@class,'header-signin-btn') or contains(text(),'Sign In')]"))
|
|
)
|
|
signin_btn.click()
|
|
print("[UnitedUcard login] Clicked 'Sign In' to open dropdown")
|
|
time.sleep(1)
|
|
except TimeoutException:
|
|
return "ERROR: 'Sign In' button not found on UHC Dental provider site"
|
|
|
|
# Match by exact visible text "Provider Login" — the dropdown
|
|
# also has an "Administrator Login" link that shares the same
|
|
# class AND the same oidc/authorize href pattern (just a
|
|
# different client_id), so text is the only reliable
|
|
# differentiator. It's also always the first item in the
|
|
# dropdown (Provider Login above Administrator Login).
|
|
clicked_provider_login = False
|
|
try:
|
|
provider_login_link = WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//a[normalize-space(text())='Provider Login']"))
|
|
)
|
|
self.driver.execute_script(
|
|
"arguments[0].scrollIntoView({block:'center'});", provider_login_link
|
|
)
|
|
time.sleep(0.3)
|
|
try:
|
|
provider_login_link.click()
|
|
print("[UnitedUcard login] Clicked 'Provider Login' (direct)")
|
|
clicked_provider_login = True
|
|
except Exception as direct_click_err:
|
|
print(f"[UnitedUcard login] Direct click failed ({direct_click_err}); trying JS click")
|
|
self.driver.execute_script("arguments[0].click();", provider_login_link)
|
|
print("[UnitedUcard login] Clicked 'Provider Login' (JS)")
|
|
clicked_provider_login = True
|
|
time.sleep(3)
|
|
except TimeoutException:
|
|
pass
|
|
|
|
if not clicked_provider_login:
|
|
# Fallback: the sign-in dropdown's first link is always
|
|
# "Provider Login" (Administrator Login is second) —
|
|
# click by position instead of matching text/href.
|
|
try:
|
|
dropdown_link = WebDriverWait(self.driver, 5).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"(//a[contains(@class,'sign-in-button-link')])[1]"))
|
|
)
|
|
self.driver.execute_script("arguments[0].click();", dropdown_link)
|
|
print("[UnitedUcard login] Clicked first dropdown link (Provider Login, by position)")
|
|
clicked_provider_login = True
|
|
time.sleep(3)
|
|
except Exception as e:
|
|
return f"ERROR: 'Provider Login' link not found in Sign In dropdown: {e}"
|
|
|
|
current_url = self.driver.current_url
|
|
print(f"[UnitedUcard login] After Provider Login click URL: {current_url}")
|
|
|
|
# SSO may skip straight back to the dashboard if already authenticated
|
|
if "secure.uhcdental.com" in current_url.lower():
|
|
print("[UnitedUcard login] Already logged in - redirected straight to dashboard")
|
|
return "ALREADY_LOGGED_IN"
|
|
|
|
# Check for OTP input first (in case device left mid-OTP)
|
|
try:
|
|
WebDriverWait(self.driver, 3).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//input[@type='tel' or contains(@placeholder,'code') or contains(@placeholder,'Code') or "
|
|
"contains(@aria-label,'Verification') or contains(@id,'otp') or contains(@name,'otp')]"))
|
|
)
|
|
print("[UnitedUcard login] OTP input found on landing")
|
|
return "OTP_REQUIRED"
|
|
except TimeoutException:
|
|
pass
|
|
|
|
# ── Step 1: "One Healthcare ID or Email Address" ────────────────
|
|
# Confirmed against live site (2026-09-13):
|
|
# <input id="username" data-testid="username" ...>
|
|
# <button id="btnLogin" data-cy="data-btnLogin-field">Continue</button>
|
|
try:
|
|
username_field = wait.until(
|
|
EC.element_to_be_clickable((By.ID, "username"))
|
|
)
|
|
username_field.clear()
|
|
username_field.send_keys(self.uniteducard_username)
|
|
print(f"[UnitedUcard login] Entered username: {self.uniteducard_username}")
|
|
except TimeoutException:
|
|
return "ERROR: Username field not found on login page"
|
|
|
|
try:
|
|
continue_btn = wait.until(
|
|
EC.element_to_be_clickable((By.ID, "btnLogin"))
|
|
)
|
|
continue_btn.click()
|
|
print("[UnitedUcard login] Clicked Continue (btnLogin) after username")
|
|
time.sleep(2)
|
|
except TimeoutException:
|
|
return "ERROR: Continue button (btnLogin) not found after entering username"
|
|
|
|
# ── Step 2: "Enter Your Password" page (#/signin/password) ─────
|
|
# Confirmed against live site (2026-09-13):
|
|
# <input id="login-pwd" data-testid="login-pwd" type="password" ...>
|
|
# <button id="btnLogin" data-cy="data-btnLogin-field">Continue</button>
|
|
# The email field on this page is pre-filled/read-only from step 1.
|
|
try:
|
|
password_field = wait.until(
|
|
EC.presence_of_element_located((By.ID, "login-pwd"))
|
|
)
|
|
password_field.clear()
|
|
password_field.send_keys(self.uniteducard_password)
|
|
print("[UnitedUcard login] Entered password")
|
|
except TimeoutException:
|
|
return "ERROR: Password field not found on login page"
|
|
|
|
try:
|
|
signin_button = wait.until(
|
|
EC.element_to_be_clickable((By.ID, "btnLogin"))
|
|
)
|
|
signin_button.click()
|
|
print("[UnitedUcard login] Clicked Continue (btnLogin) on password page")
|
|
except TimeoutException:
|
|
return "ERROR: Continue button (btnLogin) not found on password page"
|
|
|
|
if self.uniteducard_username:
|
|
browser_manager.save_credentials_hash(self.uniteducard_username)
|
|
|
|
time.sleep(5) # Wait for login to process
|
|
|
|
current_url_after_login = self.driver.current_url.lower()
|
|
print(f"[UnitedUcard login] After login URL: {current_url_after_login}")
|
|
|
|
if "secure.uhcdental.com" in current_url_after_login:
|
|
print("[UnitedUcard login] Login successful - redirected to UHC Dental dashboard")
|
|
return "SUCCESS"
|
|
|
|
# ── Step 3: "Verify Your Identity" MFA method page (#/rba/options) ──
|
|
# Confirmed against live site (2026-09-13):
|
|
# <button id="textMsg" data-cy="data-textMsg-field">Via Text Message</button>
|
|
if "rba/options" in current_url_after_login:
|
|
try:
|
|
text_msg_btn = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.ID, "textMsg"))
|
|
)
|
|
text_msg_btn.click()
|
|
print("[UnitedUcard login] Clicked 'Via Text Message' on MFA options page")
|
|
time.sleep(3)
|
|
except TimeoutException:
|
|
print("[UnitedUcard login] MFA options page detected but 'Via Text Message' button not found")
|
|
|
|
current_url_after_login = self.driver.current_url.lower()
|
|
print(f"[UnitedUcard login] After MFA method selection URL: {current_url_after_login}")
|
|
|
|
# Check for OTP input after submitting credentials
|
|
try:
|
|
WebDriverWait(self.driver, 15).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//input[@type='tel' or contains(@placeholder,'code') or contains(@placeholder,'Code') or "
|
|
"contains(@aria-label,'Verification') or contains(@aria-label,'verification') or "
|
|
"contains(@name,'otp') or contains(@name,'code') or contains(@id,'otp') or contains(@id,'code')]"
|
|
))
|
|
)
|
|
print("[UnitedUcard login] OTP input detected -> OTP_REQUIRED")
|
|
return "OTP_REQUIRED"
|
|
except TimeoutException:
|
|
print("[UnitedUcard login] No OTP input detected")
|
|
|
|
# Re-check after waiting for OTP check
|
|
current_url_after_login = self.driver.current_url.lower()
|
|
if "secure.uhcdental.com" in current_url_after_login:
|
|
print("[UnitedUcard login] Login successful - redirected to UHC Dental dashboard")
|
|
return "SUCCESS"
|
|
|
|
# Check for error messages on the page
|
|
try:
|
|
error_elem = self.driver.find_element(By.XPATH,
|
|
"//*[contains(@class,'error') or contains(@class,'alert')]")
|
|
error_text = error_elem.text
|
|
if error_text:
|
|
print(f"[UnitedUcard login] Error on page: {error_text}")
|
|
return f"ERROR: {error_text}"
|
|
except Exception:
|
|
pass
|
|
|
|
return "ERROR: Login did not complete - still on identity domain"
|
|
|
|
except Exception as e:
|
|
print(f"[UnitedUcard login] Exception: {e}")
|
|
return f"ERROR:LOGIN FAILED: {e}"
|
|
|
|
def _format_dob(self, dob_str):
|
|
"""Convert DOB from YYYY-MM-DD to MM/DD/YYYY format"""
|
|
if dob_str and "-" in dob_str:
|
|
dob_parts = dob_str.split("-")
|
|
if len(dob_parts) == 3:
|
|
# YYYY-MM-DD -> MM/DD/YYYY
|
|
return f"{dob_parts[1]}/{dob_parts[2]}/{dob_parts[0]}"
|
|
return dob_str
|
|
|
|
def step1(self):
|
|
"""
|
|
Fill in the "Eligibility Search" form — it's the default tab already
|
|
showing on the dashboard (postloginhomescreen.html) right after login.
|
|
|
|
Confirmed against live site (2026-09-13):
|
|
<input id="serviceDate"> - defaults blank; fill with today's date, MM/DD/YYYY
|
|
<input id="eligMemDob"> - Member Date of Birth, MM/DD/YYYY
|
|
<input id="eligSubsId"> - Subscriber ID
|
|
<input type="button" value="search" ng-click="submit()">
|
|
"Search By: Subscriber ID" and "Search For: Individual" are already
|
|
the default radio selections, so no extra clicks needed there.
|
|
"""
|
|
from datetime import date
|
|
|
|
def _fill_field(field_id, value, label):
|
|
"""Wait for field to be clickable, scroll into view, then fill it.
|
|
Falls back to a JS value-set (+dispatch input/change events, which
|
|
Angular's ng-model listens for) if send_keys hits an
|
|
ElementNotInteractable error — the dashboard's Angular app can
|
|
take a beat to finish rendering/enabling these fields."""
|
|
field = WebDriverWait(self.driver, 20).until(
|
|
EC.presence_of_element_located((By.ID, field_id))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", field)
|
|
time.sleep(0.3)
|
|
try:
|
|
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.ID, field_id)))
|
|
field.clear()
|
|
field.send_keys(value)
|
|
print(f"[UnitedUcard step1] Entered {label}: {value}")
|
|
except Exception as e:
|
|
print(f"[UnitedUcard step1] send_keys failed for {label} ({e}); falling back to JS set")
|
|
self.driver.execute_script("""
|
|
var el = arguments[0];
|
|
var val = arguments[1];
|
|
var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
|
nativeSetter.call(el, val);
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
el.dispatchEvent(new Event('keyup', { bubbles: true }));
|
|
""", field, value)
|
|
print(f"[UnitedUcard step1] Entered {label} via JS: {value}")
|
|
return field
|
|
|
|
try:
|
|
print(f"[UnitedUcard step1] Starting eligibility search for memberId={self.memberId}, DOB={self.dateOfBirth}")
|
|
|
|
# Give the dashboard's Angular app a moment to finish rendering
|
|
# after the redirect from login — filling fields too early was
|
|
# throwing "element not interactable".
|
|
WebDriverWait(self.driver, 20).until(
|
|
lambda d: d.execute_script("return document.readyState") == "complete"
|
|
)
|
|
time.sleep(2)
|
|
|
|
# Service Date - default to today.
|
|
# NOTE: serviceDate/eligMemDob both use onkeydown="dateFunction(...)",
|
|
# a JS input mask that auto-inserts "/" as you type raw digits
|
|
# (same as manually typing it). Sending an already-formatted
|
|
# "MM/DD/YYYY" string double-processes the slashes through the
|
|
# mask and corrupts the stored value — send digits only instead,
|
|
# matching what a human typing the field actually does.
|
|
try:
|
|
today_digits = date.today().strftime("%m%d%Y")
|
|
_fill_field("serviceDate", today_digits, "Service Date")
|
|
except TimeoutException:
|
|
return "ERROR: Service Date field (serviceDate) not found on Eligibility Search page"
|
|
except Exception as e:
|
|
return f"ERROR: Could not enter Service Date: {e}"
|
|
|
|
# Member Date of Birth
|
|
try:
|
|
dob_digits = re.sub(r"\D", "", self._format_dob(self.dateOfBirth))
|
|
_fill_field("eligMemDob", dob_digits, "Member DOB")
|
|
except Exception as e:
|
|
return f"ERROR: Could not enter Member Date of Birth: {e}"
|
|
|
|
# Subscriber ID
|
|
try:
|
|
_fill_field("eligSubsId", self.memberId, "Subscriber ID")
|
|
except Exception as e:
|
|
return f"ERROR: Could not enter Subscriber ID: {e}"
|
|
|
|
time.sleep(0.5)
|
|
|
|
# Click SEARCH button
|
|
try:
|
|
search_btn = WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//input[@type='button' and @value='search']"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", search_btn)
|
|
time.sleep(0.3)
|
|
try:
|
|
search_btn.click()
|
|
except Exception:
|
|
self.driver.execute_script("arguments[0].click();", search_btn)
|
|
print("[UnitedUcard step1] Clicked SEARCH button")
|
|
except Exception as e:
|
|
return f"ERROR: Could not click SEARCH button: {e}"
|
|
|
|
time.sleep(3)
|
|
|
|
# TODO: confirm what the results look like (same page update? new
|
|
# tab? modal?) once we see it, then extend step2() accordingly.
|
|
print("[UnitedUcard step1] Eligibility search submitted")
|
|
return "Success"
|
|
|
|
except Exception as e:
|
|
print(f"[UnitedUcard step1] Exception: {e}")
|
|
return f"ERROR:STEP1 - {e}"
|
|
|
|
def step2(self):
|
|
"""
|
|
Extract patient/eligibility info from the "Eligibility Summary" page
|
|
(secure.uhcdental.com/.../eligibility-summary.html) that step1()'s
|
|
SEARCH click lands on, then capture it as a PDF.
|
|
|
|
Confirmed against live site (2026-09-13) via screenshot/PDF export:
|
|
"Patient" panel -> patient full name (e.g. "WINGHUN KWONG")
|
|
"Insurance Information" -> status badges: "In Network",
|
|
"Eligible" (or "Not Eligible"/"Ineligible"), "No Essential
|
|
Health Benefits", and a MASKED Subscriber ID (e.g. "******444")
|
|
— we deliberately do NOT use that masked ID to overwrite the
|
|
patient record.
|
|
Exact DOM ids/classes for the badges haven't been inspected yet, so
|
|
this uses text-based extraction (same approach as UnitedSCO/DDMA)
|
|
as a robust starting point.
|
|
"""
|
|
try:
|
|
print("[UnitedUcard step2] Starting eligibility capture")
|
|
|
|
try:
|
|
WebDriverWait(self.driver, 20).until(
|
|
lambda d: d.execute_script("return document.readyState") == "complete"
|
|
)
|
|
except Exception:
|
|
pass
|
|
time.sleep(2)
|
|
|
|
current_url = self.driver.current_url
|
|
print(f"[UnitedUcard step2] Current URL: {current_url}")
|
|
|
|
page_text = ""
|
|
try:
|
|
page_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
except Exception:
|
|
pass
|
|
|
|
# ── Eligibility status ───────────────────────────────────────
|
|
eligibilityText = "unknown"
|
|
lower_text = page_text.lower()
|
|
if "not eligible" in lower_text or "ineligible" in lower_text:
|
|
eligibilityText = "inactive"
|
|
elif "eligible" in lower_text:
|
|
eligibilityText = "active"
|
|
print(f"[UnitedUcard step2] Eligibility status: {eligibilityText}")
|
|
|
|
# ── Patient name ─────────────────────────────────────────────
|
|
patientName = f"{self.firstName} {self.lastName}".strip()
|
|
name_extracted = False
|
|
|
|
# Strategy 1: text right after the "Patient" heading, e.g.
|
|
# "Patient\nWINGHUN KWONG\nSUBSCRIBER/INSURED"
|
|
try:
|
|
name_match = re.search(
|
|
r'Patient\s*\n\s*([A-Z][A-Za-z\-\']+(?:\s+[A-Z][A-Za-z\-\']+)+)\s*\n',
|
|
page_text
|
|
)
|
|
if name_match:
|
|
candidate = name_match.group(1).strip()
|
|
if candidate and len(candidate) < 60 and "eligible" not in candidate.lower():
|
|
patientName = candidate.title()
|
|
name_extracted = True
|
|
print(f"[UnitedUcard step2] Extracted patient name from text: {patientName}")
|
|
except Exception as e:
|
|
print(f"[UnitedUcard step2] Name regex failed: {e}")
|
|
|
|
# Strategy 2: DOM heuristic fallback — heading/strong element
|
|
# following a "Patient" label
|
|
if not name_extracted:
|
|
try:
|
|
elems = self.driver.find_elements(By.XPATH,
|
|
"//*[contains(text(),'Patient')]/following::*[self::h1 or self::h2 or self::h3 or self::strong][1]"
|
|
)
|
|
for elem in elems:
|
|
txt = elem.text.strip()
|
|
if txt and len(txt.split()) >= 2 and txt[0].isupper() and len(txt) < 60:
|
|
patientName = txt.title()
|
|
name_extracted = True
|
|
print(f"[UnitedUcard step2] Extracted patient name from DOM: {patientName}")
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if not name_extracted:
|
|
print("[UnitedUcard step2] WARNING: Could not extract patient name from page; using form values")
|
|
|
|
# ── Capture PDF ──────────────────────────────────────────────
|
|
pdf_path = self._capture_pdf(self.memberId)
|
|
if not pdf_path:
|
|
return {"status": "error", "message": "STEP2 FAILED: Could not generate PDF"}
|
|
|
|
print(f"[UnitedUcard step2] PDF saved: {pdf_path}")
|
|
|
|
self._hide_browser()
|
|
|
|
print("[UnitedUcard step2] Eligibility capture complete")
|
|
|
|
return {
|
|
"status": "success",
|
|
"eligibility": eligibilityText,
|
|
"ss_path": pdf_path,
|
|
"pdf_path": pdf_path,
|
|
"patientName": patientName,
|
|
# NOTE: Subscriber ID shown on this page is masked (e.g.
|
|
# "******444"), so we return the ID we searched with rather
|
|
# than a scraped-and-truncated value.
|
|
"memberId": self.memberId,
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"[UnitedUcard step2] Exception: {e}")
|
|
return {"status": "error", "message": f"STEP2 FAILED: {str(e)}"}
|
|
|
|
def _capture_pdf(self, member_id):
|
|
"""Capture the current page as PDF using Chrome DevTools Protocol."""
|
|
try:
|
|
pdf_options = {
|
|
"landscape": False,
|
|
"displayHeaderFooter": False,
|
|
"printBackground": True,
|
|
"preferCSSPageSize": True,
|
|
"paperWidth": 8.5,
|
|
"paperHeight": 11,
|
|
"marginTop": 0.4,
|
|
"marginBottom": 0.4,
|
|
"marginLeft": 0.4,
|
|
"marginRight": 0.4,
|
|
"scale": 0.9,
|
|
}
|
|
|
|
file_identifier = member_id if member_id else f"{self.firstName}_{self.lastName}"
|
|
|
|
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
|
|
pdf_data = base64.b64decode(result.get('data', ''))
|
|
pdf_path = os.path.join(self.download_dir, f"uniteducard_eligibility_{file_identifier}_{int(time.time())}.pdf")
|
|
with open(pdf_path, "wb") as f:
|
|
f.write(pdf_data)
|
|
return pdf_path
|
|
except Exception as e:
|
|
print(f"[UnitedUcard _capture_pdf] Error: {e}")
|
|
return None
|
|
|
|
def _hide_browser(self):
|
|
"""Hide the browser window after task completion using multiple strategies."""
|
|
try:
|
|
try:
|
|
self.driver.get("about:blank")
|
|
time.sleep(0.5)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.driver.minimize_window()
|
|
print("[UnitedUcard step2] Browser window minimized")
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.driver.set_window_position(-10000, -10000)
|
|
print("[UnitedUcard step2] Browser window moved off-screen")
|
|
return
|
|
except Exception:
|
|
pass
|
|
except Exception as e:
|
|
print(f"[UnitedUcard step2] Could not hide browser: {e}")
|
|
|
|
def main_workflow(self, url):
|
|
"""Main workflow that runs all steps."""
|
|
try:
|
|
self.config_driver()
|
|
|
|
login_result = self.login(url)
|
|
print(f"[main_workflow] Login result: {login_result}")
|
|
|
|
if login_result == "OTP_REQUIRED":
|
|
return {"status": "otp_required", "message": "OTP required after login"}
|
|
|
|
if isinstance(login_result, str) and login_result.startswith("ERROR"):
|
|
return {"status": "error", "message": login_result}
|
|
|
|
step1_result = self.step1()
|
|
print(f"[main_workflow] Step1 result: {step1_result}")
|
|
|
|
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step1_result}
|
|
|
|
step2_result = self.step2()
|
|
print(f"[main_workflow] Step2 result: {step2_result}")
|
|
|
|
return step2_result
|
|
|
|
except Exception as e:
|
|
return {"status": "error", "message": str(e)}
|