From f585111b238d9fb2ce4ec3615a034510edd38f65 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Mon, 9 Jun 2025 19:59:58 +0530 Subject: [PATCH] selenium fetchpdf checkpoint --- apps/Backend/src/routes/claims.ts | 88 +++++++++----- apps/Backend/src/services/seleniumClient.ts | 11 +- .../src/components/claims/claim-form.tsx | 8 +- apps/Frontend/src/pages/claims-page.tsx | 52 +++++++- apps/SeleniumService/agent.py | 33 ++++- apps/SeleniumService/agent_test_1.py | 26 ++++ apps/SeleniumService/agent_test_2.py | 42 +++++++ apps/SeleniumService/selenium_worker.py | 115 ++++++++++++++---- 8 files changed, 309 insertions(+), 66 deletions(-) create mode 100644 apps/SeleniumService/agent_test_1.py create mode 100644 apps/SeleniumService/agent_test_2.py diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 18316f5..c877a0f 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -4,8 +4,7 @@ import { storage } from "../storage"; import { z } from "zod"; import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import multer from "multer"; -import forwardToSeleniumAgent from "../services/seleniumClient"; - +import { forwardToSeleniumAgent, forwardToSeleniumAgent2 } from "../services/seleniumClient"; const router = Router(); @@ -43,38 +42,65 @@ const ExtendedClaimSchema = ( const multerStorage = multer.memoryStorage(); // NO DISK const upload = multer({ storage: multerStorage }); -router.post("/selenium", upload.array("pdfs"), async (req: Request, res: Response): Promise => { - if (!req.files || !req.body.data) { - return res.status(400).json({ error: "Missing files or claim data for selenium" }); - } - - if (!req.user || !req.user.id) { - return res.status(401).json({ error: "Unauthorized: user info missing" }); -} - - - try { - const claimData = JSON.parse(req.body.data); - const files = req.files as Express.Multer.File[]; - - const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, claimData.insuranceSiteKey); - if (!credentials) { - return res.status(404).json({ error: "No insurance credentials found for this provider." }); +router.post( + "/selenium", + upload.array("pdfs"), + async (req: Request, res: Response): Promise => { + if (!req.files || !req.body.data) { + return res + .status(400) + .json({ error: "Missing files or claim data for selenium" }); } - const enrichedData = { - ...claimData, - massdhpUsername: credentials.username, - massdhpPassword: credentials.password, - }; - const result = await forwardToSeleniumAgent(enrichedData, files); + if (!req.user || !req.user.id) { + return res.status(401).json({ error: "Unauthorized: user info missing" }); + } - res.json({ success: true, data: result }); - } catch (err) { - console.error(err); - res.status(500).json({ error: "Failed to forward to selenium agent" }); + try { + const claimData = JSON.parse(req.body.data); + const files = req.files as Express.Multer.File[]; + + const credentials = await storage.getInsuranceCredentialByUserAndSiteKey( + req.user.id, + claimData.insuranceSiteKey + ); + if (!credentials) { + return res + .status(404) + .json({ error: "No insurance credentials found for this provider." }); + } + + const enrichedData = { + ...claimData, + massdhpUsername: credentials.username, + massdhpPassword: credentials.password, + }; + const result = await forwardToSeleniumAgent(enrichedData, files); + + res.json({ success: true, data: result }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to forward to selenium agent" }); + } } -}); +); + +router.post( + "/selenium/fetchpdf", + async (req: Request, res: Response): Promise => { + if (!req.user || !req.user.id) { + return res.status(401).json({ error: "Unauthorized: user info missing" }); + } + + try { + const result = await forwardToSeleniumAgent2(); + res.json({ success: true, data: result }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to forward to selenium agent 2" }); + } + } +); // GET /api/claims?page=1&limit=5 router.get("/", async (req: Request, res: Response) => { @@ -110,7 +136,6 @@ router.get("/recent", async (req: Request, res: Response) => { } }); - // Get all claims for the logged-in user router.get("/all", async (req: Request, res: Response) => { try { @@ -121,7 +146,6 @@ router.get("/all", async (req: Request, res: Response) => { } }); - // Get a single claim by ID router.get("/:id", async (req: Request, res: Response): Promise => { try { diff --git a/apps/Backend/src/services/seleniumClient.ts b/apps/Backend/src/services/seleniumClient.ts index 2016d31..4c6ad0c 100644 --- a/apps/Backend/src/services/seleniumClient.ts +++ b/apps/Backend/src/services/seleniumClient.ts @@ -8,7 +8,7 @@ export interface SeleniumPayload { }[]; } -export default async function forwardToSeleniumAgent( +export async function forwardToSeleniumAgent( claimData: any, files: Express.Multer.File[] ): Promise { @@ -20,6 +20,13 @@ export default async function forwardToSeleniumAgent( })), }; - const response = await axios.post("http://localhost:5002/run", payload); + const response = await axios.post("http://localhost:5002/start-workflow", payload); + return response.data; +} + +export async function forwardToSeleniumAgent2( +): Promise { + + const response = await axios.post("http://localhost:5002/fetch-pdf"); return response.data; } diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index d9b525c..ab1ffa0 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -360,9 +360,9 @@ export function ClaimForm({ // 3. Create Claim(if not) // Filter out empty service lines (empty procedureCode) - const filteredServiceLines = form.serviceLines.filter( - (line) => line.procedureCode.trim() !== "" - ); + const filteredServiceLines = form.serviceLines.filter( + (line) => line.procedureCode.trim() !== "" + ); const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form; onSubmit({ @@ -377,7 +377,7 @@ export function ClaimForm({ // 4. sending form data to selenium service onHandleForSelenium({ ...form, - serviceLines: filteredServiceLines, + serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, insuranceProvider: "Mass Health", diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 25c434f..aef3158 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -16,6 +16,7 @@ import { z } from "zod"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useLocation } from "wouter"; import RecentClaims from "@/components/claims/recent-claims"; +import { Button } from "@/components/ui/button"; //creating types out of schema auto generated. type Appointment = z.infer; @@ -68,6 +69,7 @@ type UpdatePatient = z.infer; export default function ClaimsPage() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isClaimFormOpen, setIsClaimFormOpen] = useState(false); + const [isMhPopupOpen, setIsMhPopupOpen] = useState(false); const [selectedPatient, setSelectedPatient] = useState(null); const { toast } = useToast(); const { user } = useAuth(); @@ -210,6 +212,29 @@ export default function ClaimsPage() { }, }); + // selenium pdf download handler + const handleSeleniumPopup = async (actionType: string) => { + try { + const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", { + action: actionType, + }); + const result = await res.json(); + + toast({ + title: "Success", + description: "MH action completed.", + }); + + setIsMhPopupOpen(false); + } catch (error: any) { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + } + }; + // Converts local date to exact UTC date with no offset issues function parseLocalDate(dateInput: Date | string): Date { if (dateInput instanceof Date) return dateInput; @@ -496,7 +521,7 @@ export default function ClaimsPage() { description: "Your claim data was successfully sent to Selenium.", variant: "default", }); - + setIsMhPopupOpen(true); return result; } catch (error: any) { toast({ @@ -630,6 +655,31 @@ export default function ClaimsPage() { onHandleForSelenium={handleSelenium} /> )} + + {isMhPopupOpen && ( +
+
+

Selenium Handler

+ +
+ + + +
+
+
+ )} ); } diff --git a/apps/SeleniumService/agent.py b/apps/SeleniumService/agent.py index 44767bf..194de8a 100644 --- a/apps/SeleniumService/agent.py +++ b/apps/SeleniumService/agent.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware import uvicorn -from selenium_worker import AutomationMassDHP +from selenium_worker import AutomationMassHealth app = FastAPI() @@ -12,15 +12,38 @@ app.add_middleware( allow_headers=["*"], ) -@app.post("/run") -async def run_bot(request: Request): + +# Endpoint: Step 1 — Start the automation +@app.post("/start-workflow") +async def start_workflow(request: Request): data = await request.json() try: - bot = AutomationMassDHP(data) - result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp") + bot = AutomationMassHealth(data) + result = bot.main_workflow_upto_step2("https://providers.massdhp.com/providers_login.asp") return result except Exception as e: return {"status": "error", "message": str(e)} +# Endpoint: Step 2 — Extract the PDF content after manual submission +@app.post("/fetch-pdf") +async def fetch_pdf(): + try: + bot = AutomationMassHealth().get_last_instance() + if not bot: + return {"status": "error", "message": "No running automation session"} + + pdf_data = bot.reach_to_pdf() + if pdf_data.get("status") != "success": + return {"status": "error", "message": pdf_data.get("message")} + + return { + "status": "success", + "pdf_url": pdf_data["pdf_url"], + "pdf_base64": pdf_data["pdf_bytes"] + } + except Exception as e: + return {"status": "error", "message": str(e)} + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=5002) diff --git a/apps/SeleniumService/agent_test_1.py b/apps/SeleniumService/agent_test_1.py new file mode 100644 index 0000000..44767bf --- /dev/null +++ b/apps/SeleniumService/agent_test_1.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +from selenium_worker import AutomationMassDHP + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Replace with your frontend domain for security + allow_methods=["*"], + allow_headers=["*"], +) + +@app.post("/run") +async def run_bot(request: Request): + data = await request.json() + try: + bot = AutomationMassDHP(data) + result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp") + return result + except Exception as e: + return {"status": "error", "message": str(e)} + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=5002) diff --git a/apps/SeleniumService/agent_test_2.py b/apps/SeleniumService/agent_test_2.py new file mode 100644 index 0000000..f9f7a53 --- /dev/null +++ b/apps/SeleniumService/agent_test_2.py @@ -0,0 +1,42 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +from selenium_worker import AutomationMassHealth + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Replace with your frontend domain for security + allow_methods=["*"], + allow_headers=["*"], +) + + +# Endpoint: Step 1 — Start the automation +@app.post("/start-workflow") +async def start_workflow(request: Request): + data = await request.json() + try: + bot = AutomationMassHealth(data) + result = bot.main_workflow_upto_step2("https://abc.com/providers_login.asp") + return result + except Exception as e: + return {"status": "error", "message": str(e)} + +# Endpoint: Step 2 — Extract the PDF content after manual submission +@app.post("/fetch-pdf") +async def fetch_pdf(): + try: + bot = AutomationMassHealth().get_last_instance() + pdf_data = bot.reach_to_pdf() + + if not pdf_data: + return {"status": "error", "message": "Failed to fetch PDF"} + return {"status": "success", "pdf_data": pdf_data} + except Exception as e: + return {"status": "error", "message": str(e)} + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=5002) diff --git a/apps/SeleniumService/selenium_worker.py b/apps/SeleniumService/selenium_worker.py index 73ec7dd..c53d100 100644 --- a/apps/SeleniumService/selenium_worker.py +++ b/apps/SeleniumService/selenium_worker.py @@ -11,12 +11,16 @@ from datetime import datetime import tempfile import base64 import os +import requests +import json +class AutomationMassHealth: + last_instance = None -class AutomationMassDHP: def __init__(self, data): self.headless = False self.driver = None + AutomationMassHealth.last_instance = self self.data = data self.claim = data.get("claim", {}) @@ -31,6 +35,10 @@ class AutomationMassDHP: self.serviceLines = self.claim.get("serviceLines", []) self.missingTeethStatus = self.claim.get("missingTeethStatus", "") self.missingTeeth = self.claim.get("missingTeeth", {}) + + @staticmethod + def get_last_instance(): + return AutomationMassHealth.last_instance def config_driver(self): options = webdriver.ChromeOptions() @@ -279,35 +287,98 @@ class AutomationMassDHP: return "ERROR:REMARKS FAILED" return "Success" + + + def reach_to_pdf(self): + wait = WebDriverWait(self.driver, 30) + + try: + print("Waiting for PDF link to appear on success page...") + pdf_link_element = wait.until( + EC.element_to_be_clickable((By.XPATH, "//a[contains(@href, '.pdf')]")) + ) + print("PDF link found. Clicking it...") + + # Click the PDF link + pdf_link_element.click() + time.sleep(5) + + existing_windows = self.driver.window_handles + + # Wait for the new tab + WebDriverWait(self.driver, 90).until( + lambda d: len(d.window_handles) > len(existing_windows) + ) + + print("Switching to PDF tab...") + self.driver.switch_to.window(self.driver.window_handles[1]) - def main_workflow(self, url): + time.sleep(2) + current_url = self.driver.current_url + print(f"Switched to PDF tab. Current URL: {current_url}") + + + # Get full PDF URL in case it's a relative path + pdf_url = pdf_link_element.get_attribute("href") + if not pdf_url.startswith("http"): + base_url = self.driver.current_url.split("/providers")[0] + pdf_url = f"{base_url}/{pdf_url}" + + # Get cookies from Selenium session, saving just for my referece while testing. in prod just use below one line + # cookies = {c['name']: c['value'] for c in self.driver.get_cookies()} + # 1. Get raw Selenium cookies (list of dicts) + raw_cookies = self.driver.get_cookies() + with open("raw_cookies.txt", "w") as f: + json.dump(raw_cookies, f, indent=2) + + formatted_cookies = {c['name']: c['value'] for c in raw_cookies} + with open("formatted_cookies.txt", "w") as f: + for k, v in formatted_cookies.items(): + f.write(f"{k}={v}\n") + + # Use requests to download the file using session cookies + print("Downloading PDF content via requests...") + pdf_response = requests.get(pdf_url, cookies=formatted_cookies) + + if pdf_response.status_code == 200: + print("PDF successfully fetched (bytes length):") + return { + "status": "success", + "pdf_bytes": base64.b64encode(pdf_response.content).decode(), + } + else: + print("Failed to fetch PDF. Status:", pdf_response.status_code, pdf_response) + return { + "status": "error", + "message": pdf_response, + } + + except Exception as e: + print(f"ERROR: {str(e)}") + return { + "status": "error", + "message": str(e), + } + + def main_workflow_upto_step2(self, url): self.config_driver() print("Reaching Site :", url) self.driver.maximize_window() self.driver.get(url) time.sleep(3) - value = self.login() - if value.startswith("ERROR"): - # self.driver.close() - return value - - time.sleep(5) - value2 = self.step1() - if value2.startswith("ERROR"): - # self.driver.close() - return value2 + if self.login().startswith("ERROR"): + return {"status": "error", "message": "Login failed"} - time.sleep(5) - value3 = self.step2() - if value3.startswith("ERROR"): - # self.driver.close() - return value3 + if self.step1().startswith("ERROR"): + return {"status": "error", "message": "Step1 failed"} + if self.step2().startswith("ERROR"): + return {"status": "error", "message": "Step2 failed"} - input("should Close?") # here it sholud get confirmation from the frontend, - - self.driver.close() - - return {"status": "success", "message": "Successfully submitted the form."} + print("Waiting for user to manually submit form in browser...") + return { + "status": "waiting_for_user", + "message": "Automation paused. Please submit the form manually in browser." + } \ No newline at end of file