From e425a829b2f6e83ef99bc5a7fc2d7f4dd09be3d0 Mon Sep 17 00:00:00 2001 From: Emile Date: Fri, 6 Feb 2026 08:57:29 -0500 Subject: [PATCH] feat(eligibility-check) - enhance DentaQuest and United SCO workflows with flexible input handling; added Selenium session clearing on credential updates and deletions; improved patient name extraction and eligibility checks across services --- .gitignore | 3 +- apps/Backend/src/routes/insuranceCreds.ts | 44 ++++ .../src/routes/insuranceStatusDentaQuest.ts | 105 ++++++-- .../src/routes/insuranceStatusUnitedSCO.ts | 12 +- .../dentaquest-button-modal.tsx | 14 +- .../components/settings/InsuranceCredForm.tsx | 1 + .../settings/insuranceCredTable.tsx | 1 + .../helpers_ddma_eligibility.py | 22 ++ .../helpers_dentaquest_eligibility.py | 30 +++ .../selenium_DDMA_eligibilityCheckWorker.py | 103 ++------ ...enium_DentaQuest_eligibilityCheckWorker.py | 231 +++++++++++++----- ...lenium_UnitedSCO_eligibilityCheckWorker.py | 76 ++---- 12 files changed, 418 insertions(+), 224 deletions(-) diff --git a/.gitignore b/.gitignore index 8cb82bf..a040b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ dist/ # env *.env *chrome_profile_ddma* -*chrome_profile_dentaquest* \ No newline at end of file +*chrome_profile_dentaquest* +*chrome_profile_unitedsco* diff --git a/apps/Backend/src/routes/insuranceCreds.ts b/apps/Backend/src/routes/insuranceCreds.ts index fcf813e..c27460d 100644 --- a/apps/Backend/src/routes/insuranceCreds.ts +++ b/apps/Backend/src/routes/insuranceCreds.ts @@ -76,8 +76,33 @@ router.put("/:id", async (req: Request, res: Response): Promise => { const id = Number(req.params.id); if (isNaN(id)) return res.status(400).send("Invalid credential ID"); + // Get existing credential to know its siteKey + const existing = await storage.getInsuranceCredential(id); + if (!existing) { + return res.status(404).json({ message: "Credential not found" }); + } + const updates = req.body as Partial; const credential = await storage.updateInsuranceCredential(id, updates); + + // Clear Selenium browser session when credentials are changed + const seleniumAgentUrl = process.env.SELENIUM_AGENT_URL || "http://localhost:5002"; + try { + if (existing.siteKey === "DDMA") { + await fetch(`${seleniumAgentUrl}/clear-ddma-session`, { method: "POST" }); + console.log("[insuranceCreds] Cleared DDMA browser session after credential update"); + } else if (existing.siteKey === "DENTAQUEST") { + await fetch(`${seleniumAgentUrl}/clear-dentaquest-session`, { method: "POST" }); + console.log("[insuranceCreds] Cleared DentaQuest browser session after credential update"); + } else if (existing.siteKey === "UNITEDSCO") { + await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" }); + console.log("[insuranceCreds] Cleared United SCO browser session after credential update"); + } + } catch (seleniumErr) { + // Don't fail the update if Selenium session clear fails + console.error("[insuranceCreds] Failed to clear Selenium session:", seleniumErr); + } + return res.status(200).json(credential); } catch (err) { return res @@ -115,6 +140,25 @@ router.delete("/:id", async (req: Request, res: Response): Promise => { .status(404) .json({ message: "Credential not found or already deleted" }); } + + // 4) Clear Selenium browser session for this provider + const seleniumAgentUrl = process.env.SELENIUM_AGENT_URL || "http://localhost:5002"; + try { + if (existing.siteKey === "DDMA") { + await fetch(`${seleniumAgentUrl}/clear-ddma-session`, { method: "POST" }); + console.log("[insuranceCreds] Cleared DDMA browser session after credential deletion"); + } else if (existing.siteKey === "DENTAQUEST") { + await fetch(`${seleniumAgentUrl}/clear-dentaquest-session`, { method: "POST" }); + console.log("[insuranceCreds] Cleared DentaQuest browser session after credential deletion"); + } else if (existing.siteKey === "UNITEDSCO") { + await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" }); + console.log("[insuranceCreds] Cleared United SCO browser session after credential deletion"); + } + } catch (seleniumErr) { + // Don't fail the delete if Selenium session clear fails + console.error("[insuranceCreds] Failed to clear Selenium session:", seleniumErr); + } + return res.status(204).send(); } catch (err) { return res diff --git a/apps/Backend/src/routes/insuranceStatusDentaQuest.ts b/apps/Backend/src/routes/insuranceStatusDentaQuest.ts index 3e252b9..9c1d378 100644 --- a/apps/Backend/src/routes/insuranceStatusDentaQuest.ts +++ b/apps/Backend/src/routes/insuranceStatusDentaQuest.ts @@ -136,12 +136,17 @@ async function handleDentaQuestCompletedJob( // We'll wrap the processing in try/catch/finally so cleanup always runs try { - // 1) ensuring memberid. const insuranceEligibilityData = job.insuranceEligibilityData; - const insuranceId = String(insuranceEligibilityData.memberId ?? "").trim(); + + // 1) Get Member ID - prefer the one extracted from the page by Selenium, + // since we now allow searching by name only + let insuranceId = String(seleniumResult?.memberId ?? "").trim(); if (!insuranceId) { - throw new Error("Missing memberId for DentaQuest job"); + // Fallback to the one provided in the request + insuranceId = String(insuranceEligibilityData.memberId ?? "").trim(); } + + console.log(`[dentaquest-eligibility] Insurance ID: ${insuranceId || "(none)"}`); // 2) Create or update patient (with name from selenium result if available) const patientNameFromResult = @@ -149,23 +154,93 @@ async function handleDentaQuestCompletedJob( ? seleniumResult.patientName.trim() : null; - const { firstName, lastName } = splitName(patientNameFromResult); + // Get name from request data as fallback + let firstName = insuranceEligibilityData.firstName || ""; + let lastName = insuranceEligibilityData.lastName || ""; + + // Override with name from Selenium result if available + if (patientNameFromResult) { + const parsedName = splitName(patientNameFromResult); + firstName = parsedName.firstName || firstName; + lastName = parsedName.lastName || lastName; + } - await createOrUpdatePatientByInsuranceId({ - insuranceId, - firstName, - lastName, - dob: insuranceEligibilityData.dateOfBirth, - userId: job.userId, - }); + // Create or update patient if we have an insurance ID + if (insuranceId) { + await createOrUpdatePatientByInsuranceId({ + insuranceId, + firstName, + lastName, + dob: insuranceEligibilityData.dateOfBirth, + userId: job.userId, + }); + } else { + console.log("[dentaquest-eligibility] No Member ID available - will try to find patient by name/DOB"); + } // 3) Update patient status + PDF upload - const patient = await storage.getPatientByInsuranceId( - insuranceEligibilityData.memberId - ); + // First try to find by insurance ID, then by name + DOB + let patient = insuranceId + ? await storage.getPatientByInsuranceId(insuranceId) + : null; + + // If not found by ID and we have name + DOB, try to find by those + if (!patient && firstName && lastName) { + console.log(`[dentaquest-eligibility] Looking up patient by name: ${firstName} ${lastName}`); + const patients = await storage.getPatientsByUserId(job.userId); + patient = patients.find(p => + p.firstName?.toLowerCase() === firstName.toLowerCase() && + p.lastName?.toLowerCase() === lastName.toLowerCase() + ) || null; + + // If found and we now have the insurance ID, update the patient record + if (patient && insuranceId) { + await storage.updatePatient(patient.id, { insuranceId }); + console.log(`[dentaquest-eligibility] Updated patient ${patient.id} with insuranceId: ${insuranceId}`); + } + } + + // If still no patient found, CREATE a new one with the data we have + if (!patient?.id && firstName && lastName) { + console.log(`[dentaquest-eligibility] Creating new patient: ${firstName} ${lastName}`); + + const createPayload: any = { + firstName, + lastName, + dateOfBirth: insuranceEligibilityData.dateOfBirth || null, + gender: "", + phone: "", + userId: job.userId, + insuranceId: insuranceId || null, + }; + + try { + const patientData = insertPatientSchema.parse(createPayload); + const newPatient = await storage.createPatient(patientData); + if (newPatient) { + patient = newPatient; + console.log(`[dentaquest-eligibility] Created new patient with ID: ${patient.id}`); + } + } catch (err: any) { + // Try without dateOfBirth if it fails + try { + const safePayload = { ...createPayload }; + delete safePayload.dateOfBirth; + const patientData = insertPatientSchema.parse(safePayload); + const newPatient = await storage.createPatient(patientData); + if (newPatient) { + patient = newPatient; + console.log(`[dentaquest-eligibility] Created new patient (no DOB) with ID: ${patient.id}`); + } + } catch (err2: any) { + console.error(`[dentaquest-eligibility] Failed to create patient: ${err2?.message}`); + } + } + } + if (!patient?.id) { outputResult.patientUpdateStatus = - "Patient not found; no update performed"; + "Patient not found and could not be created"; return { patientUpdateStatus: outputResult.patientUpdateStatus, pdfUploadStatus: "none", diff --git a/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts b/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts index 6f210ee..e3c4dd9 100644 --- a/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts +++ b/apps/Backend/src/routes/insuranceStatusUnitedSCO.ts @@ -135,6 +135,7 @@ async function handleUnitedSCOCompletedJob( seleniumResult: any ) { let createdPdfFileId: number | null = null; + let generatedPdfPath: string | null = null; const outputResult: any = {}; // We'll wrap the processing in try/catch/finally so cleanup always runs @@ -204,7 +205,6 @@ async function handleUnitedSCOCompletedJob( // Handle PDF or convert screenshot -> pdf if available let pdfBuffer: Buffer | null = null; - let generatedPdfPath: string | null = null; if ( seleniumResult && @@ -233,7 +233,8 @@ async function handleUnitedSCOCompletedJob( // Convert image to PDF pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path); - const pdfFileName = `unitedsco_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`; + // Use insuranceId (which may come from Selenium result) for filename + const pdfFileName = `unitedsco_eligibility_${insuranceId || "unknown"}_${Date.now()}.pdf`; generatedPdfPath = path.join( path.dirname(seleniumResult.ss_path), pdfFileName @@ -287,18 +288,25 @@ async function handleUnitedSCOCompletedJob( "No valid PDF path provided by Selenium, Couldn't upload pdf to server."; } + // Get filename for frontend preview + const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null; + return { patientUpdateStatus: outputResult.patientUpdateStatus, pdfUploadStatus: outputResult.pdfUploadStatus, pdfFileId: createdPdfFileId, + pdfFilename, }; } catch (err: any) { + // Get filename for frontend preview if available + const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null; return { patientUpdateStatus: outputResult.patientUpdateStatus, pdfUploadStatus: outputResult.pdfUploadStatus ?? `Failed to process United SCO job: ${err?.message ?? String(err)}`, pdfFileId: createdPdfFileId, + pdfFilename, error: err?.message ?? String(err), }; } finally { diff --git a/apps/Frontend/src/components/insurance-status/dentaquest-button-modal.tsx b/apps/Frontend/src/components/insurance-status/dentaquest-button-modal.tsx index a183767..9c361bf 100644 --- a/apps/Frontend/src/components/insurance-status/dentaquest-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/dentaquest-button-modal.tsx @@ -127,6 +127,11 @@ export function DentaQuestEligibilityButton({ const [isStarting, setIsStarting] = useState(false); const [isSubmittingOtp, setIsSubmittingOtp] = useState(false); + // DentaQuest allows flexible search - only DOB is required, plus at least one identifier + // Can use: memberId, firstName, lastName, or any combination + const hasAnyIdentifier = memberId || firstName || lastName; + const isDentaQuestFormIncomplete = !dateOfBirth || !hasAnyIdentifier; + // Clean up socket on unmount useEffect(() => { return () => { @@ -370,10 +375,13 @@ export function DentaQuestEligibilityButton({ }; const startDentaQuestEligibility = async () => { - if (!memberId || !dateOfBirth) { + // Flexible search - DOB required plus at least one identifier + const hasAnyIdentifier = memberId || firstName || lastName; + + if (!dateOfBirth || !hasAnyIdentifier) { toast({ title: "Missing fields", - description: "Member ID and Date of Birth are required.", + description: "Please provide Date of Birth and at least one of: Member ID, First Name, or Last Name.", variant: "destructive", }); return; @@ -538,7 +546,7 @@ export function DentaQuestEligibilityButton({