feat: DentaQuest eligibility — PLAN_NOT_ACCEPTED status, DOB save, no retries
- Add PLAN_NOT_ACCEPTED to PatientStatus enum (prisma schema + db push) - Selenium: return "plan not accepted" eligibility text instead of collapsing to inactive - Backend processor: map "plan not accepted" → PLAN_NOT_ACCEPTED, fix insuranceProvider label - _shared.ts: save DOB for existing patients when field is currently empty - Frontend: show amber "Plan Not Accepted" badge in patient table and detail panel - patient-form.tsx: display "Plan Not Accepted" label in status dropdown - BullMQ: set attempts=1 (no retry on selenium failure) - DDMA: remove first/last name from search (member ID + DOB only) - patient-types.ts: allow alphanumeric insurance IDs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,7 +111,26 @@ class DentaQuestBrowserManager:
|
||||
print("[DentaQuest BrowserManager] Cleared IndexedDB")
|
||||
except Exception as e:
|
||||
print(f"[DentaQuest BrowserManager] Could not clear IndexedDB: {e}")
|
||||
|
||||
|
||||
# Clear browser caches (prevents Chrome crash from corrupted cache)
|
||||
cache_dirs = [
|
||||
os.path.join(self.profile_dir, "Default", "Cache"),
|
||||
os.path.join(self.profile_dir, "Default", "Code Cache"),
|
||||
os.path.join(self.profile_dir, "Default", "GPUCache"),
|
||||
os.path.join(self.profile_dir, "Default", "Service Worker"),
|
||||
os.path.join(self.profile_dir, "Cache"),
|
||||
os.path.join(self.profile_dir, "Code Cache"),
|
||||
os.path.join(self.profile_dir, "GPUCache"),
|
||||
os.path.join(self.profile_dir, "ShaderCache"),
|
||||
]
|
||||
for cache_dir in cache_dirs:
|
||||
if os.path.exists(cache_dir):
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
print(f"[DentaQuest BrowserManager] Cleared {os.path.basename(cache_dir)}")
|
||||
except Exception as e:
|
||||
print(f"[DentaQuest BrowserManager] Could not clear {os.path.basename(cache_dir)}: {e}")
|
||||
|
||||
# Set flag to clear session via JavaScript after browser opens
|
||||
self._needs_session_clear = True
|
||||
|
||||
@@ -181,13 +200,13 @@ class DentaQuestBrowserManager:
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# Remove SingletonLock if exists
|
||||
lock_file = os.path.join(self.profile_dir, "SingletonLock")
|
||||
try:
|
||||
if os.path.islink(lock_file) or os.path.exists(lock_file):
|
||||
os.remove(lock_file)
|
||||
except:
|
||||
pass
|
||||
for lock_file in ["SingletonLock", "SingletonSocket", "SingletonCookie"]:
|
||||
lock_path = os.path.join(self.profile_dir, lock_file)
|
||||
try:
|
||||
if os.path.islink(lock_path) or os.path.exists(lock_path):
|
||||
os.remove(lock_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_driver(self, headless=False):
|
||||
"""Get or create the persistent browser instance."""
|
||||
@@ -232,7 +251,12 @@ class DentaQuestBrowserManager:
|
||||
options.add_argument(f"--user-data-dir={self.profile_dir}")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
options.add_argument("--disable-infobars")
|
||||
|
||||
prefs = {
|
||||
"download.default_directory": self.download_dir,
|
||||
"plugins.always_open_pdf_externally": True,
|
||||
|
||||
@@ -274,6 +274,9 @@ async def start_dentaquest_run(sid: str, data: dict, url: str):
|
||||
return {"status": "error", "message": s["message"]}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[start_dentaquest_run] EXCEPTION: {e}")
|
||||
traceback.print_exc()
|
||||
s["status"] = "error"
|
||||
s["message"] = f"worker exception: {e}"
|
||||
await cleanup_session(sid)
|
||||
|
||||
@@ -305,65 +305,40 @@ class AutomationDentaQuestEligibilityCheck:
|
||||
print(f"[DentaQuest step1] Error filling {field_name}: {e}")
|
||||
return False
|
||||
|
||||
# 1. Select Provider from dropdown (required field)
|
||||
# 1. Select Location from dropdown (required field before search)
|
||||
try:
|
||||
print("[DentaQuest step1] Selecting Provider...")
|
||||
# Try to find and click Provider dropdown
|
||||
provider_selectors = [
|
||||
"//label[contains(text(),'Provider')]/following-sibling::*//div[contains(@class,'select')]",
|
||||
"//div[contains(@data-testid,'provider')]//div[contains(@class,'select')]",
|
||||
"//*[@aria-label='Provider']",
|
||||
"//select[contains(@name,'provider') or contains(@id,'provider')]",
|
||||
"//div[contains(@class,'provider')]//input",
|
||||
"//label[contains(text(),'Provider')]/..//div[contains(@class,'control')]"
|
||||
]
|
||||
|
||||
provider_clicked = False
|
||||
for selector in provider_selectors:
|
||||
print("[DentaQuest step1] Selecting Location...")
|
||||
|
||||
# Click the Location dropdown button (stable data-testid)
|
||||
location_clicked = False
|
||||
try:
|
||||
trigger = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="member-search_location_select-btn"]'))
|
||||
)
|
||||
trigger.click()
|
||||
print("[DentaQuest step1] Clicked location dropdown button")
|
||||
time.sleep(0.5)
|
||||
location_clicked = True
|
||||
except TimeoutException:
|
||||
print("[DentaQuest step1] Warning: Location button not found by data-testid")
|
||||
|
||||
if location_clicked:
|
||||
# Wait for options to appear and click the first one (role="option")
|
||||
try:
|
||||
provider_dropdown = WebDriverWait(self.driver, 3).until(
|
||||
EC.element_to_be_clickable((By.XPATH, selector))
|
||||
first_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "(//li[@role='option'])[1]"))
|
||||
)
|
||||
provider_dropdown.click()
|
||||
print(f"[DentaQuest step1] Clicked provider dropdown with selector: {selector}")
|
||||
time.sleep(0.5)
|
||||
provider_clicked = True
|
||||
break
|
||||
opt_text = first_option.get_attribute("aria-label") or first_option.text.strip()
|
||||
first_option.click()
|
||||
print(f"[DentaQuest step1] Selected location: {opt_text[:60]}")
|
||||
time.sleep(0.3)
|
||||
except TimeoutException:
|
||||
continue
|
||||
|
||||
if provider_clicked:
|
||||
# Select first available provider option
|
||||
option_selectors = [
|
||||
"//div[contains(@class,'option') and not(contains(@class,'disabled'))]",
|
||||
"//li[contains(@class,'option')]",
|
||||
"//option[not(@disabled)]",
|
||||
"//*[@role='option']"
|
||||
]
|
||||
|
||||
for opt_selector in option_selectors:
|
||||
try:
|
||||
options = self.driver.find_elements(By.XPATH, opt_selector)
|
||||
if options:
|
||||
# Select first non-placeholder option
|
||||
for opt in options:
|
||||
opt_text = opt.text.strip()
|
||||
if opt_text and "select" not in opt_text.lower():
|
||||
opt.click()
|
||||
print(f"[DentaQuest step1] Selected provider: {opt_text}")
|
||||
break
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
# Close dropdown if still open
|
||||
ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
|
||||
time.sleep(0.3)
|
||||
print("[DentaQuest step1] Warning: Location options did not appear")
|
||||
else:
|
||||
print("[DentaQuest step1] Warning: Could not find Provider dropdown")
|
||||
|
||||
print("[DentaQuest step1] Warning: Could not find Location dropdown trigger")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DentaQuest step1] Error selecting provider: {e}")
|
||||
print(f"[DentaQuest step1] Error selecting location: {e}")
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
@@ -501,29 +476,26 @@ class AutomationDentaQuestEligibilityCheck:
|
||||
if self.memberId:
|
||||
foundMemberId = self.memberId
|
||||
|
||||
# Extract eligibility status
|
||||
status_selectors = [
|
||||
"(//tbody//tr)[1]//a[contains(@href, 'eligibility')]",
|
||||
"//a[contains(@href,'eligibility')]",
|
||||
"//*[contains(@class,'status')]",
|
||||
"//*[contains(text(),'Active') or contains(text(),'Inactive') or contains(text(),'Eligible')]"
|
||||
]
|
||||
|
||||
for selector in status_selectors:
|
||||
try:
|
||||
status_elem = self.driver.find_element(By.XPATH, selector)
|
||||
status_text = status_elem.text.strip().lower()
|
||||
if status_text:
|
||||
print(f"[DentaQuest step2] Found status with selector '{selector}': {status_text}")
|
||||
if "active" in status_text or "eligible" in status_text:
|
||||
eligibilityText = "active"
|
||||
break
|
||||
elif "inactive" in status_text or "ineligible" in status_text:
|
||||
eligibilityText = "inactive"
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
# Extract eligibility status from first result row
|
||||
try:
|
||||
status_elem = self.driver.find_element(By.XPATH,
|
||||
"(//tbody//tr)[1]//*[self::span or self::a or self::div]["
|
||||
"contains(text(),'Active') or contains(text(),'Inactive') or "
|
||||
"contains(text(),'Eligible') or contains(text(),'Ineligible') or "
|
||||
"contains(text(),'Plan not accepted') or contains(text(),'Not eligible')"
|
||||
"]"
|
||||
)
|
||||
status_text = status_elem.text.strip().lower()
|
||||
print(f"[DentaQuest step2] Found eligibility status: '{status_text}'")
|
||||
if "active" in status_text or ("eligible" in status_text and "ineligible" not in status_text):
|
||||
eligibilityText = "active"
|
||||
elif "plan not accepted" in status_text:
|
||||
eligibilityText = "plan not accepted"
|
||||
else:
|
||||
eligibilityText = "inactive"
|
||||
except Exception as e:
|
||||
print(f"[DentaQuest step2] Could not find eligibility status: {e}")
|
||||
|
||||
print(f"[DentaQuest step2] Final eligibility status: {eligibilityText}")
|
||||
|
||||
# 2) Find the patient detail link and navigate DIRECTLY to it
|
||||
@@ -685,6 +657,11 @@ class AutomationDentaQuestEligibilityCheck:
|
||||
if not patientName:
|
||||
print("[DentaQuest step2] Could not extract patient name")
|
||||
else:
|
||||
# Strip any trailing date (e.g. "PRINCILLA WALKER 01/09/2026")
|
||||
import re as _re
|
||||
patientName = _re.sub(r'\s*\d{1,2}/\d{1,2}/\d{2,4}\s*$', '', patientName).strip()
|
||||
# Also strip "DOB: ..." prefix/suffix
|
||||
patientName = _re.sub(r'\s*DOB[:\s]*\d{1,2}/\d{1,2}/\d{2,4}\s*', '', patientName, flags=_re.IGNORECASE).strip()
|
||||
print(f"[DentaQuest step2] Patient name: {patientName}")
|
||||
|
||||
# Wait for page to fully load before generating PDF
|
||||
@@ -721,14 +698,14 @@ class AutomationDentaQuestEligibilityCheck:
|
||||
f.write(pdf_data)
|
||||
print(f"[DentaQuest step2] PDF saved: {pdf_path}")
|
||||
|
||||
# Close the browser window after PDF generation
|
||||
# Close browser after PDF (session preserved in profile)
|
||||
try:
|
||||
from dentaquest_browser_manager import get_browser_manager
|
||||
get_browser_manager().quit_driver()
|
||||
print("[DentaQuest step2] Browser closed")
|
||||
except Exception as e:
|
||||
print(f"[DentaQuest step2] Error closing browser: {e}")
|
||||
|
||||
|
||||
output = {
|
||||
"status": "success",
|
||||
"eligibility": eligibilityText,
|
||||
|
||||
Reference in New Issue
Block a user