From 03172f07109d3ffe465a06aa7fcd7de936f41e0c Mon Sep 17 00:00:00 2001 From: Emile Date: Wed, 11 Feb 2026 20:14:29 -0500 Subject: [PATCH] feat(eligibility-check) - enhance United SCO workflows with improved patient creation and update logic; added eligibility status handling and detailed logging; implemented browser cache clearing and anti-detection measures in Selenium service --- .../src/routes/insuranceStatusUnitedSCO.ts | 89 +- .../unitedsco-button-modal.tsx | 28 +- .../helpers_unitedsco_eligibility.py | 49 +- ...lenium_UnitedSCO_eligibilityCheckWorker.py | 844 +++++++++++++++--- .../unitedsco_browser_manager.py | 38 +- 5 files changed, 882 insertions(+), 166 deletions(-) diff --git a/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts b/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts index e3c4dd9..b166976 100644 --- a/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts +++ b/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts @@ -73,8 +73,9 @@ async function createOrUpdatePatientByInsuranceId(options: { lastName?: string | null; dob?: string | Date | null; userId: number; + eligibilityStatus?: string; // "ACTIVE" or "INACTIVE" }) { - const { insuranceId, firstName, lastName, dob, userId } = options; + const { insuranceId, firstName, lastName, dob, userId, eligibilityStatus } = options; if (!insuranceId) throw new Error("Missing insuranceId"); const incomingFirst = (firstName || "").trim(); @@ -101,14 +102,17 @@ async function createOrUpdatePatientByInsuranceId(options: { } return; } else { + console.log(`[unitedsco-eligibility] Creating new patient: ${incomingFirst} ${incomingLast} with status: ${eligibilityStatus || "UNKNOWN"}`); const createPayload: any = { firstName: incomingFirst, lastName: incomingLast, dateOfBirth: dob, - gender: "", + gender: "Unknown", phone: "", userId, insuranceId, + insuranceProvider: "United SCO", + status: eligibilityStatus || "UNKNOWN", }; let patientData: InsertPatient; try { @@ -118,7 +122,8 @@ async function createOrUpdatePatientByInsuranceId(options: { delete (safePayload as any).dateOfBirth; patientData = insertPatientSchema.parse(safePayload); } - await storage.createPatient(patientData); + const newPatient = await storage.createPatient(patientData); + console.log(`[unitedsco-eligibility] Created new patient: ${newPatient.id} with status: ${eligibilityStatus || "UNKNOWN"}`); } } @@ -171,6 +176,10 @@ async function handleUnitedSCOCompletedJob( lastName = parsedName.lastName || lastName; } + // Determine eligibility status from Selenium result + const eligibilityStatus = seleniumResult.eligibility === "active" ? "ACTIVE" : "INACTIVE"; + console.log(`[unitedsco-eligibility] Eligibility status from United SCO: ${eligibilityStatus}`); + // 3) Create or update patient if (insuranceId) { await createOrUpdatePatientByInsuranceId({ @@ -179,6 +188,7 @@ async function handleUnitedSCOCompletedJob( lastName, dob: insuranceEligibilityData.dateOfBirth, userId: job.userId, + eligibilityStatus, }); } @@ -187,9 +197,61 @@ async function handleUnitedSCOCompletedJob( ? await storage.getPatientByInsuranceId(insuranceId) : null; + // If no patient found by insuranceId, try to find by firstName + lastName + if (!patient?.id && firstName && lastName) { + const patients = await storage.getAllPatients(job.userId); + patient = patients.find( + (p) => + p.firstName?.toLowerCase() === firstName.toLowerCase() && + p.lastName?.toLowerCase() === lastName.toLowerCase() + ) ?? null; + if (patient) { + console.log(`[unitedsco-eligibility] Found patient by name: ${patient.id}`); + } + } + + // If still not found, create new patient + console.log(`[unitedsco-eligibility] Patient creation check: patient=${patient?.id || 'null'}, firstName='${firstName}', lastName='${lastName}'`); + if (!patient && firstName && lastName) { + console.log(`[unitedsco-eligibility] Creating new patient: ${firstName} ${lastName} with status: ${eligibilityStatus}`); + try { + let parsedDob: Date | undefined = undefined; + if (insuranceEligibilityData.dateOfBirth) { + try { + parsedDob = new Date(insuranceEligibilityData.dateOfBirth); + if (isNaN(parsedDob.getTime())) parsedDob = undefined; + } catch { + parsedDob = undefined; + } + } + + const newPatientData: InsertPatient = { + firstName, + lastName, + dateOfBirth: parsedDob || new Date(), // Required field + insuranceId: insuranceId || undefined, + insuranceProvider: "United SCO", + gender: "Unknown", + phone: "", + userId: job.userId, + status: eligibilityStatus, + }; + + const validation = insertPatientSchema.safeParse(newPatientData); + if (validation.success) { + patient = await storage.createPatient(validation.data); + console.log(`[unitedsco-eligibility] Created new patient: ${patient.id} with status: ${eligibilityStatus}`); + } else { + console.log(`[unitedsco-eligibility] Patient validation failed: ${validation.error.message}`); + } + } catch (createErr: any) { + console.log(`[unitedsco-eligibility] Failed to create patient: ${createErr.message}`); + } + } + if (!patient?.id) { outputResult.patientUpdateStatus = - "Patient not found; no update performed"; + "Patient not found and could not be created; no update performed"; return { patientUpdateStatus: outputResult.patientUpdateStatus, pdfUploadStatus: "none", @@ -197,11 +259,20 @@ async function handleUnitedSCOCompletedJob( }; } - // Update patient status - const newStatus = - seleniumResult.eligibility === "active" ? "ACTIVE" : "INACTIVE"; - await storage.updatePatient(patient.id, { status: newStatus }); - outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`; + // Update patient status and name from United SCO eligibility result + const updatePayload: Record = { status: eligibilityStatus }; + + // Also update first/last name if we extracted them and patient has empty names + if (firstName && (!patient.firstName || patient.firstName.trim() === "")) { + updatePayload.firstName = firstName; + } + if (lastName && (!patient.lastName || patient.lastName.trim() === "")) { + updatePayload.lastName = lastName; + } + + await storage.updatePatient(patient.id, updatePayload); + outputResult.patientUpdateStatus = `Patient ${patient.id} updated: status=${eligibilityStatus}, name=${firstName} ${lastName} (United SCO eligibility: ${seleniumResult.eligibility})`; + console.log(`[unitedsco-eligibility] ${outputResult.patientUpdateStatus}`); // Handle PDF or convert screenshot -> pdf if available let pdfBuffer: Buffer | null = null; diff --git a/apps/Frontend/src/components/insurance-status/unitedsco-button-modal.tsx b/apps/Frontend/src/components/insurance-status/unitedsco-button-modal.tsx index 9ad66ec..8b4ef26 100644 --- a/apps/Frontend/src/components/insurance-status/unitedsco-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/unitedsco-button-modal.tsx @@ -118,6 +118,10 @@ export function UnitedSCOEligibilityButton({ }: UnitedSCOEligibilityButtonProps) { const { toast } = useToast(); const dispatch = useAppDispatch(); + + // Flexible validation: require DOB + at least one identifier (memberId OR firstName OR lastName) + const isUnitedSCOFormIncomplete = + !dateOfBirth || (!memberId && !firstName && !lastName); const socketRef = useRef(null); const connectingRef = useRef | null>(null); @@ -370,10 +374,20 @@ export function UnitedSCOEligibilityButton({ }; const startUnitedSCOEligibility = async () => { - if (!memberId || !dateOfBirth) { + // Flexible: require DOB + at least one identifier (memberId OR firstName OR lastName) + if (!dateOfBirth) { toast({ title: "Missing fields", - description: "Member ID and Date of Birth are required.", + description: "Date of Birth is required for United SCO eligibility.", + variant: "destructive", + }); + return; + } + + if (!memberId && !firstName && !lastName) { + toast({ + title: "Missing fields", + description: "Member ID, First Name, or Last Name is required for United SCO eligibility.", variant: "destructive", }); return; @@ -382,11 +396,11 @@ export function UnitedSCOEligibilityButton({ const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : ""; const payload = { - memberId, + memberId: memberId || "", dateOfBirth: formattedDob, - firstName, - lastName, - insuranceSiteKey: "UNITEDSCO", // for backend credential lookup (uses DENTAQUEST) + firstName: firstName || "", + lastName: lastName || "", + insuranceSiteKey: "UNITEDSCO", }; try { @@ -538,7 +552,7 @@ export function UnitedSCOEligibilityButton({ + # This is near "Benefit Summary" and "Service History" buttons. + print("[UnitedSCO step2] Looking for 'Eligibility' button (id='eligibility-link')...") - print(f"[UnitedSCO step2] Final URL: {self.driver.current_url}") + # Record existing downloads BEFORE clicking (to detect new downloads) + existing_downloads = self._get_existing_downloads() + + # Record current window handles BEFORE clicking (to detect new tabs) + original_window = self.driver.current_window_handle + original_windows = set(self.driver.window_handles) + + eligibility_clicked = False + + # Strategy 1 (PRIMARY): Use the known button id="eligibility-link" + try: + # First check if the button exists and is visible + elig_btn = WebDriverWait(self.driver, 15).until( + EC.presence_of_element_located((By.ID, "eligibility-link")) + ) + # Wait for it to become visible (it's hidden when no results) + WebDriverWait(self.driver, 10).until( + EC.visibility_of(elig_btn) + ) + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", elig_btn) + time.sleep(0.5) + elig_btn.click() + eligibility_clicked = True + print("[UnitedSCO step2] Clicked 'Eligibility' button (id='eligibility-link')") + time.sleep(5) + except Exception as e: + print(f"[UnitedSCO step2] Could not click by ID: {e}") + + # Strategy 2: Find the button with exact "Eligibility" text (not "Eligibility Check Results" etc.) + if not eligibility_clicked: + try: + buttons = self.driver.find_elements(By.XPATH, "//button") + for btn in buttons: + try: + text = btn.text.strip() + if re.match(r'^Eligibility\s*$', text, re.IGNORECASE) and btn.is_displayed(): + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", btn) + time.sleep(0.5) + btn.click() + eligibility_clicked = True + print(f"[UnitedSCO step2] Clicked button with text 'Eligibility'") + time.sleep(5) + break + except Exception: + continue + except Exception as e: + print(f"[UnitedSCO step2] Button text search error: {e}") + + # Strategy 3: JavaScript click on #eligibility-link + if not eligibility_clicked: + try: + clicked = self.driver.execute_script(""" + var btn = document.getElementById('eligibility-link'); + if (btn) { btn.scrollIntoView({block: 'center'}); btn.click(); return true; } + // Fallback: find any button/a with exact "Eligibility" text + var all = document.querySelectorAll('button, a'); + for (var i = 0; i < all.length; i++) { + if (/^\\s*Eligibility\\s*$/i.test(all[i].textContent)) { + all[i].scrollIntoView({block: 'center'}); + all[i].click(); + return true; + } + } + return false; + """) + if clicked: + eligibility_clicked = True + print("[UnitedSCO step2] Clicked via JavaScript") + time.sleep(5) + except Exception as e: + print(f"[UnitedSCO step2] JS click error: {e}") + + if not eligibility_clicked: + print("[UnitedSCO step2] WARNING: Could not click Eligibility button") + + # 3) Handle the result of clicking: new tab, download, or same-page content + pdf_path = None + + # Check for new browser tab/window + new_windows = set(self.driver.window_handles) - original_windows + if new_windows: + new_tab = list(new_windows)[0] + print(f"[UnitedSCO step2] New tab opened! Switching to it...") + self.driver.switch_to.window(new_tab) + time.sleep(5) + + # Wait for the new page to load + try: + WebDriverWait(self.driver, 30).until( + lambda d: d.execute_script("return document.readyState") == "complete" + ) + except Exception: + pass + time.sleep(2) + + print(f"[UnitedSCO step2] New tab URL: {self.driver.current_url}") + + # Capture PDF from the new tab + pdf_path = self._capture_pdf(foundMemberId) + + # Close the new tab and switch back to original + self.driver.close() + self.driver.switch_to.window(original_window) + print("[UnitedSCO step2] Closed new tab, switched back to original") + + # Check for downloaded file + if not pdf_path: + downloaded_file = self._wait_for_new_download(existing_downloads, timeout=10) + if downloaded_file: + print(f"[UnitedSCO step2] File downloaded: {downloaded_file}") + pdf_path = downloaded_file + + # Fallback: capture current page as PDF + if not pdf_path: + print("[UnitedSCO step2] No new tab or download detected - capturing current page as PDF") + + # Wait for any dynamic content + try: + WebDriverWait(self.driver, 15).until( + lambda d: d.execute_script("return document.readyState") == "complete" + ) + except Exception: + pass + time.sleep(3) + + print(f"[UnitedSCO step2] Capturing PDF from URL: {self.driver.current_url}") + pdf_path = self._capture_pdf(foundMemberId) - # 3) Generate PDF using Chrome DevTools Protocol (same as other insurances) - print("[UnitedSCO step2] Generating PDF...") + if not pdf_path: + return {"status": "error", "message": "STEP2 FAILED: Could not generate PDF"} + + print(f"[UnitedSCO step2] PDF saved: {pdf_path}") + + # Hide browser window after completion + self._hide_browser() + + print("[UnitedSCO step2] Eligibility capture complete") + + return { + "status": "success", + "eligibility": eligibilityText, + "ss_path": pdf_path, + "pdf_path": pdf_path, + "patientName": patientName, + "memberId": foundMemberId + } + except Exception as e: + print(f"[UnitedSCO step2] Exception: {e}") + return {"status": "error", "message": f"STEP2 FAILED: {str(e)}"} + + def _hide_browser(self): + """Hide the browser window after task completion using multiple strategies.""" + try: + # Strategy 1: Navigate to blank page first (clears sensitive data from view) + try: + self.driver.get("about:blank") + time.sleep(0.5) + except Exception: + pass + + # Strategy 2: Minimize window + try: + self.driver.minimize_window() + print("[UnitedSCO step2] Browser window minimized") + return + except Exception: + pass + + # Strategy 3: Move window off-screen + try: + self.driver.set_window_position(-10000, -10000) + print("[UnitedSCO step2] Browser window moved off-screen") + return + except Exception: + pass + + # Strategy 4: Use xdotool to minimize (Linux) + try: + import subprocess + subprocess.run(["xdotool", "getactivewindow", "windowminimize"], + timeout=3, capture_output=True) + print("[UnitedSCO step2] Browser minimized via xdotool") + except Exception: + pass + + except Exception as e: + print(f"[UnitedSCO step2] Could not hide browser: {e}") + + def _capture_pdf(self, member_id): + """Capture the current page as PDF using Chrome DevTools Protocol.""" + try: pdf_options = { "landscape": False, "displayHeaderFooter": False, @@ -553,31 +1121,17 @@ class AutomationUnitedSCOEligibilityCheck: "scale": 0.9, } - # Use foundMemberId for filename - file_identifier = foundMemberId if foundMemberId else f"{self.firstName}_{self.lastName}" + 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"unitedsco_eligibility_{file_identifier}_{int(time.time())}.pdf") with open(pdf_path, "wb") as f: f.write(pdf_data) - print(f"[UnitedSCO step2] PDF saved: {pdf_path}") - - # Keep browser alive for next patient - print("[UnitedSCO step2] Eligibility capture complete - session preserved") - - return { - "status": "success", - "eligibility": eligibilityText, - "ss_path": pdf_path, - "pdf_path": pdf_path, - "patientName": patientName, - "memberId": foundMemberId # Return the Member ID found on the page - } - + return pdf_path except Exception as e: - print(f"[UnitedSCO step2] Exception: {e}") - return {"status": "error", "message": f"STEP2 FAILED: {str(e)}"} + print(f"[UnitedSCO _capture_pdf] Error: {e}") + return None def main_workflow(self, url): diff --git a/apps/SeleniumService/unitedsco_browser_manager.py b/apps/SeleniumService/unitedsco_browser_manager.py index c61fda8..692e314 100644 --- a/apps/SeleniumService/unitedsco_browser_manager.py +++ b/apps/SeleniumService/unitedsco_browser_manager.py @@ -112,6 +112,26 @@ class UnitedSCOBrowserManager: except Exception as e: print(f"[UnitedSCO BrowserManager] Could not clear IndexedDB: {e}") + # Clear browser cache (prevents corrupted cached responses) + 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, "Service Worker"), + 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"[UnitedSCO BrowserManager] Cleared {os.path.basename(cache_dir)}") + except Exception as e: + print(f"[UnitedSCO 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 @@ -233,11 +253,21 @@ class UnitedSCOBrowserManager: options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") + # Anti-detection options (prevent bot detection) + 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, "download.prompt_for_download": False, - "download.directory_upgrade": True + "download.directory_upgrade": True, + # Disable password save dialog that blocks page interactions + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, } options.add_experimental_option("prefs", prefs) @@ -245,6 +275,12 @@ class UnitedSCOBrowserManager: self._driver = webdriver.Chrome(service=service, options=options) self._driver.maximize_window() + # Remove webdriver property to avoid detection + try: + self._driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") + except Exception: + pass + # Reset the session clear flag (file-based clearing is done on startup) self._needs_session_clear = False