from selenium import webdriver from selenium.common import TimeoutException from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait, Select from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager import time import os import shutil import stat import base64 import tempfile from datetime import datetime class AutomationMassHealthClaimsLogin: def __init__(self, data): self.headless = False self.driver = None self.extracted_data = {} print(f"DEBUG: Received data = {data}") self.upload_files = [] if isinstance(data, dict): self.upload_files = (data.get("pdfs", []) or []) + (data.get("images", []) or []) self.data = data.get("data") if isinstance(data, dict) else None if not self.data and isinstance(data, dict) and isinstance(data.get("claim"), dict): claim = data.get("claim") first_line = None if isinstance(claim.get("serviceLines"), list) and len(claim.get("serviceLines")) > 0: first_line = claim.get("serviceLines")[0] if isinstance(claim.get("serviceLines")[0], dict) else None patient_name = (claim.get("patientName") or "").strip() first_name = "" last_name = "" if patient_name: parts = patient_name.split() first_name = parts[0] if len(parts) > 0 else "" last_name = " ".join(parts[1:]) if len(parts) > 1 else "" self.data = { "massdhpUsername": claim.get("massdhpUsername", ""), "massdhpPassword": claim.get("massdhpPassword", ""), "memberId": claim.get("memberId", ""), "dateOfBirth": claim.get("dateOfBirth", ""), "submissionDate": claim.get("serviceDate", ""), "firstName": claim.get("firstName", "") or first_name, "lastName": claim.get("lastName", "") or last_name, "procedureCode": (first_line or {}).get("procedureCode", "") if first_line else "", "toothNumber": (first_line or {}).get("toothNumber", "") if first_line else "", "toothSurface": (first_line or {}).get("toothSurface", "") if first_line else "", } if not self.data: self.data = {} print(f"DEBUG: Extracted data = {self.data}") # Extract service lines data for multiple rows self.serviceLines = [] if isinstance(data, dict) and isinstance(data.get("claim"), dict): claim = data.get("claim") service_lines = claim.get("serviceLines", []) if isinstance(service_lines, list) and len(service_lines) > 0: self.serviceLines = service_lines print(f"DEBUG: Found {len(self.serviceLines)} service lines") for i, line in enumerate(self.serviceLines): print(f"DEBUG: Service line {i+1}: {line}") # Flatten values for convenience self.massdhp_username = self.data.get("massdhpUsername", "") self.massdhp_password = self.data.get("massdhpPassword", "") self.dateOfBirth = self.data.get("dateOfBirth", "") self.originalDateOfBirth = self.data.get("dateOfBirth", "") # Keep original format self.memberId = self.data.get("memberId", "") self.submissionDate = self.data.get("submissionDate", "") self.firstName = self.data.get("firstName", "") self.lastName = self.data.get("lastName", "") self.procedureCode = self.data.get("procedureCode", "") self.toothNumber = self.data.get("toothNumber", "") self.toothSurface = self.data.get("toothSurface", "") print(f"DEBUG: submissionDate = '{self.submissionDate}'") print(f"DEBUG: firstName = '{self.firstName}', lastName = '{self.lastName}'") print(f"DEBUG: procedureCode = '{self.procedureCode}'") print(f"DEBUG: toothNumber = '{self.toothNumber}'") print(f"DEBUG: toothSurface = '{self.toothSurface}'") # Convert dateOfBirth to MMDDYYYY format (if needed later) if self.dateOfBirth: dob_raw = str(self.dateOfBirth).strip() if "T" in dob_raw: dob_raw = dob_raw.split("T")[0] parsed = None for fmt in ("%Y-%m-%d", "%m-%d-%Y", "%m/%d/%Y"): try: parsed = datetime.strptime(dob_raw, fmt) break except Exception: continue if parsed: self.dateOfBirth = parsed.strftime("%m%d%Y") self.download_dir = os.path.abspath("downloads") os.makedirs(self.download_dir, exist_ok=True) def config_driver(self): options = webdriver.ChromeOptions() if self.headless: options.add_argument("--headless") # Add PDF download preferences (if needed later) prefs = { "download.default_directory": self.download_dir, "plugins.always_open_pdf_externally": False, "download.prompt_for_download": False, "download.directory_upgrade": True } options.add_experimental_option("prefs", prefs) s = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=s, options=options) self.driver = driver def login(self): wait = WebDriverWait(self.driver, 30) try: # Step 1: Click the SIGN IN button on the initial page signin_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "a.btn.btn-block.btn-primary[href='https://connectsso.masshealth-dental.org/mhprovider/index.html']"))) signin_button.click() # Wait for the new page to load time.sleep(2) # Step 2: Enter email on the new login page email_field = wait.until(EC.presence_of_element_located((By.ID, "User"))) email_field.clear() email_field.send_keys(self.massdhp_username) # Step 3: Enter password password_field = wait.until(EC.presence_of_element_located((By.ID, "Password"))) password_field.clear() password_field.send_keys(self.massdhp_password) # Step 4: Click login button login_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input[type='submit'][name='submit'][value='Login']"))) login_button.click() return "Success" except Exception as e: print(f"Error while logging in: {e}") return "ERROR:LOGIN FAILED" def navigate_to_submit_claims(self): wait = WebDriverWait(self.driver, 30) try: # Step 1: Click Claims/Prior Authorizations dropdown (use presence like eligibility worker) claims_dropdown = wait.until( EC.presence_of_element_located( (By.XPATH, "//strong[contains(@class,'navtitle') and contains(text(),'Claims/Prior Authorizations')]") ) ) # Scroll + JS click = fixes most dropdown issues self.driver.execute_script("arguments[0].scrollIntoView(true);", claims_dropdown) self.driver.execute_script("arguments[0].click();", claims_dropdown) time.sleep(1) # Step 2: Click Submit Claims & Prior Authorizations (wait for dropdown to be visible) submit_claims_link = wait.until( EC.presence_of_element_located( (By.XPATH, "//a[contains(@class,'navitem subnav') and contains(text(),'Submit Claims & Prior Authorizations')]") ) ) time.sleep(1) # ensure dropdown menu is fully rendered self.driver.execute_script("arguments[0].scrollIntoView(true);", submit_claims_link) self.driver.execute_script("arguments[0].click();", submit_claims_link) time.sleep(1) return "Success" except Exception as e: print(f"Error navigating to Submit Claims: {e}") return "ERROR:NAVIGATION_FAILED" def select_dental_claim(self): wait = WebDriverWait(self.driver, 30) try: # Step 1: Wait for the submission type dropdown to be present submission_dropdown = wait.until( EC.presence_of_element_located( (By.XPATH, "//select[contains(@ng-model,'vm.newClaim.type')]") ) ) time.sleep(1) # Step 2: Use Select class to choose Dental Claim select = Select(submission_dropdown) select.select_by_visible_text("Dental Claim") time.sleep(1) return "Success" except Exception as e: print(f"Error selecting Dental Claim: {e}") return "ERROR:SELECTION FAILED" def fill_date_of_service(self): wait = WebDriverWait(self.driver, 30) try: # For now, just use today's date to avoid parsing issues formatted_date = datetime.now().strftime("%m/%d/%Y") print(f"DEBUG: Using today's date = '{formatted_date}'") # Step 1: Wait for the Date of Service input to be visible date_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@name='dateOfService' and @ng-model='date' and @placeholder='mm/dd/yyyy']") ) ) time.sleep(1) # Step 2: Use JavaScript to set the value directly to avoid date picker interference self.driver.execute_script(f"arguments[0].value = '{formatted_date}'; arguments[0].dispatchEvent(new Event('input', {{ bubbles: true }})); arguments[0].dispatchEvent(new Event('change', {{ bubbles: true }}));", date_input) time.sleep(1) # Press Tab to ensure the date is set date_input.send_keys(Keys.TAB) time.sleep(1) return "Success" except Exception as e: print(f"Error filling Date of Service: {e}") return "ERROR:DATE_FILL_FAILED" def fill_member_eligibility(self): wait = WebDriverWait(self.driver, 30) try: print(f"DEBUG: Filling member eligibility with DOB: '{self.originalDateOfBirth}'") # Step 1: Fill Date of Birth (convert from original format to MM/DD/YYYY) formatted_dob = "" if self.originalDateOfBirth: try: dob_raw = str(self.originalDateOfBirth).strip() if "T" in dob_raw: dob_raw = dob_raw.split("T")[0] date_obj = None for fmt in ("%Y-%m-%d", "%m-%d-%Y", "%m/%d/%Y"): try: date_obj = datetime.strptime(dob_raw, fmt) break except Exception: continue if date_obj: formatted_dob = date_obj.strftime("%m/%d/%Y") print(f"DEBUG: Formatted DOB = '{formatted_dob}'") except Exception as e: print(f"DEBUG: DOB parsing error: {e}") formatted_dob = "" # Find and fill DOB input (using JavaScript to avoid date picker issues) dob_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@name='dateInput' and @placeholder='mm/dd/yyyy' and @ng-model='date']") ) ) time.sleep(1) # Use JavaScript to set the value directly to avoid date picker interference self.driver.execute_script(f"arguments[0].value = '{formatted_dob}'; arguments[0].dispatchEvent(new Event('input', {{ bubbles: true }})); arguments[0].dispatchEvent(new Event('change', {{ bubbles: true }}));", dob_input) time.sleep(1) # Press Tab to move to next field and ensure the date is set dob_input.send_keys(Keys.TAB) time.sleep(1) # Step 2: Fill Member Number if self.memberId: print(f"DEBUG: Filling member ID: '{self.memberId}'") member_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@placeholder='Member Number' and @ng-model='vm.number']") ) ) time.sleep(1) member_input.click() time.sleep(1) member_input.clear() time.sleep(1) member_input.send_keys(self.memberId) time.sleep(1) member_input.send_keys(Keys.TAB) time.sleep(1) # Step 3: Fill First Name and Last Name # if self.firstName: # print(f"DEBUG: Filling first name: '{self.firstName}'") # first_name_input = wait.until( # EC.visibility_of_element_located( # (By.XPATH, "//input[@placeholder='First Name' and @ng-model='vm.firstName']") # ) # ) # time.sleep(1) # first_name_input.click() # time.sleep(1) # first_name_input.clear() # time.sleep(1) # first_name_input.send_keys(self.firstName) # time.sleep(1) # first_name_input.send_keys(Keys.TAB) # time.sleep(1) # if self.lastName: # print(f"DEBUG: Filling last name: '{self.lastName}'") # last_name_input = wait.until( # EC.visibility_of_element_located( # (By.XPATH, "//input[@placeholder='Last Name' and @ng-model='vm.lastName']") # ) # ) # time.sleep(1) # last_name_input.click() # time.sleep(1) # last_name_input.clear() # time.sleep(1) # last_name_input.send_keys(self.lastName) # time.sleep(1) # Step 4: Click SEARCH button print("DEBUG: Clicking SEARCH button") search_button = wait.until( EC.element_to_be_clickable( (By.XPATH, "//button[@ng-click='vm.searchPrep()' and contains(text(),'SEARCH')]") ) ) search_button.click() time.sleep(2) # Wait for search to complete return "Success" except Exception as e: print(f"Error filling Member Eligibility: {e}") return "ERROR:MEMBER_ELIGIBILITY_FAILED" def fill_procedure_code(self): wait = WebDriverWait(self.driver, 30) try: print(f"DEBUG: Filling procedure code: '{self.procedureCode}'") if not self.procedureCode: print("DEBUG: No procedure code provided, skipping") return "Success" # Step 1: Find the procedure code input field procedure_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@ng-model='serviceLine.procedure' and @placeholder='Code']") ) ) time.sleep(1) # Step 2: Type the procedure code to trigger typeahead procedure_input.clear() procedure_input.send_keys(self.procedureCode) time.sleep(3) # Wait for typeahead to load # Step 3: Look for the typeahead dropdown and select the first option # The dropdown appears as ul with class 'dropdown-menu' try: # Wait for the dropdown to be visible dropdown = wait.until( EC.visibility_of_element_located( (By.XPATH, "//ul[contains(@class,'dropdown-menu') and not(contains(@class,'ng-hide'))]") ) ) # Look for the first option in the dropdown first_option = dropdown.find_element(By.XPATH, ".//li") if first_option: print("DEBUG: Found first option in procedure dropdown, clicking...") first_option.click() time.sleep(2) return "Success" except: # If no dropdown appears, just press Tab to move to next field print("DEBUG: No procedure dropdown found, pressing Tab") procedure_input.send_keys(Keys.TAB) time.sleep(1) return "Success" return "Success" except Exception as e: print(f"Error filling procedure code: {e}") return "ERROR:PROCEDURE_CODE_FAILED" def fill_tooth_number(self): wait = WebDriverWait(self.driver, 30) try: # Use hardcoded value if toothNumber is not provided tooth_value = self.toothNumber if self.toothNumber else "14" # Hardcoded default value print(f"DEBUG: Filling tooth number: '{tooth_value}' (original: '{self.toothNumber}')") # Step 1: Find the tooth input field tooth_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@ng-model='serviceLine.toothCode' and @placeholder='Tooth']") ) ) time.sleep(1) # Step 2: Type the tooth number tooth_input.clear() tooth_input.send_keys(tooth_value.upper()) # Convert to uppercase as per onkeyup time.sleep(2) # Press Tab to move to next field tooth_input.send_keys(Keys.TAB) time.sleep(1) return "Success" except Exception as e: print(f"Error filling tooth number: {e}") return "ERROR:TOOTH_NUMBER_FAILED" def fill_tooth_surface(self): wait = WebDriverWait(self.driver, 30) try: # Use hardcoded value if toothSurface is not provided surface_value = self.toothSurface if self.toothSurface else "B" # Hardcoded default value print(f"DEBUG: Filling tooth surface: '{surface_value}' (original: '{self.toothSurface}')") # Step 1: Find the tooth surface input field surface_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@ng-model='serviceLine.surface' and @placeholder='Surface']") ) ) time.sleep(1) # Step 2: Type the tooth surface (convert to uppercase) surface_input.clear() surface_input.send_keys(surface_value.upper()) time.sleep(2) # Press Tab to move to next field surface_input.send_keys(Keys.TAB) time.sleep(1) return "Success" except Exception as e: print(f"Error filling tooth surface: {e}") return "ERROR:TOOTH_SURFACE_FAILED" def fill_quadrant(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Selecting quadrant 'Upper Right'") # Step 1: Find the quadrant dropdown quadrant_dropdown = wait.until( EC.visibility_of_element_located( (By.XPATH, "//select[@ng-model='serviceLine.quadrant']") ) ) time.sleep(1) # Step 2: Use Select class to choose Upper Right select = Select(quadrant_dropdown) select.select_by_visible_text("Upper Right") time.sleep(2) return "Success" except Exception as e: print(f"Error selecting quadrant: {e}") return "ERROR:QUADRANT_FAILED" def fill_arch(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Selecting arch 'Entire Oral Cavity'") # Step 1: Find the arch dropdown arch_dropdown = wait.until( EC.visibility_of_element_located( (By.XPATH, "//select[@ng-model='serviceLine.arch']") ) ) time.sleep(1) # Step 2: Use Select class to choose Entire Oral Cavity select = Select(arch_dropdown) select.select_by_visible_text("Entire Oral Cavity") time.sleep(2) return "Success" except Exception as e: print(f"Error selecting arch: {e}") return "ERROR:ARCH_FAILED" def fill_authorization_number(self): wait = WebDriverWait(self.driver, 30) try: # Use hardcoded value for authorization number auth_number = "AUTH123456" # Hardcoded default value print(f"DEBUG: Filling authorization number: '{auth_number}'") # Step 1: Find the authorization number input field auth_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@ng-model='serviceLine.authorizationNumber' and @placeholder='No.']") ) ) time.sleep(1) # Step 2: Type the authorization number auth_input.clear() auth_input.send_keys(auth_number) time.sleep(2) # Press Tab to move to next field auth_input.send_keys(Keys.TAB) time.sleep(1) return "Success" except Exception as e: print(f"Error filling authorization number: {e}") return "ERROR:AUTH_NUMBER_FAILED" def select_place_of_service_office(self): wait = WebDriverWait(self.driver, 30) try: # Step 1: Wait for the place of service dropdown to be present pos_dropdown = wait.until( EC.presence_of_element_located( (By.XPATH, "//select[contains(@ng-model,'vm.newClaim.placeOfTreatmentCode')]") ) ) time.sleep(1) # Step 2: Use Select class to choose Office select = Select(pos_dropdown) select.select_by_visible_text("Office") time.sleep(2) return "Success" except Exception as e: print(f"Error selecting Office place of service: {e}") return "ERROR:SELECTION FAILED" def select_office_summit_framingham(self): wait = WebDriverWait(self.driver, 30) try: # Step 1: Wait for the office dropdown to be visible and enabled office_dropdown = wait.until( EC.visibility_of_element_located( (By.XPATH, "//select[@id='selectOffice']") ) ) time.sleep(1) # Step 2: Use Select class to choose Summit Dental Care - Framingham - select = Select(office_dropdown) # Fallback: try by visible text, then by value try: select.select_by_visible_text("Summit Dental Care - Framingham - ") except: select.select_by_value("string:0010a00001XPhhtAAD") time.sleep(2) return "Success" except Exception as e: print(f"Error selecting Summit Dental Care - Framingham office: {e}") return "ERROR:SELECTION FAILED" def select_dentist_gao_kai(self): wait = WebDriverWait(self.driver, 30) try: # Step 1: Wait for the dentist input field to be visible and enabled dentist_input = wait.until( EC.visibility_of_element_located( (By.XPATH, "//input[@id='inputProvider' and @placeholder='Enter a dentist']") ) ) time.sleep(1) # Step 2: Type "Gao" to trigger typeahead dentist_input.clear() dentist_input.send_keys("Gao") time.sleep(3) # Wait for typeahead to load # Step 3: Wait for the typeahead dropdown to appear and find the specific option # Look for the element with ng-bind-html containing the text option_xpath = "//a[@ng-bind-html and contains(., 'Gao, Kai - 1457649006')]" try: # Wait for the option to be clickable option = wait.until( EC.element_to_be_clickable((By.XPATH, option_xpath)) ) print("DEBUG: Found dentist option, clicking...") # Scroll into view and click self.driver.execute_script("arguments[0].scrollIntoView(true);", option) time.sleep(1) option.click() time.sleep(2) return "Success" except: # Fallback: Try alternative selectors fallback_selectors = [ "//ul[contains(@class,'dropdown-menu')]//a[contains(., 'Gao, Kai - 1457649006')]", "//ul[contains(@class,'dropdown-menu')]//li[contains(., 'Gao, Kai - 1457649006')]", "//div[contains(@class,'typeahead')]//a[contains(., 'Gao, Kai - 1457649006')]", ] for selector in fallback_selectors: try: option = wait.until( EC.element_to_be_clickable((By.XPATH, selector)) ) print(f"DEBUG: Found option with fallback selector: {selector}") self.driver.execute_script("arguments[0].scrollIntoView(true);", option) time.sleep(1) option.click() time.sleep(2) return "Success" except: continue # If all selectors fail, use Arrow Down + Enter print("DEBUG: Using Arrow Down + Enter fallback") dentist_input.send_keys(Keys.ARROW_DOWN) time.sleep(1) dentist_input.send_keys(Keys.ENTER) time.sleep(2) return "Success" except Exception as e: print(f"Error selecting dentist Gao, Kai: {e}") return "ERROR:SELECTION_FAILED" def click_plus_button(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Clicking plus button to add new service line") # Step 1: Wait a moment to ensure the previous service line is fully processed time.sleep(3) # Step 2: Try multiple approaches to find the plus button plus_button = None # First try to find any button with + text try: print("DEBUG: Looking for any button with + text") all_plus_buttons = self.driver.find_elements(By.XPATH, "//button[contains(text(), '+')]") print(f"DEBUG: Found {len(all_plus_buttons)} buttons with + text") for i, btn in enumerate(all_plus_buttons): try: ng_click = btn.get_attribute("ng-click") class_attr = btn.get_attribute("class") print(f"DEBUG: Button {i+1} - ng-click: {ng_click}, class: {class_attr}") if ng_click and "serviceLineAdd" in ng_click: plus_button = btn print(f"DEBUG: Found correct plus button at index {i+1}") break except: continue except Exception as e: print(f"DEBUG: Error searching for + buttons: {e}") # If still not found, try specific selectors if not plus_button: selectors = [ "//button[@ng-click='vm.serviceLineAdd()' and contains(@class,'btn') and text()='+']", "//button[contains(@class,'btn') and @ng-click='vm.serviceLineAdd()']", "//td/button[@ng-click='vm.serviceLineAdd()']", "//button[text()='+']", "//button[contains(@class,'btn-default') and text()='+']", "//button[contains(@class,'btn-sm') and text()='+']", "//*[contains(@ng-click,'serviceLineAdd') and contains(text(),'+')]" ] for selector in selectors: try: print(f"DEBUG: Trying selector: {selector}") plus_button = wait.until( EC.element_to_be_clickable((By.XPATH, selector)) ) print(f"DEBUG: Found plus button with selector: {selector}") break except: print(f"DEBUG: Selector failed: {selector}") continue if not plus_button: # Last resort: try to find by JavaScript try: print("DEBUG: Trying JavaScript approach") plus_button = self.driver.execute_script(""" var buttons = document.getElementsByTagName('button'); for (var i = 0; i < buttons.length; i++) { if (buttons[i].textContent.trim() === '+' && buttons[i].getAttribute('ng-click') && buttons[i].getAttribute('ng-click').includes('serviceLineAdd')) { return buttons[i]; } } return null; """) if plus_button: print("DEBUG: Found plus button using JavaScript") else: raise Exception("JavaScript search also failed") except Exception as e: print(f"DEBUG: JavaScript approach failed: {e}") raise Exception("Could not find plus button with any method") time.sleep(1) # Step 3: Scroll into view self.driver.execute_script("arguments[0].scrollIntoView(true);", plus_button) time.sleep(1) # Step 4: Check if button is enabled is_disabled = plus_button.get_attribute("disabled") ng_disabled = plus_button.get_attribute("ng-disabled") print(f"DEBUG: Button disabled attribute: {is_disabled}") print(f"DEBUG: Button ng-disabled attribute: {ng_disabled}") # Check if button is actually enabled and clickable try: if not plus_button.is_enabled(): print("DEBUG: Button is not enabled, waiting...") time.sleep(2) if not plus_button.is_enabled(): raise Exception("Plus button is still not enabled after waiting") except Exception as e: print(f"DEBUG: Error checking button enabled state: {e}") # Step 5: Click the button using multiple methods try: print("DEBUG: Attempting direct click") plus_button.click() except: try: print("DEBUG: Direct click failed, trying JavaScript click") self.driver.execute_script("arguments[0].click();", plus_button) except: print("DEBUG: JavaScript click failed, trying ActionChains") from selenium.webdriver.common.action_chains import ActionChains actions = ActionChains(self.driver) actions.move_to_element(plus_button).click().perform() time.sleep(2) # Wait for new row to be created print("DEBUG: Successfully clicked plus button") return "Success" except Exception as e: print(f"Error clicking plus button: {e}") # Add debug information about current page state try: page_source_length = len(self.driver.page_source) current_url = self.driver.current_url print(f"DEBUG: Current URL: {current_url}") print(f"DEBUG: Page source length: {page_source_length}") # Try to find all buttons on page for debugging all_buttons = self.driver.find_elements(By.TAG_NAME, "button") print(f"DEBUG: Total buttons found: {len(all_buttons)}") for i, btn in enumerate(all_buttons[:10]): # Show first 10 try: text = btn.text.strip() ng_click = btn.get_attribute("ng-click") print(f"DEBUG: Button {i+1} - Text: '{text}', ng-click: {ng_click}") except: continue except: pass return "ERROR:PLUS_BUTTON_FAILED" def add_file_attachment(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Adding file attachment") if not self.upload_files: print("DEBUG: No uploaded files available for attachment") return "ERROR:NO_FILES" add_file_button_locator = ( By.XPATH, "//button[@ng-click='vm.addFile(vm.fileType)' and contains(., 'Add File')]" ) with tempfile.TemporaryDirectory() as tmp_dir: for file_obj in self.upload_files: if not isinstance(file_obj, dict): continue base64_data = file_obj.get("bufferBase64", "") file_name = file_obj.get("originalname", "file.pdf") if not base64_data: print("DEBUG: Skipping file because bufferBase64 is missing") continue safe_file_name = os.path.basename(file_name) if not any(safe_file_name.lower().endswith(ext) for ext in [".pdf", ".jpg", ".jpeg", ".png", ".webp"]): safe_file_name += ".bin" tmp_path = os.path.join(tmp_dir, safe_file_name) with open(tmp_path, "wb") as f: f.write(base64.b64decode(base64_data)) print(f"DEBUG: Uploading file: {tmp_path}") print(f"DEBUG: File base64 length: {len(base64_data)}") # Instead of using send_keys, directly inject the file into Angular's displayedArray print("DEBUG: Directly injecting file into Angular vm.displayedArray") injection_result = self.driver.execute_script(""" try { var fileInput = document.getElementById('fileAttachment'); if (!fileInput) return 'FILE_INPUT_NOT_FOUND'; var scope = angular.element(fileInput).scope(); if (!scope || !scope.vm) return 'SCOPE_NOT_FOUND'; // Create a file attachment object matching the Angular template structure // Template expects: file.type, file.name, file.updatedDate var fileAttachment = { name: arguments[0], // file.name type: scope.vm.fileType || 'RR', // file.type data: arguments[1], // base64 data updatedDate: new Date() // file.updatedDate (Date object for Angular date filter) }; // Initialize displayedArray if it doesn't exist if (!scope.vm.displayedArray) { scope.vm.displayedArray = []; } // Add the file to the array scope.vm.displayedArray.push(fileAttachment); // Trigger Angular digest scope.$apply(); return 'FILE_INJECTED:' + scope.vm.displayedArray.length; } catch (err) { return 'ERROR:' + err.message; } """, safe_file_name, base64_data) print(f"DEBUG: Injection result = {injection_result}") if not injection_result.startswith('FILE_INJECTED'): print(f"DEBUG: Injection failed: {injection_result}") return "ERROR:UPLOAD_FAILED" print("DEBUG: Waiting for attachment row to appear in webpage table") # Wait for the table to be visible with at least one row wait.until( EC.presence_of_element_located( (By.XPATH, "//table[contains(@class, 'table-striped') and @ng-if='vm.displayedArray && vm.displayedArray.length']//tbody/tr") ) ) # Verify what data is actually in the table row table_data = self.driver.execute_script(""" try { var table = document.querySelector("table[ng-if*='displayedArray']"); if (!table) return 'TABLE_NOT_FOUND'; var rows = table.querySelectorAll('tbody tr'); if (rows.length === 0) return 'NO_ROWS'; var firstRow = rows[0]; var cells = firstRow.querySelectorAll('td'); var cellData = []; for (var i = 0; i < cells.length; i++) { cellData.push(cells[i].textContent.trim()); } return {rowCount: rows.length, firstRowData: cellData}; } catch (err) { return 'ERROR:' + err.message; } """) print(f"DEBUG: Table data after injection = {table_data}") print("DEBUG: Attachment row appeared in webpage table") time.sleep(1) return "Success" except Exception as e: try: print(f"DEBUG: Current URL during upload failure = {self.driver.current_url}") file_inputs = self.driver.find_elements(By.XPATH, "//input[@type='file']") print(f"DEBUG: Total file inputs found during failure = {len(file_inputs)}") for index, item in enumerate(file_inputs[:5]): try: print(f"DEBUG: File input {index + 1} id={item.get_attribute('id')} style={item.get_attribute('style')} value={item.get_attribute('value')}") except Exception: continue except Exception: pass print(f"Error adding file attachment: {e}") return "ERROR:UPLOAD_FAILED" def select_radiology_reports(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Selecting Radiology Reports document type") # Step 1: Find the document type dropdown doc_type_dropdown = wait.until( EC.presence_of_element_located( (By.XPATH, "//select[@ng-model='vm.fileType' and @ng-options=\"item.value as item.name for item in vm.documentTypes\"]") ) ) time.sleep(1) # Step 2: Use Select class to choose Radiology Reports select = Select(doc_type_dropdown) select.select_by_visible_text("Radiology Reports") self.driver.execute_script( "arguments[0].dispatchEvent(new Event('input', { bubbles: true }));" "arguments[0].dispatchEvent(new Event('change', { bubbles: true }));", doc_type_dropdown ) wait.until(lambda d: (doc_type_dropdown.get_attribute("value") or "").strip() != "") time.sleep(2) print("DEBUG: Successfully selected Radiology Reports") return "Success" except Exception as e: print(f"Error selecting Radiology Reports: {e}") return "ERROR:RADIOLOGY_REPORTS_FAILED" def click_submit_button(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Clicking SUBMIT button") # Find and click the SUBMIT button submit_button = wait.until( EC.element_to_be_clickable( (By.XPATH, "//button[@ng-click=\"vm.submitForm('File Custom Pop Up Error')\" and contains(., 'SUBMIT')]") ) ) # Check if button is disabled if submit_button.get_attribute("disabled"): print("DEBUG: SUBMIT button is disabled") return "ERROR:SUBMIT_BUTTON_DISABLED" print(f"DEBUG: SUBMIT button found. disabled={submit_button.get_attribute('disabled')}") submit_button.click() print("DEBUG: SUBMIT button clicked successfully") # Wait for navigation to confirmation page time.sleep(6) print(f"DEBUG: Current URL after submit = {self.driver.current_url}") return "Success" except Exception as e: print(f"Error clicking SUBMIT button: {e}") return "ERROR:SUBMIT_FAILED" def extract_claim_number(self): """Extract claim number from MassHealth confirmation page""" wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Extracting claim number from confirmation page") # Wait for confirmation page to load wait.until(lambda d: d.execute_script("return document.readyState") == "complete") time.sleep(2) # Try to get page source for debugging page_text = self.driver.find_element(By.TAG_NAME, "body").text print(f"DEBUG: Page text snippet: {page_text[:500]}") # Try multiple selectors to find claim number # Based on the screenshot: "Your claim/pre-authorization has been submitted and assigned the number 202609200006700" claim_number_selectors = [ # Look for the specific text pattern "//*[contains(text(), 'assigned the number')]/text()", "//*[contains(text(), 'assigned the number')]", "//p[contains(text(), 'assigned the number')]", "//div[contains(text(), 'assigned the number')]", "//span[contains(text(), 'assigned the number')]", # Look for "Submission Success" section "//h1[contains(text(), 'Submission Success')]/following-sibling::*", "//h2[contains(text(), 'Submission Success')]/following-sibling::*", # Common patterns for claim numbers on MassHealth "//*[contains(text(), 'Claim Number') or contains(text(), 'Claim #')]/following-sibling::*", "//td[contains(text(), 'Claim Number')]/following-sibling::td", "//label[contains(text(), 'Claim Number')]/following-sibling::div", "//div[contains(@class, 'confirmation')]//div[contains(text(), 'Number')]", "//div[contains(@class, 'success')]//div[contains(text(), 'Number')]", ] for selector in claim_number_selectors: try: elements = self.driver.find_elements(By.XPATH, selector) for element in elements: text = element.text.strip() print(f"DEBUG: Checking element with selector {selector}: {text[:100]}") # Look for 15-digit claim number pattern (MassHealth format: YYYYMMDD + 7 digits) match = re.search(r'(\d{15})', text) if match: claim_number = match.group(1) print(f"DEBUG: Found 15-digit claim number: {claim_number}") return claim_number # Also try 9-14 digit patterns match = re.search(r'(\d{9,14})', text) if match: claim_number = match.group(1) print(f"DEBUG: Found claim number (9-14 digits): {claim_number}") return claim_number except Exception as e: print(f"DEBUG: Selector {selector} failed: {e}") continue # If specific selectors fail, try JavaScript to find claim number pattern # MassHealth claim numbers are 15 digits: YYYYMMDD + 7 digit sequence claim_number = self.driver.execute_script(r""" // Look for text that matches claim number patterns var allElements = document.querySelectorAll('body, p, div, span, td, label, h1, h2, h3'); for (var i = 0; i < allElements.length; i++) { var text = allElements[i].textContent || ''; // MassHealth format: 15 digits (YYYYMMDD + 7 digits) var match15 = text.match(/(\d{15})/); if (match15) { return match15[1]; } // Also check for 9-14 digit patterns var match9to14 = text.match(/(\d{9,14})/); if (match9to14) { return match9to14[1]; } } return null; """) if claim_number: print(f"DEBUG: Found claim number via JavaScript: {claim_number}") return claim_number print("DEBUG: Could not extract claim number from page") return None except Exception as e: print(f"Error extracting claim number: {e}") import traceback traceback.print_exc() return None def save_confirmation_pdf(self): wait = WebDriverWait(self.driver, 30) try: print("DEBUG: Saving confirmation page as PDF") # Wait for page to fully load wait.until(lambda d: d.execute_script("return document.readyState") == "complete") wait.until(EC.presence_of_element_located((By.TAG_NAME, "body"))) time.sleep(2) print(f"DEBUG: Capturing PDF from: {self.driver.current_url}") # Extract claim number from confirmation page claim_number = self.extract_claim_number() # Generate safe filename using member ID, claim number and timestamp safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.") timestamp = time.strftime("%Y%m%d_%H%M%S") if claim_number: safe_claim = "".join(c for c in str(claim_number) if c.isalnum() or c in "-_.")[:20] pdf_filename = f"claim_confirmation_{safe_member}_{safe_claim}_{timestamp}.pdf" else: pdf_filename = f"claim_confirmation_{safe_member}_{timestamp}.pdf" # Use Chrome DevTools to generate PDF pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", { "printBackground": True }) pdf_bytes = base64.b64decode(pdf_data['data']) pdf_path = os.path.join(self.download_dir, pdf_filename) with open(pdf_path, "wb") as f: f.write(pdf_bytes) print(f"DEBUG: PDF saved at: {pdf_path}") return { "status": "success", "pdf_path": pdf_path, "file_type": "pdf", "claimNumber": claim_number, "message": "Confirmation PDF captured successfully" } except Exception as e: print(f"PDF capture failed: {e}") # Fallback to screenshot try: safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.") timestamp = time.strftime("%Y%m%d_%H%M%S") screenshot_path = os.path.join(self.download_dir, f"claim_confirmation_{safe_member}_{timestamp}.png") self.driver.save_screenshot(screenshot_path) print(f"DEBUG: Screenshot saved at: {screenshot_path}") return { "status": "success", "pdf_path": screenshot_path, "file_type": "screenshot", "claimNumber": None, "message": "Screenshot captured (PDF failed)" } except Exception as ss_error: return { "status": "error", "message": f"PDF + Screenshot failed: {ss_error}" } def fill_service_line(self, row_number=1, procedure_code="", tooth_number="", tooth_surface="", quadrant="Upper Right", arch="Entire Oral Cavity", auth_number="AUTH123456", billed_amount=""): wait = WebDriverWait(self.driver, 30) try: print(f"DEBUG: Filling service line {row_number} - Procedure: {procedure_code}, Tooth: {tooth_number}, Surface: {tooth_surface}, Amount: {billed_amount}") # Fill Procedure Code - find the specific row's procedure input if procedure_code: print(f"DEBUG: Filling procedure code: '{procedure_code}' for row {row_number}") # Find all procedure inputs and select the one for the current row procedure_inputs = wait.until( EC.presence_of_all_elements_located( (By.XPATH, "//input[@ng-model='serviceLine.procedure' and @placeholder='Code']") ) ) if len(procedure_inputs) < row_number: raise Exception(f"Only {len(procedure_inputs)} procedure inputs found, but need row {row_number}") procedure_input = procedure_inputs[row_number - 1] # Convert to 0-based index time.sleep(1) procedure_input.clear() procedure_input.send_keys(procedure_code) time.sleep(3) # Wait for typeahead # Try to select from dropdown if available try: dropdown = wait.until( EC.visibility_of_element_located( (By.XPATH, "//ul[contains(@class,'dropdown-menu') and not(contains(@class,'ng-hide'))]") ) ) first_option = dropdown.find_element(By.XPATH, ".//li") if first_option: first_option.click() time.sleep(2) except: procedure_input.send_keys(Keys.TAB) time.sleep(1) # Fill Tooth Number - only if provided in claim form if tooth_number: print(f"DEBUG: Filling tooth number: '{tooth_number}' for row {row_number}") tooth_inputs = wait.until( EC.presence_of_all_elements_located( (By.XPATH, "//input[@ng-model='serviceLine.toothCode' and @placeholder='Tooth']") ) ) if len(tooth_inputs) < row_number: raise Exception(f"Only {len(tooth_inputs)} tooth inputs found, but need row {row_number}") tooth_input = tooth_inputs[row_number - 1] time.sleep(1) tooth_input.clear() tooth_input.send_keys(tooth_number.upper()) time.sleep(2) tooth_input.send_keys(Keys.TAB) time.sleep(1) else: print(f"DEBUG: No tooth number provided for row {row_number}, skipping") # Fill Tooth Surface - only if provided in claim form if tooth_surface: print(f"DEBUG: Filling tooth surface: '{tooth_surface}' for row {row_number}") surface_inputs = wait.until( EC.presence_of_all_elements_located( (By.XPATH, "//input[@ng-model='serviceLine.surface' and @placeholder='Surface']") ) ) if len(surface_inputs) < row_number: raise Exception(f"Only {len(surface_inputs)} surface inputs found, but need row {row_number}") surface_input = surface_inputs[row_number - 1] time.sleep(1) surface_input.clear() surface_input.send_keys(tooth_surface.upper()) time.sleep(2) surface_input.send_keys(Keys.TAB) time.sleep(1) else: print(f"DEBUG: No tooth surface provided for row {row_number}, skipping") # COMMENTED OUT: Quadrant - not needed per user requirement # Will be handled later for specific procedure codes that require it # quadrant_dropdowns = wait.until( # EC.presence_of_all_elements_located( # (By.XPATH, "//select[@ng-model='serviceLine.quadrant']") # ) # ) # if len(quadrant_dropdowns) < row_number: # raise Exception(f"Only {len(quadrant_dropdowns)} quadrant dropdowns found, but need row {row_number}") # quadrant_dropdown = quadrant_dropdowns[row_number - 1] # time.sleep(1) # select_quadrant = Select(quadrant_dropdown) # select_quadrant.select_by_visible_text(quadrant) # time.sleep(2) # COMMENTED OUT: Arch - not needed per user requirement # Will be handled later for specific procedure codes that require it # arch_dropdowns = wait.until( # EC.presence_of_all_elements_located( # (By.XPATH, "//select[@ng-model='serviceLine.arch']") # ) # ) # if len(arch_dropdowns) < row_number: # raise Exception(f"Only {len(arch_dropdowns)} arch dropdowns found, but need row {row_number}") # arch_dropdown = arch_dropdowns[row_number - 1] # time.sleep(1) # select_arch = Select(arch_dropdown) # select_arch.select_by_visible_text(arch) # time.sleep(2) # COMMENTED OUT: Authorization Number - not needed per user requirement # auth_inputs = wait.until( # EC.presence_of_all_elements_located( # (By.XPATH, "//input[@ng-model='serviceLine.authorizationNumber' and @placeholder='No.']") # ) # ) # if len(auth_inputs) < row_number: # raise Exception(f"Only {len(auth_inputs)} auth inputs found, but need row {row_number}") # auth_input = auth_inputs[row_number - 1] # time.sleep(1) # auth_input.clear() # auth_input.send_keys(auth_number) # time.sleep(2) # auth_input.send_keys(Keys.TAB) # time.sleep(1) # Fill Billing Amount - find the specific row's billed amount input if billed_amount: print(f"DEBUG: Filling billing amount: '{billed_amount}' for row {row_number}") billed_amount_inputs = wait.until( EC.presence_of_all_elements_located( (By.XPATH, "//input[@ng-model='serviceLine.billedAmount' and @placeholder='00.00' and @type='number']") ) ) if len(billed_amount_inputs) < row_number: raise Exception(f"Only {len(billed_amount_inputs)} billed amount inputs found, but need row {row_number}") billed_amount_input = billed_amount_inputs[row_number - 1] time.sleep(1) billed_amount_input.clear() billed_amount_input.send_keys(billed_amount) time.sleep(2) # Trigger change event to ensure Angular recognizes the input self.driver.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true })); arguments[0].dispatchEvent(new Event('change', { bubbles: true }));", billed_amount_input) time.sleep(1) billed_amount_input.send_keys(Keys.TAB) time.sleep(2) # Extra wait to ensure the service line is fully processed else: print(f"DEBUG: No billing amount provided for row {row_number}, skipping") time.sleep(2) # Wait even if no amount to ensure processing print(f"DEBUG: Service line {row_number} filled successfully") return "Success" except Exception as e: print(f"Error filling service line {row_number}: {e}") return "ERROR:SERVICE_LINE_FILL_FAILED" def run(self): try: self.config_driver() self.driver.get("https://provider.masshealth-dental.org/mh_provider_login") time.sleep(2) login_result = self.login() if login_result != "Success": return {"status": "error", "message": login_result} nav_result = self.navigate_to_submit_claims() if nav_result != "Success": return {"status": "error", "message": nav_result} select_result = self.select_dental_claim() if select_result != "Success": return {"status": "error", "message": select_result} date_result = self.fill_date_of_service() if date_result != "Success": return {"status": "error", "message": date_result} pos_result = self.select_place_of_service_office() if pos_result != "Success": return {"status": "error", "message": pos_result} office_result = self.select_office_summit_framingham() if office_result != "Success": return {"status": "error", "message": office_result} dentist_result = self.select_dentist_gao_kai() if dentist_result != "Success": return {"status": "error", "message": dentist_result} eligibility_result = self.fill_member_eligibility() if eligibility_result != "Success": return {"status": "error", "message": eligibility_result} # Fill service lines - handle multiple lines if not self.serviceLines: print("DEBUG: No service lines found, using fallback values") # Fallback: fill one service line with default values first_service_result = self.fill_service_line( row_number=1, procedure_code=self.procedureCode, tooth_number=self.toothNumber, tooth_surface=self.toothSurface ) if first_service_result != "Success": return {"status": "error", "message": first_service_result} else: # Fill each service line for i, service_line in enumerate(self.serviceLines): row_num = i + 1 # Convert to 1-based indexing print(f"DEBUG: Processing service line {row_num} of {len(self.serviceLines)}") # For first line, fill directly if i == 0: service_result = self.fill_service_line( row_number=row_num, procedure_code=service_line.get("procedureCode", ""), tooth_number=service_line.get("toothNumber", ""), tooth_surface=service_line.get("toothSurface", ""), billed_amount=service_line.get("totalBilled", "") ) else: # For subsequent lines, click plus button first plus_result = self.click_plus_button() if plus_result != "Success": return {"status": "error", "message": plus_result} service_result = self.fill_service_line( row_number=row_num, procedure_code=service_line.get("procedureCode", ""), tooth_number=service_line.get("toothNumber", ""), tooth_surface=service_line.get("toothSurface", ""), billed_amount=service_line.get("totalBilled", "") ) if service_result != "Success": return {"status": "error", "message": f"Service line {row_num} failed: {service_result}"} # Select Radiology Reports document type after filling all service lines radiology_result = self.select_radiology_reports() if radiology_result != "Success": return {"status": "error", "message": radiology_result} # Add file attachment after selecting document type (optional - only if files provided) if self.upload_files: file_result = self.add_file_attachment() if file_result != "Success": return {"status": "error", "message": file_result} else: print("DEBUG: No files to attach, skipping attachment step") # Click SUBMIT button to submit the claim submit_result = self.click_submit_button() if submit_result != "Success": return {"status": "error", "message": submit_result} # Save confirmation page as PDF pdf_result = self.save_confirmation_pdf() if pdf_result.get("status") == "error": return {"status": "error", "message": pdf_result.get("message")} # Optional: wait a moment so you can see the confirmation page time.sleep(3) return { "status": "success", "message": "Claims automation completed successfully.", "pdf_path": pdf_result.get("pdf_path"), "file_type": pdf_result.get("file_type"), "claimNumber": pdf_result.get("claimNumber") } except Exception as e: print(f"Claims automation error: {e}") return {"status": "error", "message": str(e)} finally: # Close the browser after all processes complete if self.driver: self.driver.quit()