From 0844c1296e7f753b579eba13b37427ad50b9d383 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Sun, 1 Jun 2025 18:35:51 +0530 Subject: [PATCH] checkpoint date working --- apps/Backend/src/routes/claims.ts | 23 ++ apps/Backend/src/routes/patients.ts | 8 - apps/Backend/src/routes/pdfExtraction.ts | 4 +- .../{pythonClient.ts => pdfClient.ts} | 2 +- apps/Backend/src/services/seleniumClient.ts | 25 ++ apps/Backend/src/storage/index.ts | 3 +- .../src/components/claims/claim-form.tsx | 73 +++-- .../file-upload/multiple-file-upload-zone.tsx | 2 +- apps/Frontend/src/pages/claims-page.tsx | 98 ++++-- apps/SeleniumService/.gitignore | 3 +- apps/SeleniumService/agent.py | 26 ++ apps/SeleniumService/requirements.txt | Bin 1406 -> 2118 bytes .../{main.py => selenium_raw.py} | 7 +- apps/SeleniumService/selenium_worker.py | 302 ++++++++++++++++++ packages/db/prisma/schema.prisma | 12 +- 15 files changed, 511 insertions(+), 77 deletions(-) rename apps/Backend/src/services/{pythonClient.ts => pdfClient.ts} (91%) create mode 100644 apps/Backend/src/services/seleniumClient.ts create mode 100644 apps/SeleniumService/agent.py rename apps/SeleniumService/{main.py => selenium_raw.py} (98%) create mode 100644 apps/SeleniumService/selenium_worker.py diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 8fd3c54..150d0f5 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -3,6 +3,9 @@ import { Request, Response } from "express"; import { storage } from "../storage"; import { z } from "zod"; import { ClaimUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; +import multer from "multer"; +import forwardToSeleniumAgent from "../services/seleniumClient"; + const router = Router(); @@ -37,6 +40,26 @@ const ExtendedClaimSchema = ( }); // Routes +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" }); + } + + try { + const claimData = JSON.parse(req.body.data); + const files = req.files as Express.Multer.File[]; + + const result = await forwardToSeleniumAgent(claimData, files); + + res.json({ success: true, data: result }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to forward to selenium agent" }); + } +}); // Get all claims for the logged-in user router.get("/", async (req: Request, res: Response) => { diff --git a/apps/Backend/src/routes/patients.ts b/apps/Backend/src/routes/patients.ts index e8b7c65..d003b67 100644 --- a/apps/Backend/src/routes/patients.ts +++ b/apps/Backend/src/routes/patients.ts @@ -200,18 +200,10 @@ router.delete( .json({ message: "Forbidden: Patient belongs to a different user" }); } - const appointments = await storage.getAppointmentsByPatientId(patientId); - console.log(appointments) - if (appointments.length > 0) { - throw new Error(`Cannot delete patient with ID ${patientId} because they have appointments`); - } // Delete patient await storage.deletePatient(patientId); res.status(204).send(); } catch (error:any) { - if (error.message.includes("have appointments")) { - return res.status(400).json({ message: error.message }); - } console.error("Delete patient error:", error); res.status(500).json({ message: "Failed to delete patient" }); } diff --git a/apps/Backend/src/routes/pdfExtraction.ts b/apps/Backend/src/routes/pdfExtraction.ts index 1173f81..1cd141b 100644 --- a/apps/Backend/src/routes/pdfExtraction.ts +++ b/apps/Backend/src/routes/pdfExtraction.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import type { Request, Response } from "express"; const router = Router(); import multer from "multer"; -import forwardToPythonService from "../services/pythonClient"; +import forwardToPdfService from "../services/PdfClient"; const upload = multer({ storage: multer.memoryStorage() }); @@ -12,7 +12,7 @@ router.post("/extract", upload.single("pdf"), async (req: Request, res: Response } try { - const result = await forwardToPythonService(req.file); + const result = await forwardToPdfService(req.file); res.json(result); } catch (err) { console.error(err); diff --git a/apps/Backend/src/services/pythonClient.ts b/apps/Backend/src/services/pdfClient.ts similarity index 91% rename from apps/Backend/src/services/pythonClient.ts rename to apps/Backend/src/services/pdfClient.ts index 1a02a2c..101239b 100644 --- a/apps/Backend/src/services/pythonClient.ts +++ b/apps/Backend/src/services/pdfClient.ts @@ -9,7 +9,7 @@ export interface ExtractedData { [key: string]: any; } -export default async function forwardToPythonService( +export default async function forwardToPdfService( file: Express.Multer.File ): Promise { const form = new FormData(); diff --git a/apps/Backend/src/services/seleniumClient.ts b/apps/Backend/src/services/seleniumClient.ts new file mode 100644 index 0000000..2016d31 --- /dev/null +++ b/apps/Backend/src/services/seleniumClient.ts @@ -0,0 +1,25 @@ +import axios from "axios"; + +export interface SeleniumPayload { + claim: any; + pdfs: { + originalname: string; + bufferBase64: string; + }[]; +} + +export default async function forwardToSeleniumAgent( + claimData: any, + files: Express.Multer.File[] +): Promise { + const payload: SeleniumPayload = { + claim: claimData, + pdfs: files.map(file => ({ + originalname: file.originalname, + bufferBase64: file.buffer.toString("base64"), + })), + }; + + const response = await axios.post("http://localhost:5002/run", payload); + return response.data; +} diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 3d41bac..c16260c 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -227,7 +227,8 @@ export const storage: IStorage = { try { await db.patient.delete({ where: { id } }); } catch (err) { - throw new Error(`Patient with ID ${id} not found`); + console.error("Error deleting patient:", err); + throw new Error(`Failed to delete patient: ${err}`); } }, diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index e3df3fe..eea9e0d 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -91,6 +91,8 @@ interface ClaimFormData { serviceDate: string; // YYYY-MM-DD insuranceProvider: string; status: string; // default "pending" + massdhp_username?:string, + massdhp_password?:string, serviceLines: ServiceLine[]; } @@ -102,6 +104,7 @@ interface ClaimFormProps { appointmentData: InsertAppointment | UpdateAppointment ) => void; onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void; + onHandleForSelenium: (data: ClaimFormData) => void; onClose: () => void; } @@ -117,6 +120,7 @@ export function ClaimForm({ extractedData, onHandleAppointmentSubmit, onHandleUpdatePatient, + onHandleForSelenium, onSubmit, onClose, }: ClaimFormProps) { @@ -170,31 +174,23 @@ export function ClaimForm({ // Service date state const [serviceDateValue, setServiceDateValue] = useState(new Date()); const [serviceDate, setServiceDate] = useState( - format(new Date(), "MM/dd/yyyy") + new Date().toLocaleDateString("en-CA") // "YYYY-MM-DD" ); - const formatServiceDate = (date: Date | undefined): string => { - return date ? format(date, "MM/dd/yyyy") : ""; - }; - useEffect(() => { if (extractedData?.serviceDate) { const parsed = new Date(extractedData.serviceDate); + const isoFormatted = parsed.toLocaleDateString("en-CA"); setServiceDateValue(parsed); - setServiceDate(formatServiceDate(parsed)); + setServiceDate(isoFormatted); } }, [extractedData]); - // used in submit button to send correct date. - function convertToISODate(mmddyyyy: string): string { - const [month, day, year] = mmddyyyy.split("/"); - return `${year}-${month?.padStart(2, "0")}-${day?.padStart(2, "0")}`; - } // Update service date when calendar date changes const onServiceDateChange = (date: Date | undefined) => { if (date) { - const formattedDate = format(date, "MM/dd/yyyy"); + const formattedDate = date.toLocaleDateString("en-CA"); // "YYYY-MM-DD" setServiceDateValue(date); setServiceDate(formattedDate); setForm((prev) => ({ ...prev, serviceDate: formattedDate })); @@ -215,7 +211,7 @@ export function ClaimForm({ }; // MAIN FORM INITIAL STATE - const [form, setForm] = useState({ + const [form, setForm] = useState({ patientId: patientId || 0, appointmentId: 0, //need to update userId: Number(user?.id), @@ -227,6 +223,8 @@ export function ClaimForm({ serviceDate: serviceDate, insuranceProvider: "", status: "pending", + massdhp_username: "", + massdhp_password: "", serviceLines: [ { procedureCode: "", @@ -237,6 +235,8 @@ export function ClaimForm({ billedAmount: 0, }, ], + + uploadedFiles: [], }); // Sync patient data to form when patient updates @@ -294,7 +294,7 @@ export function ClaimForm({ const updateProcedureDate = (index: number, date: Date | undefined) => { if (!date) return; - const formattedDate = format(date, "MM/dd/yyyy"); + const formattedDate = date.toLocaleDateString("en-CA"); const updatedLines = [...form.serviceLines]; if (updatedLines[index]) { @@ -305,24 +305,26 @@ export function ClaimForm({ }; // FILE UPLOAD ZONE - const [uploadedFiles, setUploadedFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const handleFileUpload = (files: File[]) => { setIsUploading(true); const validFiles = files.filter((file) => file.type === "application/pdf"); - if (validFiles.length > 10) { + if (validFiles.length > 5) { toast({ title: "Too Many Files", - description: "You can only upload up to 10 PDFs.", + description: "You can only upload up to 5 PDFs.", variant: "destructive", }); setIsUploading(false); return; } - setUploadedFiles(validFiles); + setForm((prev) => ({ + ...prev, + uploadedFiles: validFiles, + })); toast({ title: "Files Selected", @@ -334,9 +336,15 @@ export function ClaimForm({ // Delta MA Button Handler const handleDeltaMASubmit = async () => { + + function convertToISODate(mmddyyyy: string): string { + const [month, day, year] = mmddyyyy.split("/"); + return `${year}-${month?.padStart(2, "0")}-${day?.padStart(2, "0")}`; + } + const appointmentData = { patientId: patientId, - date: convertToISODate(serviceDate), + date: serviceDate, staffId: staff?.id, }; @@ -361,14 +369,25 @@ export function ClaimForm({ } // 3. Create Claim(if not) + const { uploadedFiles, massdhp_username, massdhp_password, ...formToCreateClaim } = form; onSubmit({ - ...form, + ...formToCreateClaim, staffId: Number(staff?.id), - patientId: patient?.id, + patientId: patientId, insuranceProvider: "Delta MA", appointmentId: appointmentId!, }); + // 4. sending form data to selenium service + onHandleForSelenium({ + ...form, + staffId: Number(staff?.id), + patientId: patientId, + insuranceProvider: "Delta MA", + appointmentId: appointmentId!, + massdhp_username: "kqkgaox@yahoo.com", + massdhp_password: "Lex123456", //fetch this from db, by call + }); // 4. Close form onClose(); }; @@ -630,9 +649,9 @@ export function ClaimForm({ {/* File Upload Section */}

