diff --git a/apps/SeleniumService/agent.py b/apps/SeleniumService/agent.py index baecec0..e9c9794 100644 --- a/apps/SeleniumService/agent.py +++ b/apps/SeleniumService/agent.py @@ -4,6 +4,7 @@ import uvicorn import asyncio from selenium_claimSubmitWorker import AutomationMassHealth from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck +from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck import os from dotenv import load_dotenv @@ -80,6 +81,33 @@ async def start_workflow(request: Request): finally: async with lock: active_jobs -= 1 + +# Endpoint: Step 3 — Start the automation of cheking claim status +@app.post("/claim-status-check") +async def start_workflow(request: Request): + global active_jobs, waiting_jobs + data = await request.json() + + async with lock: + waiting_jobs += 1 + + async with semaphore: + async with lock: + waiting_jobs -= 1 + active_jobs += 1 + try: + bot = AutomationMassHealthClaimStatusCheck(data) + result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp") + + if result.get("status") != "success": + return {"status": "error", "message": result.get("message")} + + return result + except Exception as e: + return {"status": "error", "message": str(e)} + finally: + async with lock: + active_jobs -= 1 # ✅ Status Endpoint @app.get("/status") diff --git a/apps/SeleniumService/requirements.txt b/apps/SeleniumService/requirements.txt index 232572b..ee7865a 100644 Binary files a/apps/SeleniumService/requirements.txt and b/apps/SeleniumService/requirements.txt differ diff --git a/apps/SeleniumService/selenium_claimStatusCheckWorker.py b/apps/SeleniumService/selenium_claimStatusCheckWorker.py new file mode 100644 index 0000000..b98c70a --- /dev/null +++ b/apps/SeleniumService/selenium_claimStatusCheckWorker.py @@ -0,0 +1,222 @@ +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.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from webdriver_manager.chrome import ChromeDriverManager +import time +import os +from io import BytesIO +from PIL import Image + +class AutomationMassHealthClaimStatusCheck: + def __init__(self, data): + self.headless = False + self.driver = None + + self.data = data.get("data") + + # Flatten values for convenience + self.memberId = self.data.get("memberId", "") + self.dateOfBirth = self.data.get("dateOfBirth", "") + self.massdhp_username = self.data.get("massdhpUsername", "") + self.massdhp_password = self.data.get("massdhpPassword", "") + + self.download_dir = os.path.abspath("seleniumDownloads") + 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 + prefs = { + "download.default_directory": self.download_dir, + "plugins.always_open_pdf_externally": True, + "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: + # Enter email + email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='Email' and @type='text']"))) + email_field.clear() + email_field.send_keys(self.massdhp_username) + + # Enter password + password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='Pass' and @type='password']"))) + password_field.clear() + password_field.send_keys(self.massdhp_password) + + # Click login + login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Login']"))) + login_button.click() + + return "Success" + + except Exception as e: + print(f"Error while logging in: {e}") + return "ERROR:LOGIN FAILED" + + def step1(self): + wait = WebDriverWait(self.driver, 30) + + try: + eligibility_link = wait.until( + EC.element_to_be_clickable((By.XPATH, "//a[text()='Claim Status']")) + ) + eligibility_link.click() + + time.sleep(3) + + # Fill Member ID + member_id_input = wait.until(EC.presence_of_element_located((By.XPATH, '//input[@name="MAMedicaidID"]'))) + member_id_input.clear() + member_id_input.send_keys(self.memberId) + + # Click Search button + search_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@id="Submit1"]'))) + search_btn.click() + + time.sleep(2) + + # Check for error message + try: + error_msg = WebDriverWait(self.driver, 5).until(EC.presence_of_element_located( + (By.XPATH, "//font[contains(text(), 'Your search did not return any results. Please try again.')]") + )) + if error_msg: + return "ERROR: THIS MEMBERID HAS NO CLAIM RESULTS" + except TimeoutException: + pass + + # check success message + try: + success_msg = WebDriverWait(self.driver, 5).until(EC.presence_of_element_located( + (By.XPATH, "//td[contains(text(), 'Your search returned')]") + )) + if not success_msg: + return "ERROR: THIS MEMBERID HAS NO CLAIM RESULTS" + except TimeoutException: + pass + + return "Success" + + except Exception as e: + print(f"Error while step1 i.e Cheking the MemberId and DOB in: {e}") + return "ERROR:STEP1" + + + def step2(self): + try: + # ---- Take full page screenshot ---- + screenshot_path = os.path.join(self.download_dir, f"ss_{self.memberId}.png") + + # JS to get sizes + total_width = self.driver.execute_script("return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.documentElement.clientWidth);") + total_height = self.driver.execute_script("return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight);") + viewport_width = self.driver.execute_script("return document.documentElement.clientWidth;") + viewport_height = self.driver.execute_script("return window.innerHeight;") + + # Ensure integers + total_width = int(total_width) + total_height = int(total_height) + viewport_width = int(viewport_width) + viewport_height = int(viewport_height) + + # number of vertical steps + rectangles = [] + y = 0 + while y < total_height: + top = y + height = viewport_height if (y + viewport_height) <= total_height else (total_height - y) + rectangles.append((0, top, viewport_width, top + height)) + y += viewport_height + + stitched_image = Image.new('RGB', (total_width, total_height)) + stitched_y = 0 + + for idx, rect in enumerate(rectangles): + scroll_y = rect[1] + # Scroll to the position (do not trigger reload) + self.driver.execute_script(f"window.scrollTo(0, {scroll_y});") + # Allow time for lazy content to load after scroll (tweak if needed) + time.sleep(0.35) + + # capture viewport screenshot as PNG bytes + png = self.driver.get_screenshot_as_png() # returns bytes + img = Image.open(BytesIO(png)) + + + # If full page width > viewport width (rare), optionally resize/crop: + if img.width != total_width: + # Try to crop or expand: prefer cropping center horizontally + img = img.crop((0, 0, total_width, img.height)) + + # Compute paste height (might be partial for last slice) + paste_height = rect[3] - rect[1] # rect bottom - rect top + # If captured image has same viewport height, we may need to crop to paste_height + if img.height > paste_height: + img = img.crop((0, 0, img.width, paste_height)) + + stitched_image.paste(img, (0, rect[1])) + + # Save stitched image + stitched_image.save(screenshot_path) + + print("Screenshot saved at:", screenshot_path) + + return { + "status": "success", + "ss_path":screenshot_path + } + except Exception as e: + print(f"ERROR: {str(e)}") + return { + "status": "error", + "message": str(e), + } + + finally: + if self.driver: + self.driver.quit() + + def main_workflow(self, url): + try: + self.config_driver() + self.driver.maximize_window() + self.driver.get(url) + time.sleep(3) + + login_result = self.login() + if login_result.startswith("ERROR"): + return {"status": "error", "message": login_result} + + step1_result = self.step1() + if step1_result.startswith("ERROR"): + return {"status": "error", "message": step1_result} + + step2_result = self.step2() + if step2_result.get("status") == "error": + return {"status": "error", "message": step2_result.get("message")} + + return step2_result + except Exception as e: + return { + "status": "error", + "message": e + } + + finally: + self.driver.quit() diff --git a/package-lock.json b/package-lock.json index b751c82..00a4d94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "node-cron": "^4.2.1", "passport": "^0.7.0", "passport-local": "^1.0.0", + "pdfkit": "^0.17.2", "ws": "^8.18.0", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" @@ -64,6 +65,7 @@ "@types/node": "20.16.11", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", + "@types/pdfkit": "^0.17.3", "@types/ws": "^8.5.13", "ts-node-dev": "^2.0.0" } @@ -3893,6 +3895,15 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz", @@ -4594,6 +4605,16 @@ "@types/passport": "*" } }, + "node_modules/@types/pdfkit": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.3.tgz", + "integrity": "sha512-E4tp2qFaghqfS4K5TR4Gn1uTIkg0UAkhUgvVIszr5cS6ZmbioPWEkvhNDy3GtR9qdKC8DLQAnaaMlTcf346VsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -5507,6 +5528,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.25.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", @@ -5894,6 +5924,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -6176,6 +6215,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -6498,6 +6543,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -7482,6 +7533,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -8453,6 +8521,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8907,6 +8981,25 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9862,6 +9955,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10052,6 +10151,19 @@ "path2d-polyfill": "^2.0.1" } }, + "node_modules/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -10260,6 +10372,11 @@ "pathe": "^2.0.3" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -11517,6 +11634,12 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -12528,6 +12651,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -13026,6 +13155,26 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unicorn-magic": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",