- Only PDF files allowed. You can upload up to 10 files. File - types with 4+ character extensions like .DOCX, .PPTX, or .XLSX - are not allowed. + Only PDF files allowed. You can upload up to 5 files. File types + with 4+ character extensions like .DOCX, .PPTX, or .XLSX are not + allowed.

@@ -657,12 +676,12 @@ export function ClaimForm({ onFileUpload={handleFileUpload} isUploading={isUploading} acceptedFileTypes="application/pdf" - maxFiles={10} + maxFiles={5} /> - {uploadedFiles.length > 0 && ( + {form.uploadedFiles.length > 0 && (
    - {uploadedFiles.map((file, index) => ( + {form.uploadedFiles.map((file, index) => (
  • {file.name}
  • ))}
diff --git a/apps/Frontend/src/components/file-upload/multiple-file-upload-zone.tsx b/apps/Frontend/src/components/file-upload/multiple-file-upload-zone.tsx index da32fea..ff5d048 100644 --- a/apps/Frontend/src/components/file-upload/multiple-file-upload-zone.tsx +++ b/apps/Frontend/src/components/file-upload/multiple-file-upload-zone.tsx @@ -15,7 +15,7 @@ export function MultipleFileUploadZone({ onFileUpload, isUploading, acceptedFileTypes = "application/pdf", - maxFiles = 10, + maxFiles = 5, }: FileUploadZoneProps) { const { toast } = useToast(); const [isDragging, setIsDragging] = useState(false); diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 13d564a..66061d4 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -10,8 +10,8 @@ import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; -import { FileCheck, CheckCircle, Clock, AlertCircle } from "lucide-react"; -import { format } from "date-fns"; +import { FileCheck } from "lucide-react"; +import { parse, format } from "date-fns"; import { z } from "zod"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useLocation } from "wouter"; @@ -209,30 +209,33 @@ export default function ClaimsPage() { }, }); + // Converts local date to exact UTC date with no offset issues + function parseLocalDate(dateInput: Date | string): Date { + if (dateInput instanceof Date) return dateInput; + + const dateString = dateInput.split("T")[0] || dateInput; + + const parts = dateString.split("-"); + if (parts.length !== 3) { + throw new Error(`Invalid date format: ${dateString}`); + } + + const year = Number(parts[0]); + const month = Number(parts[1]); + const day = Number(parts[2]); + + if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) { + throw new Error(`Invalid date parts in date string: ${dateString}`); + } + + return new Date(year, month - 1, day); // month is 0-indexed + } + const handleAppointmentSubmit = async ( appointmentData: InsertAppointment | UpdateAppointment ): Promise => { - // Converts local date to exact UTC date with no offset issues - function parseLocalDate(dateInput: Date | string): Date { - if (dateInput instanceof Date) return dateInput; - - const parts = dateInput.split("-"); - if (parts.length !== 3) { - throw new Error(`Invalid date format: ${dateInput}`); - } - - const year = Number(parts[0]); - const month = Number(parts[1]); - const day = Number(parts[2]); - - if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) { - throw new Error(`Invalid date parts in date string: ${dateInput}`); - } - - return new Date(year, month - 1, day); // month is 0-indexed - } - const rawDate = parseLocalDate(appointmentData.date); + const formattedDate = rawDate.toLocaleDateString("en-CA"); // YYYY-MM-DD format // Prepare minimal data to update/create @@ -354,12 +357,14 @@ export default function ClaimsPage() { (a) => a.patientId === patient.id ); + const dateToUse = lastAppointment + ? parseLocalDate(lastAppointment.date) + : new Date(); + setClaimFormData((prev: any) => ({ ...prev, patientId: patient.id, - serviceDate: lastAppointment - ? new Date(lastAppointment.date).toISOString().slice(0, 10) - : new Date().toISOString().slice(0, 10), + serviceDate: dateToUse.toLocaleDateString("en-CA"), // consistent "YYYY-MM-DD" })); }; @@ -378,13 +383,15 @@ export default function ClaimsPage() { const [firstName, ...rest] = name.trim().split(" "); const lastName = rest.join(" ") || ""; - const parsedDob = new Date(dob); + const parsedDob = parse(dob, "M/d/yyyy", new Date()); // robust for "4/17/1964", "12/1/1975", etc. const isValidDob = !isNaN(parsedDob.getTime()); const newPatient: InsertPatient = { firstName, lastName, - dateOfBirth: isValidDob ? parsedDob : new Date(), + dateOfBirth: isValidDob + ? format(parsedDob, "yyyy-MM-dd") + : format(new Date(), "yyyy-MM-dd"), gender: "", phone: "", userId: user?.id ?? 1, @@ -463,6 +470,42 @@ export default function ClaimsPage() { } }; + // handle selenium + const handleSelenium = async (data: any) => { + const formData = new FormData(); + + formData.append("data", JSON.stringify(data)); + + const uploadedFiles: File[] = data.uploadedFiles ?? []; + + uploadedFiles.forEach((file: File) => { + formData.append("pdfs", file); + }); + + try { + const response = await apiRequest( + "POST", + "/api/claims/selenium", + formData + ); + const result = await response.json(); + + toast({ + title: "Selenium service notified", + description: "Your claim data was successfully sent to Selenium.", + variant: "default", + }); + + return result; + } catch (error: any) { + toast({ + title: "Selenium service error", + description: error.message || "An error occurred.", + variant: "destructive", + }); + } + }; + return (
)}
diff --git a/apps/SeleniumService/.gitignore b/apps/SeleniumService/.gitignore index 2eea525..09515b8 100644 --- a/apps/SeleniumService/.gitignore +++ b/apps/SeleniumService/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +/__pycache__ \ No newline at end of file diff --git a/apps/SeleniumService/agent.py b/apps/SeleniumService/agent.py new file mode 100644 index 0000000..44767bf --- /dev/null +++ b/apps/SeleniumService/agent.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/requirements.txt b/apps/SeleniumService/requirements.txt index c9f6de9ca34ac37e5a11b6f1b3b331011b3fc132..232572b7813c1b5e6f969a900fec3cb846ac4f33 100644 GIT binary patch delta 713 zcmZ8f!AiqW5Zp?=6a;UcdXOS`sWwSt6)7GpJt)PB2k{`Hu`RW28k0s)5fA+VLq5Z! z_zV7uAK<~8IJ2+8B4J6$?9R;2dq0Jb<(Ku2FTRXrBA2L+TuT}6dkOFzNgYo`Hl&8C z$dVM$;&*cjmB2dcZ==b`^JkZKJGYwX||2Lx8>v0EY@&98Y$qN1j3+AhL48 z=9DUW&>EsbXpc~tCu(nLM?-GZS8wLZP2OwuE`CM2Rshv($O5UW)V7W`LCroROuO`C zC7-qDK87RY!Hs)rc`WfP6S`xWOb&D{dazX}qcQGU_#Z+bMy4nD?t+VU*u`v(5pqw2 zyN(Q=Zsw+k0kSp@CeZM7&Ll&duCDOE=R#R{22^3UL#^~RcU)|PuLY&HG^D9Gd2Efu zef!SsE$l~fi%bGF9nDB+lc_l>Xi^Q))q!PRnk?Na4g_?ml|L_)HIzs%l7}u{8Go)5 rDW^~Q$6_pZx*8Ao4|nux+38n)P&|bK1AM?c#!+P9(YlkKEw23n-%NO= delta 49 zcmV-10M7r$5dI2~D3O#LkfG diff --git a/apps/SeleniumService/main.py b/apps/SeleniumService/selenium_raw.py similarity index 98% rename from apps/SeleniumService/main.py rename to apps/SeleniumService/selenium_raw.py index d005a8e..1a2b496 100644 --- a/apps/SeleniumService/main.py +++ b/apps/SeleniumService/selenium_raw.py @@ -1,6 +1,5 @@ from selenium import webdriver from selenium.common import TimeoutException -from selenium.common.exceptions import ElementNotInteractableException from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait @@ -13,6 +12,7 @@ from dotenv import load_dotenv import os load_dotenv() +# this file is just for testing selenium solely. procedure_codes = [ { "procedure_code": "D0210", @@ -38,7 +38,7 @@ data = { "massdhp_username": os.getenv("MASSDHP_USERNAME"), "massdhp_password": os.getenv("MASSDHP_PASSWORD"), "memberId": os.getenv("memberId"), - "dob":os.getenv("dob"), + "dateOfBirth":os.getenv("dob"), "procedure_codes": procedure_codes, "pdfs" : pdfs, "missingTeethStatus": "Yes_missing", # can be Yes_missing , No_missing, or endentulous @@ -111,7 +111,7 @@ class AutomationMassDHP: # Fill DOB parts try: - dob_parts = data["dob"].split("/") + dob_parts = data["dateOfBirth"].split("/") month= dob_parts[0].zfill(2) # "12" day= dob_parts[1].zfill(2) # "13" year = dob_parts[2] # "1965" @@ -319,7 +319,6 @@ class AutomationMassDHP: input("should Close?") # here it sholud get confirmation from the frontend, - self.driver.close() diff --git a/apps/SeleniumService/selenium_worker.py b/apps/SeleniumService/selenium_worker.py new file mode 100644 index 0000000..ffd8440 --- /dev/null +++ b/apps/SeleniumService/selenium_worker.py @@ -0,0 +1,302 @@ +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 +from selenium.webdriver.support.ui import Select +import time +from datetime import datetime +import tempfile +import base64 +import os + + +class AutomationMassDHP: + def __init__(self, data): + self.data = data + self.headless = False + self.driver = None + + def config_driver(self): + options = webdriver.ChromeOptions() + if self.headless: + options.add_argument("--headless") + 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.data["claim"]["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.data["claim"]["massdhp_password"]) + + # Click login + login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Login']"))) + login_button.click() + + print("Login submitted successfully.") + 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: + claim_upload_link = wait.until( + EC.element_to_be_clickable((By.XPATH, "//a[text()='Claim Upload']")) + ) + claim_upload_link.click() + + time.sleep(3) + + # Fill Member ID + member_id_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text1"]'))) + member_id_input.clear() + member_id_input.send_keys(self.data["claim"]["memberId"]) + + # Fill DOB parts + try: + dob_parts = self.data["claim"]["dateOfBirth"].split("/") + month= dob_parts[0].zfill(2) # "12" + day= dob_parts[1].zfill(2) # "13" + year = dob_parts[2] # "1965" + except Exception as e: + print(f"Error parsing DOB: {e}") + return "ERROR: PARSING DOB" + + + wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text2"]'))).send_keys(month) + wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text3"]'))).send_keys(day) + wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text4"]'))).send_keys(year) + + # Rendering Provider NPI dropdown + npi_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Select1"]'))) + select_npi = Select(npi_dropdown) + select_npi.select_by_index(1) + + # Office Location dropdown + location_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Select2"]'))) + select_location = Select(location_dropdown) + select_location.select_by_index(1) + + # Click Continue button + continue_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@type="submit" and @value="Continue"]'))) + continue_btn.click() + + + # Check for error message + try: + error_msg = WebDriverWait(self.driver, 5).until(EC.presence_of_element_located( + (By.XPATH, "//td[@class='text_err_msg' and contains(text(), 'Invalid Member ID or Date of Birth')]") + )) + if error_msg: + print("Error: Invalid Member ID or Date of Birth.") + input("HERE?") + return "ERROR: INVALID MEMBERID OR DOB" + 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): + + wait = WebDriverWait(self.driver, 30) + + # already waiting in step1 last part, so no time sleep. + + # 1 - Procedure Codes part + try: + for proc in self.data["procedure_codes"]: + # Wait for Procedure Code dropdown and select code + + select_element = wait.until(EC.presence_of_element_located((By.XPATH, "//select[@id='Select3']"))) + Select(select_element).select_by_value(proc['procedure_code']) + + # Fill Procedure Date if present + if proc.get("procedure_date"): + try: + # Try to normalize date to MM/DD/YYYY format + parsed_date = datetime.strptime(proc["procedure_date"], "%m/%d/%Y") + formatted_date = parsed_date.strftime("%m/%d/%Y") + + date_xpath = "//input[@name='ProcedureDate']" + wait.until(EC.presence_of_element_located((By.XPATH, date_xpath))).clear() + self.driver.find_element(By.XPATH, date_xpath).send_keys(formatted_date) + + except ValueError: + # Invalid date format - skip filling ProcedureDate field + pass + + # Fill Oral Cavity Area if present + if proc.get("oralCavityArea"): + oral_xpath = "//input[@name='OralCavityArea']" + wait.until(EC.presence_of_element_located((By.XPATH, oral_xpath))).clear() + self.driver.find_element(By.XPATH, oral_xpath).send_keys(proc["oralCavityArea"]) + + # Fill Tooth Number if present + if proc.get("toothNumber"): + tooth_num_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, "//select[@name='ToothNumber']"))) + select_tooth = Select(tooth_num_dropdown) + select_tooth.select_by_value(proc["toothNumber"]) + + + # Fill Tooth Surface if present + if proc.get("toothSurface"): + surface = proc["toothSurface"] + checkbox_xpath = f"//input[@type='checkbox' and @name='TS_{surface}']" + checkbox = wait.until(EC.element_to_be_clickable((By.XPATH, checkbox_xpath))) + if not checkbox.is_selected(): + checkbox.click() + + + # Fill Fees if present + if proc.get("fees"): + fees_xpath = "//input[@name='ProcedureFee']" + wait.until(EC.presence_of_element_located((By.XPATH, fees_xpath))).clear() + self.driver.find_element(By.XPATH, fees_xpath).send_keys(proc["fees"]) + + # Click "Add Procedure" button + add_proc_xpath = "//input[@type='submit' and @value='Add Procedure']" + wait.until(EC.element_to_be_clickable((By.XPATH, add_proc_xpath))).click() + + time.sleep(1) + + + print("Procedure codes submitted successfully.") + + + except Exception as e: + print(f"Error while filling Procedure Codes: {e}") + return "ERROR:PROCEDURE CODES" + + # 2 - Upload PDFs: + try: + + pdfs_abs = [proc for proc in self.data.get("pdfs", [])] + + with tempfile.TemporaryDirectory() as tmp_dir: + for pdf_obj in pdfs_abs: + base64_data = pdf_obj["buffer"] + file_name = pdf_obj.get("originalname", "tempfile.pdf") + + # Full path with original filename inside temp dir + tmp_file_path = os.path.join(tmp_dir, file_name) + + # Decode and save + with open(tmp_file_path, "wb") as tmp_file: + tmp_file.write(base64.b64decode(base64_data)) + + # Upload using Selenium + file_input = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='FileName' and @type='file']"))) + file_input.send_keys(tmp_file_path) + + upload_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Upload File']"))) + upload_button.click() + + time.sleep(3) + except Exception as e: + print(f"Error while uploading PDFs: {e}") + return "ERROR:PDF FAILED" + + # 3 - Indicate Missing Teeth Part + try: + # Handle the missing teeth section based on the status + missing_status = self.data.get("missingTeethStatus") + + if missing_status == "No_missing": + missing_teeth_no = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@type='checkbox' and @name='PAU_Step3_Checkbox1']"))) + missing_teeth_no.click() + + elif missing_status == "endentulous": + missing_teeth_edentulous = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@type='checkbox' and @name='PAU_Step3_Checkbox2']"))) + missing_teeth_edentulous.click() + + elif missing_status == "Yes_missing": + missing_teeth_dict = self.data.get("missingTeeth", {}) + + # For each tooth in the missing teeth dict, select the dropdown option + for tooth_name, value in missing_teeth_dict.items(): + if value: # only if there's a value to select + select_element = wait.until(EC.presence_of_element_located((By.XPATH, f"//select[@name='{tooth_name}']"))) + select_obj = Select(select_element) + select_obj.select_by_value(value) + + + # Wait for upload button and click it + update_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Update Missing Teeth']"))) + update_button.click() + + time.sleep(3) + except Exception as e: + print(f"Error while filling missing teeth: {e}") + return "ERROR:MISSING TEETH FAILED" + + + # 4 - Update Remarks + try: + textarea = wait.until(EC.presence_of_element_located((By.XPATH, "//textarea[@name='Remarks']"))) + textarea.clear() + textarea.send_keys(self.data["remarks"]) + + # Wait for update button and click it + update_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Update Remarks']"))) + update_button.click() + + time.sleep(3) + + except Exception as e: + print(f"Error while filling remarks: {e}") + return "ERROR:REMARKS FAILED" + + return "Success" + + + def main_workflow(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 + + time.sleep(5) + value3 = self.step2() + if value3.startswith("ERROR"): + self.driver.close() + return value3 + + + input("should Close?") # here it sholud get confirmation from the frontend, + + self.driver.close() + + return {"status": "success", "message": "Successfully submitted the form."} diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index d6e442b..12ffb5f 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -67,10 +67,11 @@ model Appointment { status String @default("scheduled") // "scheduled", "completed", "cancelled", "no-show" createdAt DateTime @default(now()) - patient Patient @relation(fields: [patientId], references: [id]) + patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id]) staff Staff? @relation(fields: [staffId], references: [id]) - claims Claim[] + claims Claim[] + } model Staff { @@ -100,10 +101,11 @@ model Claim { updatedAt DateTime @updatedAt status String @default("pending") // "pending", "completed", "cancelled", "no-show" - patient Patient @relation(fields: [patientId], references: [id]) - appointment Appointment @relation(fields: [appointmentId], references: [id]) + patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) + appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id]) staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id]) + serviceLines ServiceLine[] } @@ -116,5 +118,5 @@ model ServiceLine { toothNumber String? toothSurface String? billedAmount Float - claim Claim @relation(fields: [claimId], references: [id]) + claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade) }