From 0e664e48132e565472e916323f245ac22fde90e9 Mon Sep 17 00:00:00 2001 From: Gitead Date: Fri, 22 May 2026 13:34:03 -0400 Subject: [PATCH] feat: add CCA claim submission with Selenium automation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CCA claim submit Selenium worker (login, fill form, attach docs, submit, capture dashboard PDF) - Add CCA fee schedule (procedureCodesMH.json renamed, procedureCodesCCA.json added with D6010) - Add backend route /api/claims/cca-claim, processor, and Selenium client - Wire CCA claim handler in claims-page with job tracking and PDF preview popup - Add insurance type dropdown in claim form (same options as eligibility page) - Auto-populate insurance type from patient.insuranceProvider in claim form and patient edit form - Map fee schedule by insurance type in Map Price button and combo buttons - Fix CCA login speed (remove fixed sleeps, use readyState check) - Fix CCA claim DOB format bug (was sending MM-DD-YYYY, now sends YYYY-MM-DD) - Fix npiProviderId not saved for CCA claims - Change Add Service → CCA Claim button (blue), MH → MH Claim Co-Authored-By: Claude Sonnet 4.6 --- apps/Backend/src/queue/jobRunner.ts | 12 + .../src/queue/processors/ccaClaimProcessor.ts | 153 +++ apps/Backend/src/queue/queues.ts | 2 + apps/Backend/src/routes/index.ts | 2 + .../src/routes/insuranceStatusCCAClaim.ts | 95 ++ .../src/services/seleniumCCAClaimClient.ts | 24 + apps/Frontend/src/App.jsx | 14 +- .../src/assets/data/procedureCodesCCA.json | 1196 +++++++++++++++++ .../src/assets/data/procedureCodesMH.json | 1191 ++++++++++++++++ .../src/components/claims/claim-form.tsx | 150 ++- .../src/components/patients/patient-form.jsx | 328 +++++ .../src/components/patients/patient-form.tsx | 56 +- apps/Frontend/src/lib/api/documents.js | 49 +- apps/Frontend/src/pages/appointments-page.tsx | 1 + apps/Frontend/src/pages/claims-page.tsx | 56 +- .../src/utils/procedureCombosMapping.js | 241 ++++ .../src/utils/procedureCombosMapping.ts | 33 +- apps/SeleniumService/agent.py | 44 + apps/SeleniumService/helpers_cca_claim.py | 216 +++ .../selenium_CCA_claimSubmitWorker.py | 977 ++++++++++++++ .../selenium_CCA_eligibilityCheckWorker.py | 5 +- 21 files changed, 4768 insertions(+), 77 deletions(-) create mode 100644 apps/Backend/src/queue/processors/ccaClaimProcessor.ts create mode 100644 apps/Backend/src/routes/insuranceStatusCCAClaim.ts create mode 100644 apps/Backend/src/services/seleniumCCAClaimClient.ts create mode 100644 apps/Frontend/src/assets/data/procedureCodesCCA.json create mode 100755 apps/Frontend/src/assets/data/procedureCodesMH.json create mode 100644 apps/Frontend/src/components/patients/patient-form.jsx create mode 100644 apps/Frontend/src/utils/procedureCombosMapping.js create mode 100644 apps/SeleniumService/helpers_cca_claim.py create mode 100644 apps/SeleniumService/selenium_CCA_claimSubmitWorker.py diff --git a/apps/Backend/src/queue/jobRunner.ts b/apps/Backend/src/queue/jobRunner.ts index dc5ba5cf..973f3cf7 100644 --- a/apps/Backend/src/queue/jobRunner.ts +++ b/apps/Backend/src/queue/jobRunner.ts @@ -16,6 +16,7 @@ import { runDeltaInsEligibilityProcessor } from "./processors/deltaInsEligibilit import { runUnitedSCOEligibilityProcessor } from "./processors/unitedSCOEligibilityProcessor"; import { runDentaQuestEligibilityProcessor } from "./processors/dentaQuestEligibilityProcessor"; import { runCCAEligibilityProcessor } from "./processors/ccaEligibilityProcessor"; +import { runCCAClaimProcessor } from "./processors/ccaClaimProcessor"; import { runEligibilityHistoryProcessor } from "./processors/eligibilityHistoryProcessor"; import { runCmspEligibilityHistoryRemainingProcessor } from "./processors/cmspEligibilityHistoryRemainingProcessor"; import type { SeleniumJobData, OcrJobData } from "./queues"; @@ -132,6 +133,17 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string { job.id ); } + if (jobType === "cca-claim-submit") { + return runCCAClaimProcessor( + { + enrichedPayload: data.enrichedPayload, + userId: data.userId, + claimId: data.claimId, + socketId: data.socketId, + }, + job.id + ); + } if (jobType === "cca-eligibility-check") { return runCCAEligibilityProcessor( { diff --git a/apps/Backend/src/queue/processors/ccaClaimProcessor.ts b/apps/Backend/src/queue/processors/ccaClaimProcessor.ts new file mode 100644 index 00000000..c801e084 --- /dev/null +++ b/apps/Backend/src/queue/processors/ccaClaimProcessor.ts @@ -0,0 +1,153 @@ +/** + * Processor for "cca-claim-submit" jobs. + * Submits a dental claim to CCA via the ScionDental portal. + * + * Flow: + * 1. POST /cca-claim to Python agent → get session_id + * 2. Emit selenium:cca_claim_started to frontend + * 3. Poll until completed/error + * 4. Emit result and update claim status in DB + */ +import { storage } from "../../storage"; +import { + forwardToSeleniumCCAClaimAgent, + getSeleniumCCAClaimSessionStatus, +} from "../../services/seleniumCCAClaimClient"; +import { io } from "../../socket"; + +function log(tag: string, msg: string, ctx?: any) { + console.log(`${new Date().toISOString()} [${tag}] ${msg}`, ctx ?? ""); +} + +function emitToSocket(socketId: string | undefined, event: string, payload: any) { + if (!socketId || !io) return; + try { + const socket = io.sockets.sockets.get(socketId); + if (socket) socket.emit(event, payload); + } catch (_) {} +} + +async function pollUntilDone( + sessionId: string, + pollTimeoutMs = 10 * 60 * 1000 +): Promise { + const maxAttempts = 1200; + const pollIntervalMs = 500; + const maxTransientErrors = 12; + let transientErrors = 0; + const deadline = Date.now() + pollTimeoutMs; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + if (Date.now() > deadline) { + throw new Error(`CCA claim polling timeout for session ${sessionId}`); + } + try { + const st = await getSeleniumCCAClaimSessionStatus(sessionId); + const status: string = st?.status ?? "unknown"; + log("cca-claim-processor", `poll attempt=${attempt}`, { sessionId, status }); + transientErrors = 0; + + if (status === "completed") return st.result; + if (status === "error" || status === "not_found") { + throw new Error(st?.message || `CCA claim session ended with status: ${status}`); + } + await new Promise((r) => setTimeout(r, pollIntervalMs)); + } catch (err: any) { + const isTerminal = + err?.response?.status === 404 || + (typeof err?.message === "string" && + (err.message.includes("not_found") || err.message.includes("polling timeout"))); + if (isTerminal) throw err; + transientErrors++; + if (transientErrors > maxTransientErrors) { + throw new Error(`Too many transient errors polling CCA claim session ${sessionId}`); + } + const backoff = Math.min(30_000, 500 * Math.pow(2, transientErrors - 1)); + await new Promise((r) => setTimeout(r, backoff)); + } + } + throw new Error(`CCA claim polling exhausted all attempts for session ${sessionId}`); +} + +export interface CCAClaimProcessorInput { + enrichedPayload: any; + userId: number; + claimId?: number; + socketId?: string; +} + +export async function runCCAClaimProcessor( + input: CCAClaimProcessorInput, + jobId: string +): Promise<{ status: string; claimNumber?: string | null; pdfFileId?: number | null }> { + const { enrichedPayload, userId, claimId, socketId } = input; + + log("cca-claim-processor", "starting Python agent session", { claimId }); + const agentResp = await forwardToSeleniumCCAClaimAgent(enrichedPayload); + + if (!agentResp?.session_id) { + throw new Error("Python agent did not return a session_id for CCA claim"); + } + + const sessionId = agentResp.session_id as string; + log("cca-claim-processor", "got session_id", { sessionId }); + + emitToSocket(socketId, "selenium:cca_claim_started", { session_id: sessionId, jobId }); + + const seleniumResult = await pollUntilDone(sessionId); + + if (!seleniumResult || seleniumResult.status === "error") { + throw new Error(seleniumResult?.message ?? "CCA claim session returned an error"); + } + + const claimNumber: string | null = seleniumResult?.claimNumber ?? null; + const pdfBase64: string = seleniumResult?.pdfBase64 ?? ""; + const pdfFilename: string = + seleniumResult?.pdfFilename || `cca_claim_${claimId ?? "unknown"}_${Date.now()}.pdf`; + + // Save PDF to patient's Claims document group + let pdfFileId: number | null = null; + if (pdfBase64 && enrichedPayload?.claim?.patientId) { + try { + const patientId = Number(enrichedPayload.claim.patientId); + const pdfBuffer = Buffer.from(pdfBase64, "base64"); + let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); + if (!group) { + group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM"); + } + const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer); + if (created && typeof created === "object" && "id" in created) { + pdfFileId = Number((created as any).id); + } + log("cca-claim-processor", "PDF saved", { pdfFilename, pdfFileId, patientId }); + } catch (e: any) { + log("cca-claim-processor", "failed to save PDF", { error: e?.message }); + } + } + + // Update claim: status → REVIEW, persist claimNumber + if (claimId) { + try { + const updates: Record = { status: "REVIEW" }; + if (claimNumber) updates.claimNumber = claimNumber; + await storage.updateClaim(claimId, updates); + log("cca-claim-processor", "claim updated", { claimId, claimNumber, status: "REVIEW" }); + } catch (e: any) { + log("cca-claim-processor", "failed to update claim", { error: e?.message }); + } + } + + emitToSocket(socketId, "selenium:cca_claim_completed", { + jobId, + claimId, + claimNumber, + pdfFileId, + pdfFilename, + message: claimNumber + ? `CCA claim submitted — claim number: ${claimNumber}` + : "CCA claim submitted successfully", + }); + + log("cca-claim-processor", "done", { claimId, claimNumber }); + return { status: "success", claimNumber, pdfFileId }; +} diff --git a/apps/Backend/src/queue/queues.ts b/apps/Backend/src/queue/queues.ts index 8005dc31..c3beac45 100644 --- a/apps/Backend/src/queue/queues.ts +++ b/apps/Backend/src/queue/queues.ts @@ -11,6 +11,8 @@ export type SeleniumJobType = | "deltains-eligibility-check" | "unitedsco-eligibility-check" | "cca-eligibility-check" + | "cca-claim-submit" + | "tuftssco-eligibility-check" | "mh-eligibility-history-check" | "cmsp-eligibility-history-remaining-check"; diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts index 0b7ac8ce..edb24c38 100755 --- a/apps/Backend/src/routes/index.ts +++ b/apps/Backend/src/routes/index.ts @@ -16,6 +16,7 @@ import insuranceStatusDeltaInsRoutes from "./insuranceStatusDeltaIns"; import insuranceStatusUnitedSCORoutes from "./insuranceStatusUnitedSCO"; import insuranceStatusTuftsSCORoutes from "./insuranceStatusTuftsSCO"; import insuranceStatusCCARoutes from "./insuranceStatusCCA"; +import insuranceStatusCCAClaimRoutes from "./insuranceStatusCCAClaim"; import paymentsRoutes from "./payments"; import databaseManagementRoutes from "./database-management"; import notificationsRoutes from "./notifications"; @@ -53,6 +54,7 @@ router.use("/insurance-status-deltains", insuranceStatusDeltaInsRoutes); router.use("/insurance-status-unitedsco", insuranceStatusUnitedSCORoutes); router.use("/insurance-status-tuftssco", insuranceStatusTuftsSCORoutes); router.use("/insurance-status-cca", insuranceStatusCCARoutes); +router.use("/claims", insuranceStatusCCAClaimRoutes); router.use("/payments", paymentsRoutes); router.use("/database-management", databaseManagementRoutes); router.use("/notifications", notificationsRoutes); diff --git a/apps/Backend/src/routes/insuranceStatusCCAClaim.ts b/apps/Backend/src/routes/insuranceStatusCCAClaim.ts new file mode 100644 index 00000000..21fca66e --- /dev/null +++ b/apps/Backend/src/routes/insuranceStatusCCAClaim.ts @@ -0,0 +1,95 @@ +import { Router, Request, Response } from "express"; +import { storage } from "../storage"; +import { enqueueSeleniumJob } from "../queue/jobRunner"; +import multer from "multer"; + +const router = Router(); +const upload = multer({ storage: multer.memoryStorage() }); + +/** + * POST /cca-claim + * + * Enqueues a CCA claim submission job. + * Accepts multipart/form-data with optional pdfs/images attachments. + * + * Body fields: + * data — JSON string with claim payload (memberId, dateOfBirth, serviceDate, + * serviceLines, patientName, etc.) + * socketId — socket.io client id + * claimId — existing claim DB id (optional) + * + * Response: { status: "queued", jobId: "…" } + */ +router.post( + "/cca-claim", + upload.fields([ + { name: "pdfs", maxCount: 10 }, + { name: "images", maxCount: 10 }, + ]), + async (req: Request, res: Response): Promise => { + if (!req.user?.id) { + return res.status(401).json({ error: "Unauthorized: user info missing" }); + } + + try { + const claimData = + typeof req.body.data === "string" + ? JSON.parse(req.body.data) + : req.body.data ?? {}; + + const pdfs = + (req.files as Record)?.pdfs ?? []; + const images = + (req.files as Record)?.images ?? []; + + // Fetch CCA credentials + const credentials = await storage.getInsuranceCredentialByUserAndSiteKey( + req.user.id, + "CCA" + ); + if (!credentials) { + return res.status(404).json({ + error: + "No CCA credentials found. Please add them on the Settings page.", + }); + } + + const filesForQueue = [...pdfs, ...images].map((f) => ({ + originalname: f.originalname, + bufferBase64: f.buffer.toString("base64"), + mimetype: f.mimetype, + })); + + const enrichedPayload = { + claim: { + ...claimData, + cca_username: credentials.username, + cca_password: credentials.password, + }, + files: filesForQueue, + }; + + const socketId: string | undefined = req.body.socketId; + const claimId: number | undefined = claimData.claimId + ? Number(claimData.claimId) + : undefined; + + const jobId = enqueueSeleniumJob({ + jobType: "cca-claim-submit", + userId: req.user.id, + socketId, + enrichedPayload, + claimId, + }); + + return res.json({ status: "queued", jobId }); + } catch (err: any) { + console.error("[cca-claim route] error:", err); + return res.status(500).json({ + error: err.message || "Failed to enqueue CCA claim job", + }); + } + } +); + +export default router; diff --git a/apps/Backend/src/services/seleniumCCAClaimClient.ts b/apps/Backend/src/services/seleniumCCAClaimClient.ts new file mode 100644 index 00000000..3238c17d --- /dev/null +++ b/apps/Backend/src/services/seleniumCCAClaimClient.ts @@ -0,0 +1,24 @@ +import axios from "axios"; + +const SELENIUM_BASE = process.env.SELENIUM_SERVICE_URL ?? "http://localhost:5002"; + +/** + * POST /cca-claim + * Returns { status: "started", session_id: "" } + */ +export async function forwardToSeleniumCCAClaimAgent( + data: Record +): Promise<{ status: string; session_id: string }> { + const resp = await axios.post(`${SELENIUM_BASE}/cca-claim`, data); + return resp.data; +} + +/** + * GET /session/{sid}/status + */ +export async function getSeleniumCCAClaimSessionStatus( + sessionId: string +): Promise> { + const resp = await axios.get(`${SELENIUM_BASE}/session/${sessionId}/status`); + return resp.data; +} diff --git a/apps/Frontend/src/App.jsx b/apps/Frontend/src/App.jsx index 4d93f807..54d5c67e 100755 --- a/apps/Frontend/src/App.jsx +++ b/apps/Frontend/src/App.jsx @@ -22,6 +22,10 @@ const DocumentPage = lazy(() => import("./pages/documents-page")); const DatabaseManagementPage = lazy(() => import("./pages/database-management-page")); const ReportsPage = lazy(() => import("./pages/reports-page")); const CloudStoragePage = lazy(() => import("./pages/cloud-storage-page")); +const JobMonitorPage = lazy(() => import("./pages/job-monitor-page")); +const ChartPage = lazy(() => import("./pages/chart-page")); +const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page")); +const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page")); const NotFound = lazy(() => import("./pages/not-found")); function Router() { return ( @@ -31,14 +35,20 @@ function Router() { }/> }/> }/> - }/> + }/> + }/> + } adminOnly/> + } adminOnly/> }/> }/> }/> }/> - }/> + } adminOnly/> }/> }/> + }/> + }/> + } adminOnly/> }/> }/> ); diff --git a/apps/Frontend/src/assets/data/procedureCodesCCA.json b/apps/Frontend/src/assets/data/procedureCodesCCA.json new file mode 100644 index 00000000..7f5516c1 --- /dev/null +++ b/apps/Frontend/src/assets/data/procedureCodesCCA.json @@ -0,0 +1,1196 @@ +[ + { + "Procedure Code": "D0120", + "Description": "Periodic oral evaluation - established patient", + "PriceLTEQ21": "24", + "PriceGT21": "24" + }, + { + "Procedure Code": "D0140", + "Description": "Limited oral evaluation - problem focused", + "PriceLTEQ21": "43", + "PriceGT21": "43" + }, + { + "Procedure Code": "D0145", + "Description": "Oral evaluation for a patient under three years of age and counseling with primary caregiver", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D0150", + "Description": "Comprehensive oral evaluation - new or established patient", + "PriceLTEQ21": "41", + "PriceGT21": "41" + }, + { + "Procedure Code": "D0180", + "Description": "Comprehensive periodontal evaluation - new or established patient", + "PriceLTEQ21": "37", + "PriceGT21": "37" + }, + { + "Procedure Code": "D0190", + "Description": "Screening of a patient (PHDH only)", + "PriceLTEQ21": "20", + "PriceGT21": "20" + }, + { + "Procedure Code": "D0191", + "Description": "Assessment of a patient (PHDH only)", + "PriceLTEQ21": "20", + "PriceGT21": "20" + }, + { + "Procedure Code": "D0210", + "Description": "Intraoral - complete series of radiographic images", + "PriceLTEQ21": "76", + "PriceGT21": "76" + }, + { + "Procedure Code": "D0220", + "Description": "Intraoral - periapical, first radiographic image", + "PriceLTEQ21": "15", + "PriceGT21": "15" + }, + { + "Procedure Code": "D0230", + "Description": "Intraoral - periapical, each additional radiographic image", + "PriceLTEQ21": "13", + "PriceGT21": "13" + }, + { + "Procedure Code": "D0240", + "Description": "Intraoral - occlusal radiographic image", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D0270", + "Description": "Bitewing - single radiographic image", + "PriceLTEQ21": "14", + "PriceGT21": "14" + }, + { + "Procedure Code": "D0272", + "Description": "Bitewings - two radiographic images", + "PriceLTEQ21": "25", + "PriceGT21": "25" + }, + { + "Procedure Code": "D0273", + "Description": "Bitewings - three radiographic images", + "PriceLTEQ21": "27", + "PriceGT21": "27" + }, + { + "Procedure Code": "D0274", + "Description": "Bitewings - four radiographic images", + "PriceLTEQ21": "36", + "PriceGT21": "36" + }, + { + "Procedure Code": "D0330", + "Description": "Panoramic radiographic image", + "PriceLTEQ21": "69", + "PriceGT21": "69" + }, + { + "Procedure Code": "D0340", + "Description": "Cephalometric radiograph image (Oral surgeon only)", + "PriceLTEQ21": "74", + "PriceGT21": "74" + }, + { + "Procedure Code": "D0364", + "Description": "Less than one jaw", + "Price": "350" + }, + { + "Procedure Code": "D0365", + "Description": "Mand", + "Price": "350" + }, + { + "Procedure Code": "D0366", + "Description": "Max", + "Price": "350" + }, + { + "Procedure Code": "D0367", + "Description": "", + "Price": "400" + }, + { + "Procedure Code": "D0368", + "Description": "include TMJ", + "Price": "375" + }, + { + "Procedure Code": "D0380", + "Description": "Less than one jaw", + "Price": "300" + }, + { + "Procedure Code": "D0381", + "Description": "Mand", + "Price": "300" + }, + { + "Procedure Code": "D0382", + "Description": "Max", + "Price": "300" + }, + { + "Procedure Code": "D0383", + "Description": "", + "Price": "350" + }, + { + "Procedure Code": "D1110", + "Description": "Prophylaxis \u2013 adult, 14 yo or older", + "PriceLTEQ21": "60", + "PriceGT21": "60" + }, + { + "Procedure Code": "D1120", + "Description": "Prophylaxis \u2013 child, 0-13 yo", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1206", + "Description": "Topical application of fluoride varnish", + "PriceLTEQ21": "26", + "PriceGT21": "26" + }, + { + "Procedure Code": "D1208", + "Description": "Topical application of fluoride \u2013 excluding varnish", + "PriceLTEQ21": "29", + "PriceGT21": "29" + }, + { + "Procedure Code": "D1351", + "Description": "Sealant \u2013 per tooth", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1354", + "Description": "Application of caries arresting medicament - per tooth", + "PriceLTEQ21": "15", + "PriceGT21": "15" + }, + { + "Procedure Code": "D1510", + "Description": "Space maintainer \u2013 fixed,unilateral \u2013 per quadrant", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1516", + "Description": "Space maintainer- fixed- bilateral, maxillary", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1517", + "Description": "Space maintainer- fixed- bilateral, mandibular", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1520", + "Description": "Space maintainer \u2013 removable- unilateral- per quadrant", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1526", + "Description": "Space maintainer- removable- bilateral, maxillary", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1527", + "Description": "Space maintainer- removable- bilateral, mandibular", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1575", + "Description": "Distal shoe space maintainer - fixed- unilateral- Per Quadrant I.C", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1701", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration \u2013 first dose SARSCOV2 COVID-19 VAC mRNA 30mcg/0.3mL IM DOSE 1", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1702", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration \u2013 second dose SARSCOV2 COVID-19 VAC mRNA 30mcg/0.3mL IM DOSE 2", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1707", + "Description": "Janssen Covid-19 vaccine administration SARSCOV2 COVID-19 VAC Ad26 5x1010 VP/.5mL IM SINGLE DOSE", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1708", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration \u2013 third dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1709", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration \u2013 booster dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1712", + "Description": "Janssen Covid-19 vaccine administration - booster dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1713", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration tris-sucrose pediatric \u2013 first dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1714", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration tris-sucrose pediatric \u2013 second dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1999", + "Description": "", + "Price": "50" + }, + { + "Procedure Code": "D2140", + "Description": "Amalgam-one surface, primary or permanent", + "PriceLTEQ21": "62", + "PriceGT21": "62" + }, + { + "Procedure Code": "D2150", + "Description": "Amalgam-two surfaces, primary or permanent", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D2955", + "Description": "post renoval", + "Price": "350" + }, + { + "Procedure Code": "D4910", + "Description": "perio maintains", + "Price": "250" + }, + { + "Procedure Code": "D5510", + "Description": "Repair broken complete denture base (QUAD)", + "Price": "400" + }, + { + "Procedure Code": "D6010", + "Description": "Surgical placement of implant body", + "Price": "1600" + }, + { + "Procedure Code": "D6056", + "Description": "pre fab abut", + "Price": "750" + }, + { + "Procedure Code": "D6057", + "Description": "custom abut", + "Price": "800" + }, + { + "Procedure Code": "D6058", + "Description": "porcelain, implant crown, ceramic crown", + "Price": "1400" + }, + { + "Procedure Code": "D6059", + "Description": "", + "Price": "1400" + }, + { + "Procedure Code": "D6100", + "Description": "", + "Price": "320" + }, + { + "Procedure Code": "D6110", + "Description": "implant", + "Price": "1600" + }, + { + "Procedure Code": "D6242", + "Description": "noble metal. For united", + "Price": "1400" + }, + { + "Procedure Code": "D6245", + "Description": "porcelain, not for united", + "Price": "1400" + }, + { + "Procedure Code": "D7910", + "Description": "suture, small wound up to 5 mm", + "Price": "400" + }, + { + "Procedure Code": "D7950", + "Description": "max", + "Price": "800" + }, + { + "Procedure Code": "D2160", + "Description": "Amalgam-three surfaces, primary or permanent", + "PriceLTEQ21": "92", + "PriceGT21": "92" + }, + { + "Procedure Code": "D2161", + "Description": "Amalgam-four or more surfaces, primary or permanent", + "PriceLTEQ21": "116", + "PriceGT21": "116" + }, + { + "Procedure Code": "D2330", + "Description": "Resin-based composite \u2013 one surface, anterior", + "PriceLTEQ21": "72", + "PriceGT21": "72" + }, + { + "Procedure Code": "D2331", + "Description": "Resin-based composite \u2013 two surfaces, anterior", + "PriceLTEQ21": "92", + "PriceGT21": "92" + }, + { + "Procedure Code": "D2332", + "Description": "Resin-based composite \u2013 three surfaces, anterior", + "PriceLTEQ21": "116", + "PriceGT21": "116" + }, + { + "Procedure Code": "D2335", + "Description": "Resin-based composite \u2013 four or more surfaces or involving incisal angle (anterior)", + "PriceLTEQ21": "146", + "PriceGT21": "146" + }, + { + "Procedure Code": "D2390", + "Description": "Resin-based composite crown, anterior", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2391", + "Description": "Resin-based composite \u2013 one surface, posterior", + "PriceLTEQ21": "62", + "PriceGT21": "62" + }, + { + "Procedure Code": "D2392", + "Description": "Resin-based composite \u2013 two surfaces, posterior", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D2393", + "Description": "Resin-based composite \u2013 three surfaces, posterior", + "PriceLTEQ21": "92", + "PriceGT21": "92" + }, + { + "Procedure Code": "D2394", + "Description": "Resin-based composite \u2013 four or more surfaces, posterior", + "PriceLTEQ21": "116", + "PriceGT21": "116" + }, + { + "Procedure Code": "D2710", + "Description": "Crown \u2013 resin-based composite (indirect)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2740", + "Description": "Crown \u2013 porcelain/ceramic", + "PriceLTEQ21": "729", + "PriceGT21": "729" + }, + { + "Procedure Code": "D2750", + "Description": "Crown \u2013 porcelain fused to high noble metal", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2751", + "Description": "Crown \u2013 porcelain fused to predominantly base metal", + "PriceLTEQ21": "613", + "PriceGT21": "613" + }, + { + "Procedure Code": "D2752", + "Description": "Crown \u2013 porcelain fused to noble metal", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2790", + "Description": "Crown \u2013 full cast high noble metal", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2910", + "Description": "Re-cement or re-bond inlay, onlay or partial coverage restoration", + "PriceLTEQ21": "57", + "PriceGT21": "57" + }, + { + "Procedure Code": "D2920", + "Description": "Re-cement or re-bond crown", + "PriceLTEQ21": "57", + "PriceGT21": "57" + }, + { + "Procedure Code": "D2929", + "Description": "Prefabricated porcelain/ceramic crown \u2013 primary tooth", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2930", + "Description": "Prefabricated stainless steel crown \u2013 primary tooth", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2931", + "Description": "Prefabricated stainless steel crown \u2013 permanent tooth", + "PriceLTEQ21": "171", + "PriceGT21": "171" + }, + { + "Procedure Code": "D2932", + "Description": "Prefabricated resin crown", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2934", + "Description": "Prefabricated esthetic coated stainless steel crown \u2013 primary tooth", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2950", + "Description": "Core buildup, including any pins when required", + "PriceLTEQ21": "164", + "PriceGT21": "164" + }, + { + "Procedure Code": "D2951", + "Description": "Pin retention \u2013 per tooth, in addition to restoration", + "PriceLTEQ21": "27", + "PriceGT21": "27" + }, + { + "Procedure Code": "D2954", + "Description": "Prefabricated post and core in addition to crown", + "PriceLTEQ21": "191", + "PriceGT21": "191" + }, + { + "Procedure Code": "D2980", + "Description": "Crown repair necessitated by restorative material failure", + "PriceLTEQ21": "115", + "PriceGT21": "115" + }, + { + "Procedure Code": "D2999", + "Description": "Unspecified restorative procedure, by report", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + }, + { + "Procedure Code": "D3120", + "Description": "Pulp cap \u2013 indirect (excluding final restoration)", + "PriceLTEQ21": "34", + "PriceGT21": "34" + }, + { + "Procedure Code": "D3220", + "Description": "Therapeutic pulpotomy (excluding final restoration) \u2013 removal of pulp coronal to the dentinocemental junction and application of medicament", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D3310", + "Description": "Endodontic therapy, anterior (excluding final restoration)", + "PriceLTEQ21": "544", + "PriceGT21": "544" + }, + { + "Procedure Code": "D3320", + "Description": "Endodontic therapy, premolar tooth (excluding final restoration)", + "PriceLTEQ21": "639", + "PriceGT21": "639" + }, + { + "Procedure Code": "D3330", + "Description": "Endodontic therapy, molar tooth (excluding final restoration)", + "PriceLTEQ21": "829", + "PriceGT21": "829" + }, + { + "Procedure Code": "D3346", + "Description": "Retreatment of previous root canal therapy \u2013 anterior", + "PriceLTEQ21": "456", + "PriceGT21": "456" + }, + { + "Procedure Code": "D3347", + "Description": "Retreatment of previous root canal therapy \u2013 premolar", + "PriceLTEQ21": "538", + "PriceGT21": "538" + }, + { + "Procedure Code": "D3348", + "Description": "Retreatment of previous root canal therapy \u2013 molar", + "PriceLTEQ21": "613", + "PriceGT21": "613" + }, + { + "Procedure Code": "D3410", + "Description": "Apicoectomy \u2013 anterior", + "PriceLTEQ21": "407", + "PriceGT21": "407" + }, + { + "Procedure Code": "D3421", + "Description": "Apicoectomy \u2013 premolar (first root)", + "PriceLTEQ21": "460", + "PriceGT21": "460" + }, + { + "Procedure Code": "D3425", + "Description": "Apicoectomy \u2013 molar (first root)", + "PriceLTEQ21": "598", + "PriceGT21": "598" + }, + { + "Procedure Code": "D3426", + "Description": "Apicoectomy (each additional root)", + "PriceLTEQ21": "230", + "PriceGT21": "230" + }, + { + "Procedure Code": "D4210", + "Description": "Gingivectomy or gingivoplasty - Four or more contiguous teeth or bounded teeth spaces per quadrant", + "PriceLTEQ21": "307", + "PriceGT21": "307" + }, + { + "Procedure Code": "D4211", + "Description": "Gingivectomy or gingivoplasty - one to three contiguous teeth or bounded teeth spaces per quadrant", + "PriceLTEQ21": "111", + "PriceGT21": "111" + }, + { + "Procedure Code": "D4341", + "Description": "Periodontal scaling and root planing - four or more teeth per quadrant", + "PriceLTEQ21": "134", + "PriceGT21": "134" + }, + { + "Procedure Code": "D4342", + "Description": "Periodontal scaling and root planing - one to three teeth, per quadrant", + "PriceLTEQ21": "90", + "PriceGT21": "90" + }, + { + "Procedure Code": "D4346", + "Description": "Scaling in presence of generalized moderate or severe gingival inflammation \u2013 full mouth, after oral evaluation", + "PriceLTEQ21": "60", + "PriceGT21": "60" + }, + { + "Procedure Code": "D5110", + "Description": "Complete denture \u2013 maxillary", + "PriceLTEQ21": "730", + "PriceGT21": "730" + }, + { + "Procedure Code": "D5120", + "Description": "Complete denture \u2013 mandibular", + "PriceLTEQ21": "730", + "PriceGT21": "730" + }, + { + "Procedure Code": "D5130", + "Description": "Immediate denture \u2013 maxillary", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5140", + "Description": "Immediate denture - mandibular", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5211", + "Description": "Maxillary partial denture - resin base (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "556", + "PriceGT21": "556" + }, + { + "Procedure Code": "D5212", + "Description": "Mandibular partial denture - resin base (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "595", + "PriceGT21": "595" + }, + { + "Procedure Code": "D5213", + "Description": "Maxillary partial denture- cast metal framework with resin denture bases (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5214", + "Description": "Mandibular partial denture - cast metal framework with resin denture bases (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5225", + "Description": "Maxillary partial denture- flexible base", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5226", + "Description": "Mandibular partial denture- flexible base", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5511", + "Description": "Repair broken complete denture base, mandibular", + "PriceLTEQ21": "85", + "PriceGT21": "85" + }, + { + "Procedure Code": "D5512", + "Description": "Repair broken complete denture base, maxillary", + "PriceLTEQ21": "85", + "PriceGT21": "85" + }, + { + "Procedure Code": "D5520", + "Description": "Replace missing or broken teeth - complete denture (each tooth)", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5611", + "Description": "Repair broken resin partial denture base, mandibular", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5612", + "Description": "Repair broken resin partial denture base, maxillary", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5621", + "Description": "Repair broken cast partial denture base, mandibular", + "PriceLTEQ21": "104", + "PriceGT21": "104" + }, + { + "Procedure Code": "D5622", + "Description": "Repair broken cast partial denture base, maxillary", + "PriceLTEQ21": "104", + "PriceGT21": "104" + }, + { + "Procedure Code": "D5630", + "Description": "Repair or replace broken retentive/clasping materials \u2013 per tooth", + "PriceLTEQ21": "99", + "PriceGT21": "99" + }, + { + "Procedure Code": "D5640", + "Description": "Replace broken teeth - per tooth", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5650", + "Description": "Add tooth to existing partial denture", + "PriceLTEQ21": "92", + "PriceGT21": "92" + }, + { + "Procedure Code": "D5660", + "Description": "Add clasp to existing partial denture per tooth", + "PriceLTEQ21": "98", + "PriceGT21": "98" + }, + { + "Procedure Code": "D5730", + "Description": "Reline complete maxillary denture (direct)", + "PriceLTEQ21": "158", + "PriceGT21": "158" + }, + { + "Procedure Code": "D5731", + "Description": "Reline lower complete mandibular denture (direct)", + "PriceLTEQ21": "173", + "PriceGT21": "173" + }, + { + "Procedure Code": "D5740", + "Description": "Reline maxillary partial denture(chairside)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5741", + "Description": "Reline mandibular partial denture(chairside)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5750", + "Description": "Reline complete maxillary denture (indirect)", + "PriceLTEQ21": "214", + "PriceGT21": "214" + }, + { + "Procedure Code": "D5751", + "Description": "Reline complete mandibular denture (indirect)", + "PriceLTEQ21": "215", + "PriceGT21": "215" + }, + { + "Procedure Code": "D5760", + "Description": "Reline maxillary partial denture (laboratory)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5761", + "Description": "Reline mandibular partial denture (laboratory)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6241", + "Description": "Pontic-porcelain fused metal", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6751", + "Description": "Retainer crown-porcelain fused to metal", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6930", + "Description": "Re-cement or re-bond fixed partial denture", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6980", + "Description": "Fixed partial denture repair", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6999", + "Description": "Fixed prosthodontic procedure", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + }, + { + "Procedure Code": "D7111", + "Description": "Extraction, coronal remnants - primary tooth", + "PriceLTEQ21": "75", + "PriceGT21": "75" + }, + { + "Procedure Code": "D7140", + "Description": "Extraction, erupted tooth or exposed root (elevation and/or forceps removal)", + "PriceLTEQ21": "77", + "PriceGT21": "77" + }, + { + "Procedure Code": "D7210", + "Description": "Extraction, erupted tooth requiring removal of bone and/or sectioning of tooth, and including elevation of mucoperiosteal flap if indicated", + "PriceLTEQ21": "149", + "PriceGT21": "149" + }, + { + "Procedure Code": "D7220", + "Description": "Removal of impacted tooth - soft tissue", + "PriceLTEQ21": "191", + "PriceGT21": "191" + }, + { + "Procedure Code": "D7230", + "Description": "Removal of impacted tooth - partially bony", + "PriceLTEQ21": "249", + "PriceGT21": "249" + }, + { + "Procedure Code": "D7240", + "Description": "Removal of impacted tooth - completely bony", + "PriceLTEQ21": "295", + "PriceGT21": "295" + }, + { + "Procedure Code": "D7250", + "Description": "Surgical removal of residual tooth roots (cutting procedure)", + "PriceLTEQ21": "144", + "PriceGT21": "144" + }, + { + "Procedure Code": "D7251", + "Description": "Coronectomy- intentional partial tooth removal, impacted teeth only", + "PriceLTEQ21": "134", + "PriceGT21": "134" + }, + { + "Procedure Code": "D7270", + "Description": "Tooth reimplantation and/or stabilization of accidentally evulsed or displaced tooth", + "PriceLTEQ21": "106", + "PriceGT21": "106" + }, + { + "Procedure Code": "D7280", + "Description": "Surgical access of an unerupted tooth", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D7283", + "Description": "Placement of device to facilitate eruption of impacted tooth", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D7310", + "Description": "Alveoloplasty in conjunction with extractions-four or more teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "142", + "PriceGT21": "142" + }, + { + "Procedure Code": "D7311", + "Description": "Alveoloplasty in conjunction with extractions - one to three teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "128", + "PriceGT21": "128" + }, + { + "Procedure Code": "D7320", + "Description": "Alveoloplasty not in conjunction with extractions- four or more teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "187", + "PriceGT21": "187" + }, + { + "Procedure Code": "D7321", + "Description": "Alveoloplasty not in conjunction with extractions - one to three teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "149", + "PriceGT21": "149" + }, + { + "Procedure Code": "D7340", + "Description": "Vestibuloplasty - ridge extension (second epithelialization)", + "PriceLTEQ21": "747", + "PriceGT21": "747" + }, + { + "Procedure Code": "D7350", + "Description": "Vestibuloplasty - ridge extension (Oral surgeon only)", + "PriceLTEQ21": "943", + "PriceGT21": "943" + }, + { + "Procedure Code": "D7410", + "Description": "Radical excision - lesion diameter up to 1.25cm", + "PriceLTEQ21": "115", + "PriceGT21": "115" + }, + { + "Procedure Code": "D7411", + "Description": "Excision of benign lesion greater than 1.25 cm", + "PriceLTEQ21": "208", + "PriceGT21": "208" + }, + { + "Procedure Code": "D7450", + "Description": "Removal of benign odontogenic cyst or tumor - lesion diameter up to 1.25 cm", + "PriceLTEQ21": "248", + "PriceGT21": "248" + }, + { + "Procedure Code": "D7451", + "Description": "Removal of benign odontogenic cyst or tumor - lesion diameter greater than 1.25 cm", + "PriceLTEQ21": "288", + "PriceGT21": "288" + }, + { + "Procedure Code": "D7460", + "Description": "Removal of benign nonodontogenic cyst or tumor - lesion diameter up to 1.25 cm", + "PriceLTEQ21": "121", + "PriceGT21": "121" + }, + { + "Procedure Code": "D7461", + "Description": "Removal of benign nonodontogenic cyst or tumor - lesion diameter greater than 1.25 cm", + "PriceLTEQ21": "143", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7471", + "Description": "Removal of lateral exostosis (maxilla or mandible) (Oral surgeon only)", + "PriceLTEQ21": "143", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7472", + "Description": "Removal of torus palatinus (Oral surgeon only)", + "PriceLTEQ21": "143", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7473", + "Description": "Removal of torus mandibularis (Oral surgeon only)", + "PriceLTEQ21": "143", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7961", + "Description": "Buccal/labial frenectomy (frenulectomy)", + "PriceLTEQ21": "107", + "PriceGT21": "107" + }, + { + "Procedure Code": "D7962", + "Description": "Lingual frenectomy (frenulectomy)", + "PriceLTEQ21": "107", + "PriceGT21": "107" + }, + { + "Procedure Code": "D7963", + "Description": "Frenuloplasty", + "PriceLTEQ21": "416", + "PriceGT21": "416" + }, + { + "Procedure Code": "D7970", + "Description": "Excision of hyperplastic tissue - per arch", + "PriceLTEQ21": "246", + "PriceGT21": "246" + }, + { + "Procedure Code": "D7999", + "Description": "Unspecified oral surgery procedure, by report", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + }, + { + "Procedure Code": "D8010", + "Description": "Limited orthodontic treamtnent of the primary transition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8020", + "Description": "Limited orthodontic treatment of the transitional dentition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8030", + "Description": "Limited orthodontic treatment of the adolescent dentition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8040", + "Description": "Limited orthodontic treatment of the adult dentition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8070", + "Description": "Comprehensive orthodontic treatment of the transitional dentition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8080", + "Description": "Comprehensive orthodontic treatment of the adolescent dentition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8090", + "Description": "Comprehensive orthodontic treatment of the adult dentition (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8660", + "Description": "Pre-orthodontic treatment examination to monitor growth and development (records fee) (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8670", + "Description": "Periodic orthodontic treatment visit (Orthodontist only)", + "PriceLTEQ21": "215", + "PriceGT21": "215" + }, + { + "Procedure Code": "D8680", + "Description": "Orthodontic retention (removal of appliances, construction and placement of retainer(s)) (Orthodontist only)", + "PriceLTEQ21": "85", + "PriceGT21": "85" + }, + { + "Procedure Code": "D8703", + "Description": "Replacement of lost or broken retainer- maxillary (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8704", + "Description": "Replacement of lost or broken retainer- mandibular (Orthodontist only)", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8999", + "Description": "Unspecified orthodontic procedure, by report (Orthodontist only) I.C I.C** Y Y**", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9110", + "Description": "Palliative treatment of dental pain \u2013 per visit", + "PriceLTEQ21": "36", + "PriceGT21": "36" + }, + { + "Procedure Code": "D9222", + "Description": "Deep sedation/general anesthesia \u2013 first 15 minutes", + "PriceLTEQ21": "90", + "PriceGT21": "90" + }, + { + "Procedure Code": "D9223", + "Description": "Deep sedation/general anesthesia \u2013 each additional 15- minute increment", + "PriceLTEQ21": "90", + "PriceGT21": "90" + }, + { + "Procedure Code": "D9230", + "Description": "Analgesia, anxiolysis, inhalation of nitrous oxide", + "PriceLTEQ21": "15", + "PriceGT21": "15" + }, + { + "Procedure Code": "D9248", + "Description": "Nonintravenous conscious sedation", + "PriceLTEQ21": "45", + "PriceGT21": "45" + }, + { + "Procedure Code": "D9310", + "Description": "Consultation- Diagnostic service provided by dentist or physician other than requesting dentist or physician (Specialist only)", + "PriceLTEQ21": "63", + "PriceGT21": "63" + }, + { + "Procedure Code": "D9410", + "Description": "House/extended care facility call, once per facility per day", + "PriceLTEQ21": "39", + "PriceGT21": "39" + }, + { + "Procedure Code": "D9450", + "Description": "Rural add-on encounter payment", + "PriceLTEQ21": "31", + "PriceGT21": "31" + }, + { + "Procedure Code": "D9920", + "Description": "Behavior management, by report", + "PriceLTEQ21": "86", + "PriceGT21": "86" + }, + { + "Procedure Code": "D9930", + "Description": "Treatment of complications (postsurgical) - unusual circumstances, by report", + "PriceLTEQ21": "30", + "PriceGT21": "30" + }, + { + "Procedure Code": "D9941", + "Description": "Fabrication of athletic mouthguard", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9944", + "Description": "Occlusal guard - hard appliance, full arch", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9945", + "Description": "Occlusal guard - soft appliance, full arch", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9946", + "Description": "Occlusal guard - hard appliance, partial arch", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9999", + "Description": "Unspecified adjunctive procedure, by report", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + } +] diff --git a/apps/Frontend/src/assets/data/procedureCodesMH.json b/apps/Frontend/src/assets/data/procedureCodesMH.json new file mode 100755 index 00000000..ec281470 --- /dev/null +++ b/apps/Frontend/src/assets/data/procedureCodesMH.json @@ -0,0 +1,1191 @@ +[ + { + "Procedure Code": "D0120", + "Description": "Periodic oral evaluation - established patient", + "PriceLTEQ21": "31", + "PriceGT21": "24" + }, + { + "Procedure Code": "D0140", + "Description": "Limited oral evaluation - problem focused", + "PriceLTEQ21": "49", + "PriceGT21": "43" + }, + { + "Procedure Code": "D0145", + "Description": "Oral evaluation for a patient under three years of age and counseling with primary caregiver", + "PriceLTEQ21": "27", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D0150", + "Description": "Comprehensive oral evaluation - new or established patient", + "PriceLTEQ21": "62", + "PriceGT21": "41" + }, + { + "Procedure Code": "D0180", + "Description": "Comprehensive periodontal evaluation - new or established patient", + "PriceLTEQ21": "58", + "PriceGT21": "37" + }, + { + "Procedure Code": "D0190", + "Description": "Screening of a patient (PHDH only)", + "PriceLTEQ21": "29", + "PriceGT21": "20" + }, + { + "Procedure Code": "D0191", + "Description": "Assessment of a patient (PHDH only)", + "PriceLTEQ21": "29", + "PriceGT21": "20" + }, + { + "Procedure Code": "D0210", + "Description": "Intraoral - complete series of radiographic images", + "PriceLTEQ21": "94", + "PriceGT21": "76" + }, + { + "Procedure Code": "D0220", + "Description": "Intraoral - periapical, first radiographic image", + "PriceLTEQ21": "21", + "PriceGT21": "15" + }, + { + "Procedure Code": "D0230", + "Description": "Intraoral - periapical, each additional radiographic image", + "PriceLTEQ21": "17", + "PriceGT21": "13" + }, + { + "Procedure Code": "D0240", + "Description": "Intraoral - occlusal radiographic image", + "PriceLTEQ21": "26", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D0270", + "Description": "Bitewing - single radiographic image", + "PriceLTEQ21": "17", + "PriceGT21": "14" + }, + { + "Procedure Code": "D0272", + "Description": "Bitewings - two radiographic images", + "PriceLTEQ21": "32", + "PriceGT21": "25" + }, + { + "Procedure Code": "D0273", + "Description": "Bitewings - three radiographic images", + "PriceLTEQ21": "35", + "PriceGT21": "27" + }, + { + "Procedure Code": "D0274", + "Description": "Bitewings - four radiographic images", + "PriceLTEQ21": "46", + "PriceGT21": "36" + }, + { + "Procedure Code": "D0330", + "Description": "Panoramic radiographic image", + "PriceLTEQ21": "94", + "PriceGT21": "69" + }, + { + "Procedure Code": "D0340", + "Description": "Cephalometric radiograph image (Oral surgeon only)", + "PriceLTEQ21": "85", + "PriceGT21": "74" + }, + { + "Procedure Code": "D0364", + "Description": "Less than one jaw", + "Price": "350" + }, + { + "Procedure Code": "D0365", + "Description": "Mand", + "Price": "350" + }, + { + "Procedure Code": "D0366", + "Description": "Max", + "Price": "350" + }, + { + "Procedure Code": "D0367", + "Description": "", + "Price": "400" + }, + { + "Procedure Code": "D0368", + "Description": "include TMJ", + "Price": "375" + }, + { + "Procedure Code": "D0380", + "Description": "Less than one jaw", + "Price": "300" + }, + { + "Procedure Code": "D0381", + "Description": "Mand", + "Price": "300" + }, + { + "Procedure Code": "D0382", + "Description": "Max", + "Price": "300" + }, + { + "Procedure Code": "D0383", + "Description": "", + "Price": "350" + }, + { + "Procedure Code": "D1110", + "Description": "Prophylaxis – adult, 14 yo or older", + "PriceLTEQ21": "75", + "PriceGT21": "60" + }, + { + "Procedure Code": "D1120", + "Description": "Prophylaxis – child, 0-13 yo", + "PriceLTEQ21": "55", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1206", + "Description": "Topical application of fluoride varnish", + "PriceLTEQ21": "28", + "PriceGT21": "26" + }, + { + "Procedure Code": "D1208", + "Description": "Topical application of fluoride – excluding varnish", + "PriceLTEQ21": "31", + "PriceGT21": "29" + }, + { + "Procedure Code": "D1351", + "Description": "Sealant – per tooth", + "PriceLTEQ21": "44", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1354", + "Description": "Application of caries arresting medicament - per tooth", + "PriceLTEQ21": "15", + "PriceGT21": "15" + }, + { + "Procedure Code": "D1510", + "Description": "Space maintainer – fixed,unilateral – per quadrant", + "PriceLTEQ21": "229", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1516", + "Description": "Space maintainer- fixed- bilateral, maxillary", + "PriceLTEQ21": "345", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1517", + "Description": "Space maintainer- fixed- bilateral, mandibular", + "PriceLTEQ21": "345", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1520", + "Description": "Space maintainer – removable- unilateral- per quadrant", + "PriceLTEQ21": "244", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1526", + "Description": "Space maintainer- removable- bilateral, maxillary", + "PriceLTEQ21": "368", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1527", + "Description": "Space maintainer- removable- bilateral, mandibular", + "PriceLTEQ21": "368", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1575", + "Description": "Distal shoe space maintainer - fixed- unilateral- Per Quadrant I.C", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D1701", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration – first dose SARSCOV2 COVID-19 VAC mRNA 30mcg/0.3mL IM DOSE 1", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1702", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration – second dose SARSCOV2 COVID-19 VAC mRNA 30mcg/0.3mL IM DOSE 2", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1707", + "Description": "Janssen Covid-19 vaccine administration SARSCOV2 COVID-19 VAC Ad26 5x1010 VP/.5mL IM SINGLE DOSE", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1708", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration – third dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1709", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration – booster dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1712", + "Description": "Janssen Covid-19 vaccine administration - booster dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1713", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration tris-sucrose pediatric – first dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1714", + "Description": "Pfizer-BioNTech Covid-19 vaccine administration tris-sucrose pediatric – second dose", + "PriceLTEQ21": "45.87", + "PriceGT21": "45.87" + }, + { + "Procedure Code": "D1999", + "Description": "", + "Price": "50" + }, + { + "Procedure Code": "D2140", + "Description": "Amalgam-one surface, primary or permanent", + "PriceLTEQ21": "77", + "PriceGT21": "62" + }, + { + "Procedure Code": "D2150", + "Description": "Amalgam-two surfaces, primary or permanent", + "PriceLTEQ21": "95", + "PriceGT21": "77" + }, + { + "Procedure Code": "D2955", + "Description": "post renoval", + "Price": "350" + }, + { + "Procedure Code": "D4910", + "Description": "perio maintains", + "Price": "250" + }, + { + "Procedure Code": "D5510", + "Description": "Repair broken complete denture base (QUAD)", + "Price": "400" + }, + { + "Procedure Code": "D6056", + "Description": "pre fab abut", + "Price": "750" + }, + { + "Procedure Code": "D6057", + "Description": "custom abut", + "Price": "800" + }, + { + "Procedure Code": "D6058", + "Description": "porcelain, implant crown, ceramic crown", + "Price": "1400" + }, + { + "Procedure Code": "D6059", + "Description": "", + "Price": "1400" + }, + { + "Procedure Code": "D6100", + "Description": "", + "Price": "320" + }, + { + "Procedure Code": "D6110", + "Description": "implant", + "Price": "1600" + }, + { + "Procedure Code": "D6242", + "Description": "noble metal. For united", + "Price": "1400" + }, + { + "Procedure Code": "D6245", + "Description": "porcelain, not for united", + "Price": "1400" + }, + { + "Procedure Code": "D7910", + "Description": "suture, small wound up to 5 mm", + "Price": "400" + }, + { + "Procedure Code": "D7950", + "Description": "max", + "Price": "800" + }, + { + "Procedure Code": "D2160", + "Description": "Amalgam-three surfaces, primary or permanent", + "PriceLTEQ21": "110", + "PriceGT21": "92" + }, + { + "Procedure Code": "D2161", + "Description": "Amalgam-four or more surfaces, primary or permanent", + "PriceLTEQ21": "137", + "PriceGT21": "116" + }, + { + "Procedure Code": "D2330", + "Description": "Resin-based composite – one surface, anterior", + "PriceLTEQ21": "98", + "PriceGT21": "72" + }, + { + "Procedure Code": "D2331", + "Description": "Resin-based composite – two surfaces, anterior", + "PriceLTEQ21": "118", + "PriceGT21": "92" + }, + { + "Procedure Code": "D2332", + "Description": "Resin-based composite – three surfaces, anterior", + "PriceLTEQ21": "147", + "PriceGT21": "116" + }, + { + "Procedure Code": "D2335", + "Description": "Resin-based composite – four or more surfaces or involving incisal angle (anterior)", + "PriceLTEQ21": "188", + "PriceGT21": "146" + }, + { + "Procedure Code": "D2390", + "Description": "Resin-based composite crown, anterior", + "PriceLTEQ21": "133", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2391", + "Description": "Resin-based composite – one surface, posterior", + "PriceLTEQ21": "99", + "PriceGT21": "62" + }, + { + "Procedure Code": "D2392", + "Description": "Resin-based composite – two surfaces, posterior", + "PriceLTEQ21": "123", + "PriceGT21": "77" + }, + { + "Procedure Code": "D2393", + "Description": "Resin-based composite – three surfaces, posterior", + "PriceLTEQ21": "133", + "PriceGT21": "92" + }, + { + "Procedure Code": "D2394", + "Description": "Resin-based composite – four or more surfaces, posterior", + "PriceLTEQ21": "182", + "PriceGT21": "116" + }, + { + "Procedure Code": "D2710", + "Description": "Crown – resin-based composite (indirect)", + "PriceLTEQ21": "244", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2740", + "Description": "Crown – porcelain/ceramic", + "PriceLTEQ21": "853", + "PriceGT21": "729" + }, + { + "Procedure Code": "D2750", + "Description": "Crown – porcelain fused to high noble metal", + "PriceLTEQ21": "800", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2751", + "Description": "Crown – porcelain fused to predominantly base metal", + "PriceLTEQ21": "727", + "PriceGT21": "613" + }, + { + "Procedure Code": "D2752", + "Description": "Crown – porcelain fused to noble metal", + "PriceLTEQ21": "735", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2790", + "Description": "Crown – full cast high noble metal", + "PriceLTEQ21": "808", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2910", + "Description": "Re-cement or re-bond inlay, onlay or partial coverage restoration", + "PriceLTEQ21": "69", + "PriceGT21": "57" + }, + { + "Procedure Code": "D2920", + "Description": "Re-cement or re-bond crown", + "PriceLTEQ21": "68", + "PriceGT21": "57" + }, + { + "Procedure Code": "D2929", + "Description": "Prefabricated porcelain/ceramic crown – primary tooth", + "PriceLTEQ21": "224", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2930", + "Description": "Prefabricated stainless steel crown – primary tooth", + "PriceLTEQ21": "205", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2931", + "Description": "Prefabricated stainless steel crown – permanent tooth", + "PriceLTEQ21": "199", + "PriceGT21": "171" + }, + { + "Procedure Code": "D2932", + "Description": "Prefabricated resin crown", + "PriceLTEQ21": "224", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2934", + "Description": "Prefabricated esthetic coated stainless steel crown – primary tooth", + "PriceLTEQ21": "184", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D2950", + "Description": "Core buildup, including any pins when required", + "PriceLTEQ21": "197", + "PriceGT21": "164" + }, + { + "Procedure Code": "D2951", + "Description": "Pin retention – per tooth, in addition to restoration", + "PriceLTEQ21": "31", + "PriceGT21": "27" + }, + { + "Procedure Code": "D2954", + "Description": "Prefabricated post and core in addition to crown", + "PriceLTEQ21": "229", + "PriceGT21": "191" + }, + { + "Procedure Code": "D2980", + "Description": "Crown repair necessitated by restorative material failure", + "PriceLTEQ21": "137", + "PriceGT21": "115" + }, + { + "Procedure Code": "D2999", + "Description": "Unspecified restorative procedure, by report", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + }, + { + "Procedure Code": "D3120", + "Description": "Pulp cap – indirect (excluding final restoration)", + "PriceLTEQ21": "40", + "PriceGT21": "34" + }, + { + "Procedure Code": "D3220", + "Description": "Therapeutic pulpotomy (excluding final restoration) – removal of pulp coronal to the dentinocemental junction and application of medicament", + "PriceLTEQ21": "106", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D3310", + "Description": "Endodontic therapy, anterior (excluding final restoration)", + "PriceLTEQ21": "544", + "PriceGT21": "544" + }, + { + "Procedure Code": "D3320", + "Description": "Endodontic therapy, premolar tooth (excluding final restoration)", + "PriceLTEQ21": "639", + "PriceGT21": "639" + }, + { + "Procedure Code": "D3330", + "Description": "Endodontic therapy, molar tooth (excluding final restoration)", + "PriceLTEQ21": "829", + "PriceGT21": "829" + }, + { + "Procedure Code": "D3346", + "Description": "Retreatment of previous root canal therapy – anterior", + "PriceLTEQ21": "545", + "PriceGT21": "456" + }, + { + "Procedure Code": "D3347", + "Description": "Retreatment of previous root canal therapy – premolar", + "PriceLTEQ21": "641", + "PriceGT21": "538" + }, + { + "Procedure Code": "D3348", + "Description": "Retreatment of previous root canal therapy – molar", + "PriceLTEQ21": "789", + "PriceGT21": "613" + }, + { + "Procedure Code": "D3410", + "Description": "Apicoectomy – anterior", + "PriceLTEQ21": "471", + "PriceGT21": "407" + }, + { + "Procedure Code": "D3421", + "Description": "Apicoectomy – premolar (first root)", + "PriceLTEQ21": "550", + "PriceGT21": "460" + }, + { + "Procedure Code": "D3425", + "Description": "Apicoectomy – molar (first root)", + "PriceLTEQ21": "639", + "PriceGT21": "598" + }, + { + "Procedure Code": "D3426", + "Description": "Apicoectomy (each additional root)", + "PriceLTEQ21": "264", + "PriceGT21": "230" + }, + { + "Procedure Code": "D4210", + "Description": "Gingivectomy or gingivoplasty - Four or more contiguous teeth or bounded teeth spaces per quadrant", + "PriceLTEQ21": "343", + "PriceGT21": "307" + }, + { + "Procedure Code": "D4211", + "Description": "Gingivectomy or gingivoplasty - one to three contiguous teeth or bounded teeth spaces per quadrant", + "PriceLTEQ21": "133", + "PriceGT21": "111" + }, + { + "Procedure Code": "D4341", + "Description": "Periodontal scaling and root planing - four or more teeth per quadrant", + "PriceLTEQ21": "160", + "PriceGT21": "134" + }, + { + "Procedure Code": "D4342", + "Description": "Periodontal scaling and root planing - one to three teeth, per quadrant", + "PriceLTEQ21": "107", + "PriceGT21": "90" + }, + { + "Procedure Code": "D4346", + "Description": "Scaling in presence of generalized moderate or severe gingival inflammation – full mouth, after oral evaluation", + "PriceLTEQ21": "75", + "PriceGT21": "60" + }, + { + "Procedure Code": "D5110", + "Description": "Complete denture – maxillary", + "PriceLTEQ21": "858", + "PriceGT21": "730" + }, + { + "Procedure Code": "D5120", + "Description": "Complete denture – mandibular", + "PriceLTEQ21": "852", + "PriceGT21": "730" + }, + { + "Procedure Code": "D5130", + "Description": "Immediate denture – maxillary", + "PriceLTEQ21": "935", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5140", + "Description": "Immediate denture - mandibular", + "PriceLTEQ21": "934", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5211", + "Description": "Maxillary partial denture - resin base (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "650", + "PriceGT21": "556" + }, + { + "Procedure Code": "D5212", + "Description": "Mandibular partial denture - resin base (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "691", + "PriceGT21": "595" + }, + { + "Procedure Code": "D5213", + "Description": "Maxillary partial denture- cast metal framework with resin denture bases (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "974", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5214", + "Description": "Mandibular partial denture - cast metal framework with resin denture bases (including retentive/clasping materials, rests and teeth)", + "PriceLTEQ21": "986", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5225", + "Description": "Maxillary partial denture- flexible base", + "PriceLTEQ21": "974", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5226", + "Description": "Mandibular partial denture- flexible base", + "PriceLTEQ21": "986", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5511", + "Description": "Repair broken complete denture base, mandibular", + "PriceLTEQ21": "109", + "PriceGT21": "85" + }, + { + "Procedure Code": "D5512", + "Description": "Repair broken complete denture base, maxillary", + "PriceLTEQ21": "109", + "PriceGT21": "85" + }, + { + "Procedure Code": "D5520", + "Description": "Replace missing or broken teeth - complete denture (each tooth)", + "PriceLTEQ21": "89", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5611", + "Description": "Repair broken resin partial denture base, mandibular", + "PriceLTEQ21": "93", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5612", + "Description": "Repair broken resin partial denture base, maxillary", + "PriceLTEQ21": "93", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5621", + "Description": "Repair broken cast partial denture base, mandibular", + "PriceLTEQ21": "121", + "PriceGT21": "104" + }, + { + "Procedure Code": "D5622", + "Description": "Repair broken cast partial denture base, maxillary", + "PriceLTEQ21": "121", + "PriceGT21": "104" + }, + { + "Procedure Code": "D5630", + "Description": "Repair or replace broken retentive/clasping materials – per tooth", + "PriceLTEQ21": "107", + "PriceGT21": "99" + }, + { + "Procedure Code": "D5640", + "Description": "Replace broken teeth - per tooth", + "PriceLTEQ21": "91", + "PriceGT21": "77" + }, + { + "Procedure Code": "D5650", + "Description": "Add tooth to existing partial denture", + "PriceLTEQ21": "110", + "PriceGT21": "92" + }, + { + "Procedure Code": "D5660", + "Description": "Add clasp to existing partial denture per tooth", + "PriceLTEQ21": "125", + "PriceGT21": "98" + }, + { + "Procedure Code": "D5730", + "Description": "Reline complete maxillary denture (direct)", + "PriceLTEQ21": "188", + "PriceGT21": "158" + }, + { + "Procedure Code": "D5731", + "Description": "Reline lower complete mandibular denture (direct)", + "PriceLTEQ21": "184", + "PriceGT21": "173" + }, + { + "Procedure Code": "D5740", + "Description": "Reline maxillary partial denture(chairside)", + "PriceLTEQ21": "169", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5741", + "Description": "Reline mandibular partial denture(chairside)", + "PriceLTEQ21": "160", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5750", + "Description": "Reline complete maxillary denture (indirect)", + "PriceLTEQ21": "255", + "PriceGT21": "214" + }, + { + "Procedure Code": "D5751", + "Description": "Reline complete mandibular denture (indirect)", + "PriceLTEQ21": "256", + "PriceGT21": "215" + }, + { + "Procedure Code": "D5760", + "Description": "Reline maxillary partial denture (laboratory)", + "PriceLTEQ21": "252", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D5761", + "Description": "Reline mandibular partial denture (laboratory)", + "PriceLTEQ21": "252", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6241", + "Description": "Pontic-porcelain fused metal", + "PriceLTEQ21": "691", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6751", + "Description": "Retainer crown-porcelain fused to metal", + "PriceLTEQ21": "691", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6930", + "Description": "Re-cement or re-bond fixed partial denture", + "PriceLTEQ21": "87", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6980", + "Description": "Fixed partial denture repair", + "PriceLTEQ21": "155", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D6999", + "Description": "Fixed prosthodontic procedure", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + }, + { + "Procedure Code": "D7111", + "Description": "Extraction, coronal remnants - primary tooth", + "PriceLTEQ21": "80", + "PriceGT21": "75" + }, + { + "Procedure Code": "D7140", + "Description": "Extraction, erupted tooth or exposed root (elevation and/or forceps removal)", + "PriceLTEQ21": "107", + "PriceGT21": "77" + }, + { + "Procedure Code": "D7210", + "Description": "Extraction, erupted tooth requiring removal of bone and/or sectioning of tooth, and including elevation of mucoperiosteal flap if indicated", + "PriceLTEQ21": "179", + "PriceGT21": "149" + }, + { + "Procedure Code": "D7220", + "Description": "Removal of impacted tooth - soft tissue", + "PriceLTEQ21": "223", + "PriceGT21": "191" + }, + { + "Procedure Code": "D7230", + "Description": "Removal of impacted tooth - partially bony", + "PriceLTEQ21": "286", + "PriceGT21": "249" + }, + { + "Procedure Code": "D7240", + "Description": "Removal of impacted tooth - completely bony", + "PriceLTEQ21": "378", + "PriceGT21": "295" + }, + { + "Procedure Code": "D7250", + "Description": "Surgical removal of residual tooth roots (cutting procedure)", + "PriceLTEQ21": "173", + "PriceGT21": "144" + }, + { + "Procedure Code": "D7251", + "Description": "Coronectomy- intentional partial tooth removal, impacted teeth only", + "PriceLTEQ21": "173", + "PriceGT21": "134" + }, + { + "Procedure Code": "D7270", + "Description": "Tooth reimplantation and/or stabilization of accidentally evulsed or displaced tooth", + "PriceLTEQ21": "145", + "PriceGT21": "106" + }, + { + "Procedure Code": "D7280", + "Description": "Surgical access of an unerupted tooth", + "PriceLTEQ21": "452", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D7283", + "Description": "Placement of device to facilitate eruption of impacted tooth", + "PriceLTEQ21": "84", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D7310", + "Description": "Alveoloplasty in conjunction with extractions-four or more teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "163", + "PriceGT21": "142" + }, + { + "Procedure Code": "D7311", + "Description": "Alveoloplasty in conjunction with extractions - one to three teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "146", + "PriceGT21": "128" + }, + { + "Procedure Code": "D7320", + "Description": "Alveoloplasty not in conjunction with extractions- four or more teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "202", + "PriceGT21": "187" + }, + { + "Procedure Code": "D7321", + "Description": "Alveoloplasty not in conjunction with extractions - one to three teeth or tooth spaces, per quadrant", + "PriceLTEQ21": "162", + "PriceGT21": "149" + }, + { + "Procedure Code": "D7340", + "Description": "Vestibuloplasty - ridge extension (second epithelialization)", + "PriceLTEQ21": "796", + "PriceGT21": "747" + }, + { + "Procedure Code": "D7350", + "Description": "Vestibuloplasty - ridge extension (Oral surgeon only)", + "PriceLTEQ21": "1236", + "PriceGT21": "943" + }, + { + "Procedure Code": "D7410", + "Description": "Radical excision - lesion diameter up to 1.25cm", + "PriceLTEQ21": "124", + "PriceGT21": "115" + }, + { + "Procedure Code": "D7411", + "Description": "Excision of benign lesion greater than 1.25 cm", + "PriceLTEQ21": "254", + "PriceGT21": "208" + }, + { + "Procedure Code": "D7450", + "Description": "Removal of benign odontogenic cyst or tumor - lesion diameter up to 1.25 cm", + "PriceLTEQ21": "252", + "PriceGT21": "248" + }, + { + "Procedure Code": "D7451", + "Description": "Removal of benign odontogenic cyst or tumor - lesion diameter greater than 1.25 cm", + "PriceLTEQ21": "343", + "PriceGT21": "288" + }, + { + "Procedure Code": "D7460", + "Description": "Removal of benign nonodontogenic cyst or tumor - lesion diameter up to 1.25 cm", + "PriceLTEQ21": "142", + "PriceGT21": "121" + }, + { + "Procedure Code": "D7461", + "Description": "Removal of benign nonodontogenic cyst or tumor - lesion diameter greater than 1.25 cm", + "PriceLTEQ21": "194", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7471", + "Description": "Removal of lateral exostosis (maxilla or mandible) (Oral surgeon only)", + "PriceLTEQ21": "194", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7472", + "Description": "Removal of torus palatinus (Oral surgeon only)", + "PriceLTEQ21": "194", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7473", + "Description": "Removal of torus mandibularis (Oral surgeon only)", + "PriceLTEQ21": "194", + "PriceGT21": "143" + }, + { + "Procedure Code": "D7961", + "Description": "Buccal/labial frenectomy (frenulectomy)", + "PriceLTEQ21": "353", + "PriceGT21": "107" + }, + { + "Procedure Code": "D7962", + "Description": "Lingual frenectomy (frenulectomy)", + "PriceLTEQ21": "353", + "PriceGT21": "107" + }, + { + "Procedure Code": "D7963", + "Description": "Frenuloplasty", + "PriceLTEQ21": "480", + "PriceGT21": "416" + }, + { + "Procedure Code": "D7970", + "Description": "Excision of hyperplastic tissue - per arch", + "PriceLTEQ21": "334", + "PriceGT21": "246" + }, + { + "Procedure Code": "D7999", + "Description": "Unspecified oral surgery procedure, by report", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + }, + { + "Procedure Code": "D8010", + "Description": "Limited orthodontic treamtnent of the primary transition (Orthodontist only)", + "PriceLTEQ21": "250", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8020", + "Description": "Limited orthodontic treatment of the transitional dentition (Orthodontist only)", + "PriceLTEQ21": "250", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8030", + "Description": "Limited orthodontic treatment of the adolescent dentition (Orthodontist only)", + "PriceLTEQ21": "250", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8040", + "Description": "Limited orthodontic treatment of the adult dentition (Orthodontist only)", + "PriceLTEQ21": "250", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8070", + "Description": "Comprehensive orthodontic treatment of the transitional dentition (Orthodontist only)", + "PriceLTEQ21": "1302", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8080", + "Description": "Comprehensive orthodontic treatment of the adolescent dentition (Orthodontist only)", + "PriceLTEQ21": "1302", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8090", + "Description": "Comprehensive orthodontic treatment of the adult dentition (Orthodontist only)", + "PriceLTEQ21": "1302", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8660", + "Description": "Pre-orthodontic treatment examination to monitor growth and development (records fee) (Orthodontist only)", + "PriceLTEQ21": "136", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8670", + "Description": "Periodic orthodontic treatment visit (Orthodontist only)", + "PriceLTEQ21": "288", + "PriceGT21": "215" + }, + { + "Procedure Code": "D8680", + "Description": "Orthodontic retention (removal of appliances, construction and placement of retainer(s)) (Orthodontist only)", + "PriceLTEQ21": "102", + "PriceGT21": "85" + }, + { + "Procedure Code": "D8703", + "Description": "Replacement of lost or broken retainer- maxillary (Orthodontist only)", + "PriceLTEQ21": "95", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8704", + "Description": "Replacement of lost or broken retainer- mandibular (Orthodontist only)", + "PriceLTEQ21": "95", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D8999", + "Description": "Unspecified orthodontic procedure, by report (Orthodontist only) I.C I.C** Y Y**", + "PriceLTEQ21": "NC", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9110", + "Description": "Palliative treatment of dental pain – per visit", + "PriceLTEQ21": "75", + "PriceGT21": "36" + }, + { + "Procedure Code": "D9222", + "Description": "Deep sedation/general anesthesia – first 15 minutes", + "PriceLTEQ21": "109", + "PriceGT21": "90" + }, + { + "Procedure Code": "D9223", + "Description": "Deep sedation/general anesthesia – each additional 15- minute increment", + "PriceLTEQ21": "109", + "PriceGT21": "90" + }, + { + "Procedure Code": "D9230", + "Description": "Analgesia, anxiolysis, inhalation of nitrous oxide", + "PriceLTEQ21": "22", + "PriceGT21": "15" + }, + { + "Procedure Code": "D9248", + "Description": "Nonintravenous conscious sedation", + "PriceLTEQ21": "45", + "PriceGT21": "45" + }, + { + "Procedure Code": "D9310", + "Description": "Consultation- Diagnostic service provided by dentist or physician other than requesting dentist or physician (Specialist only)", + "PriceLTEQ21": "54", + "PriceGT21": "63" + }, + { + "Procedure Code": "D9410", + "Description": "House/extended care facility call, once per facility per day", + "PriceLTEQ21": "36", + "PriceGT21": "39" + }, + { + "Procedure Code": "D9450", + "Description": "Rural add-on encounter payment", + "PriceLTEQ21": "31", + "PriceGT21": "31" + }, + { + "Procedure Code": "D9920", + "Description": "Behavior management, by report", + "PriceLTEQ21": "86", + "PriceGT21": "86" + }, + { + "Procedure Code": "D9930", + "Description": "Treatment of complications (postsurgical) - unusual circumstances, by report", + "PriceLTEQ21": "66", + "PriceGT21": "30" + }, + { + "Procedure Code": "D9941", + "Description": "Fabrication of athletic mouthguard", + "PriceLTEQ21": "85", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9944", + "Description": "Occlusal guard - hard appliance, full arch", + "PriceLTEQ21": "308", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9945", + "Description": "Occlusal guard - soft appliance, full arch", + "PriceLTEQ21": "308", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9946", + "Description": "Occlusal guard - hard appliance, partial arch", + "PriceLTEQ21": "308", + "PriceGT21": "NC" + }, + { + "Procedure Code": "D9999", + "Description": "Unspecified adjunctive procedure, by report", + "PriceLTEQ21": "IC", + "PriceGT21": "IC" + } +] diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index c6917a37..e8f85e7f 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -75,6 +75,7 @@ interface ClaimFormProps { onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void; onHandleForMHSeleniumClaim: (data: ClaimFormData) => void; onHandleForMHSeleniumClaimPreAuth: (data: ClaimPreAuthData) => void; + onHandleForCCASeleniumClaim: (data: ClaimFormData) => void; onClose: () => void; } @@ -87,6 +88,7 @@ export function ClaimForm({ onHandleUpdatePatient, onHandleForMHSeleniumClaim, onHandleForMHSeleniumClaimPreAuth, + onHandleForCCASeleniumClaim, onSubmit, onClose, }: ClaimFormProps) { @@ -331,6 +333,7 @@ export function ClaimForm({ missingTeethStatus: (claim.missingTeethStatus as MissingTeethStatus) ?? "No_missing", missingTeeth: (claim.missingTeeth as Record) ?? {}, insuranceProvider: claim.insuranceProvider ?? "", + insuranceSiteKey: claim.insuranceSiteKey || deriveInsuranceSiteKey(claim.insuranceProvider), ...(claim.staffId ? { staffId: claim.staffId } : {}), claimFiles: claim.claimFiles ?? [], })); @@ -590,17 +593,41 @@ export function ClaimForm({ uploadedFiles: [], }); + // Map patient.insuranceProvider (free-text from eligibility) → insuranceSiteKey + const deriveInsuranceSiteKey = (provider: string | null | undefined): string => { + const p = (provider || "").toLowerCase().trim(); + if (!p) return ""; + if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH"; + if (p.includes("commonwealth care alliance") || p === "cca") return "CCA"; + if (p.includes("ddma")) return "DDMA"; + if (p.includes("delta ins") || p === "deltains") return "DeltaIns"; + if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO"; + if (p.includes("united sco") || p === "unitedsco") return "UnitedSCO"; + if (p.includes("cmsp")) return "CMSP"; + if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS"; + if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR"; + if (p.includes("aetna")) return "Aetna"; + if (p.includes("altus")) return "Altus"; + if (p.includes("metlife")) return "MetlifeDental"; + if (p.includes("cigna")) return "Cigna"; + if (p.includes("delta wa") || p === "deltawa") return "DeltaWA"; + if (p.includes("delta il") || p === "deltail") return "DeltaIL"; + return ""; + }; + // Sync patient data to form when patient updates useEffect(() => { if (patient) { const fullName = `${patient.firstName || ""} ${patient.lastName || ""}`.trim(); + const siteKey = deriveInsuranceSiteKey(patient.insuranceProvider); setForm((prev) => ({ ...prev, patientId: Number(patient.id), patientName: fullName, dateOfBirth: normalizeToIsoDateString(patient.dateOfBirth), memberId: patient.insuranceId || "", + ...(siteKey ? { insuranceSiteKey: siteKey } : {}), })); } }, [patient]); @@ -674,12 +701,13 @@ export function ClaimForm({ } }; - // Map Price function + // Map Price function — uses the fee schedule for the selected insurance type const onMapPrice = () => { setForm((prev) => mapPricesForForm({ form: prev, patientDOB: patient?.dateOfBirth ?? "", + insuranceSiteKey: prev.insuranceSiteKey, }), ); }; @@ -867,15 +895,12 @@ export function ClaimForm({ onClose(); }; - // 3nd Button workflow - Only Creates Data, patient, appointmetn, claim, payment, not actually submits claim to MH site. - const handleAddService = async () => { - // 0. Validate required fields + // 3rd Button workflow — CCA Claim: saves to DB then submits via Selenium + const handleCCAClaim = async () => { const missingFields: string[] = []; - if (!form.memberId?.trim()) missingFields.push("Member ID"); if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth"); if (!patient?.firstName?.trim()) missingFields.push("First Name"); - if (missingFields.length > 0) { toast({ title: "Missing Required Fields", @@ -885,31 +910,26 @@ export function ClaimForm({ return; } - // require at least one procedure code before proceeding const filteredServiceLines = (form.serviceLines || []).filter( (line) => (line.procedureCode ?? "").trim() !== "", ); if (filteredServiceLines.length === 0) { toast({ title: "No procedure codes", - description: - "Please add at least one procedure code before submitting the claim.", + description: "Please add at least one procedure code before submitting the claim.", variant: "destructive", }); return; } - // 1. Create or update appointment + // Create appointment if needed let appointmentIdToUse = appointmentId; - if (appointmentIdToUse == null) { - const appointmentData = { - patientId: patientId, + const created = await onHandleAppointmentSubmit({ + patientId, date: serviceDate, staffId: appointmentStaffId ?? staff?.id, - }; - const created = await onHandleAppointmentSubmit(appointmentData); - + }); if (typeof created === "number" && created > 0) { appointmentIdToUse = created; } else if (created && typeof (created as any).id === "number") { @@ -917,27 +937,40 @@ export function ClaimForm({ } } - // 3. Create Claim(if not) - // Filter out empty service lines (empty procedureCode) - const { uploadedFiles, insuranceSiteKey, npiProvider: _npi, ...formToCreateClaim } = form; - - // build claimFiles metadata from uploadedFiles (only filename + mimeType) + const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form; const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({ filename: f.name, mimeType: f.type, })); + const selectedNpiProviderId = npiProvider?.npiNumber + ? npiProviders.find((p) => p.npiNumber === npiProvider.npiNumber)?.id ?? null + : null; + + // Save claim to DB const createdClaim = await onSubmit({ ...formToCreateClaim, serviceLines: filteredServiceLines, staffId: appointmentStaffId ?? Number(staff?.id), - patientId: patientId, - insuranceProvider: "MassHealth", + patientId, + insuranceProvider: "CCA", appointmentId: appointmentIdToUse!, claimFiles: claimFilesMeta, + ...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}), + }); + + // Send to CCA Selenium — send raw YYYY-MM-DD so Python _format_dob converts correctly + onHandleForCCASeleniumClaim({ + ...form, + serviceLines: filteredServiceLines, + staffId: appointmentStaffId ?? Number(staff?.id), + patientId, + insuranceProvider: "CCA", + appointmentId: appointmentIdToUse!, + insuranceSiteKey: "CCA", + claimId: createdClaim.id, }); - // 4. Close form onClose(); }; @@ -1186,6 +1219,7 @@ export function ClaimForm({ comboId, patient?.dateOfBirth ?? "", { replaceAll: false, lineDate: form.serviceDate }, + form.insuranceSiteKey, ); setForm(nextForm); @@ -1337,6 +1371,35 @@ export function ClaimForm({
+ + scrollToLine(0), 0); return next; @@ -1765,14 +1829,13 @@ export function ClaimForm({ className="w-32 bg-blue-600 hover:bg-blue-700 text-white" onClick={() => handleMHSubmit()} > - MH + MH Claim + + ); +}); diff --git a/apps/Frontend/src/components/patients/patient-form.tsx b/apps/Frontend/src/components/patients/patient-form.tsx index 0b0f2edf..e9250afd 100755 --- a/apps/Frontend/src/components/patients/patient-form.tsx +++ b/apps/Frontend/src/components/patients/patient-form.tsx @@ -60,6 +60,27 @@ export const PatientForm = forwardRef( [isEditing], ); + const normalizeInsuranceProvider = (val: string | null | undefined): string => { + const p = (val || "").toLowerCase().trim(); + if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MassHealth"; + if (p.includes("commonwealth care alliance") || p === "cca") return "CCA"; + if (p.includes("ddma")) return "DDMA"; + if (p.includes("delta dental") || p.includes("delta ins") || p === "deltains") return "DeltaIns"; + if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO"; + if (p.includes("united sco") || p === "unitedsco") return "UnitedSCO"; + if (p.includes("cmsp")) return "CMSP"; + if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS"; + if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR"; + if (p.includes("aetna")) return "Aetna"; + if (p.includes("altus")) return "Altus"; + if (p.includes("metlife")) return "MetlifeDental"; + if (p.includes("cigna")) return "Cigna"; + if (p.includes("delta wa") || p === "deltawa") return "DeltaWA"; + if (p.includes("delta il") || p === "deltail") return "DeltaIL"; + if (p.includes("other")) return "Others"; + return val || ""; + }; + const computedDefaultValues = useMemo(() => { if (isEditing && patient) { const { id, userId, createdAt, ...sanitizedPatient } = patient; @@ -68,6 +89,7 @@ export const PatientForm = forwardRef( dateOfBirth: patient.dateOfBirth ? formatLocalDate(new Date(patient.dateOfBirth)) : null, + insuranceProvider: normalizeInsuranceProvider(patient.insuranceProvider), }; } return { @@ -116,13 +138,17 @@ export const PatientForm = forwardRef( useEffect(() => { if (patient) { const { id, userId, createdAt, ...sanitizedPatient } = patient; + const normalized = normalizeInsuranceProvider(patient.insuranceProvider); const resetValues: Partial = { ...sanitizedPatient, dateOfBirth: patient.dateOfBirth ? formatLocalDate(new Date(patient.dateOfBirth)) : null, + insuranceProvider: normalized, }; form.reset(resetValues); + // Explicit setValue ensures the controlled Select re-renders with the new value + form.setValue("insuranceProvider" as any, normalized); } else { form.reset(computedDefaultValues); } @@ -396,27 +422,33 @@ export const PatientForm = forwardRef( name="insuranceProvider" render={({ field }) => ( - Insurance Provider + Insurance Type diff --git a/apps/Frontend/src/lib/api/documents.js b/apps/Frontend/src/lib/api/documents.js index e1611045..bf9b4646 100755 --- a/apps/Frontend/src/lib/api/documents.js +++ b/apps/Frontend/src/lib/api/documents.js @@ -3,11 +3,11 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL_BACKEND ?? ""; // Upload a document for a patient export const uploadDocument = async (patientId, file) => { const formData = new FormData(); - formData.append('file', file); - formData.append('patientId', patientId.toString()); + formData.append("file", file); + formData.append("patientId", patientId.toString()); const token = localStorage.getItem("token"); const response = await fetch(`${API_BASE_URL}/api/patient-documents/upload`, { - method: 'POST', + method: "POST", headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, @@ -22,12 +22,15 @@ export const uploadDocument = async (patientId, file) => { }; // Get all documents for a patient export const getPatientDocuments = async (patientId, limit, offset) => { - const url = new URL(`${API_BASE_URL}/api/patient-documents/patient/${patientId}`); + const urlPath = `/api/patient-documents/patient/${patientId}`; + const url = API_BASE_URL + ? new URL(`${API_BASE_URL}${urlPath}`) + : new URL(urlPath, window.location.origin); if (limit !== undefined) { - url.searchParams.append('limit', limit.toString()); + url.searchParams.append("limit", limit.toString()); } if (offset !== undefined) { - url.searchParams.append('offset', offset.toString()); + url.searchParams.append("offset", offset.toString()); } const token = localStorage.getItem("token"); const headers = { @@ -45,11 +48,17 @@ export const getPatientDocuments = async (patientId, limit, offset) => { }; // View a document (inline display) export const viewDocument = (documentId) => { - return `${API_BASE_URL}/api/patient-documents/${documentId}/view`; + if (API_BASE_URL) { + return `${API_BASE_URL}/api/patient-documents/${documentId}/view`; + } + return `/api/patient-documents/${documentId}/view`; }; // Download a document export const downloadDocument = (documentId) => { - return `${API_BASE_URL}/api/patient-documents/${documentId}/download`; + if (API_BASE_URL) { + return `${API_BASE_URL}/api/patient-documents/${documentId}/download`; + } + return `/api/patient-documents/${documentId}/download`; }; // Delete a document export const deleteDocument = async (documentId) => { @@ -74,21 +83,21 @@ export const scanDocument = async (patientId) => { // Helper function to format file size export const formatFileSize = (bytes) => { if (bytes === 0) - return '0 Bytes'; + return "0 Bytes"; const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; }; // Helper function to get file icon based on MIME type export const getFileIcon = (mimeType) => { - if (mimeType.startsWith('image/')) - return '🖼️'; - if (mimeType === 'application/pdf') - return '📄'; - if (mimeType.includes('word') || mimeType.includes('document')) - return '📝'; - if (mimeType.includes('text')) - return '📄'; - return '📎'; + if (mimeType.startsWith("image/")) + return "🖼️"; + if (mimeType === "application/pdf") + return "📄"; + if (mimeType.includes("word") || mimeType.includes("document")) + return "📝"; + if (mimeType.includes("text")) + return "📄"; + return "📎"; }; diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index 6e0494e7..30405af4 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -1978,6 +1978,7 @@ export default function AppointmentsPage() { onHandleUpdatePatient={(_patient: UpdatePatient & { id: number }) => {}} onHandleForMHSeleniumClaim={() => {}} onHandleForMHSeleniumClaimPreAuth={() => {}} + onHandleForCCASeleniumClaim={() => {}} /> )}
diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 04b55bb4..b5b2ebac 100755 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -145,12 +145,28 @@ export default function ClaimsPage() { if (!pendingClaimJobId) return; if (jobStatus === "completed" && jobResult) { setPendingClaimJobId(null); - handleMHSeleniumPdfDownload( - jobResult, - pendingClaimMeta.current.patientId, - pendingClaimMeta.current.groupKey - ); queryClient.invalidateQueries({ queryKey: ["claims-recent"] }); + + // CCA result: pdfFileId is already saved by the processor — open preview directly + if (jobResult.pdfFileId) { + setPreviewPdfId(jobResult.pdfFileId); + setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`); + setPreviewOpen(true); + dispatch(setTaskStatus({ + key: "claimSubmit", + status: "success", + message: jobResult.claimNumber + ? `CCA claim submitted — Encounter ID: ${jobResult.claimNumber}` + : "CCA claim submitted successfully", + })); + } else { + // MH claim: needs PDF download step + handleMHSeleniumPdfDownload( + jobResult, + pendingClaimMeta.current.patientId, + pendingClaimMeta.current.groupKey + ); + } } else if (jobStatus === "failed") { setPendingClaimJobId(null); dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: "Selenium job failed" })); @@ -409,6 +425,35 @@ export default function ClaimsPage() { } }; + // CCA claim selenium handler + const handleCCAClaimSubmitSelenium = async (data: any) => { + const formData = new FormData(); + formData.append("data", JSON.stringify(data)); + if (socketId) formData.append("socketId", socketId); + const uploadedFiles: File[] = data.uploadedFiles ?? []; + uploadedFiles.forEach((file: File) => { + if (file.type === "application/pdf") { + formData.append("pdfs", file); + } else if (file.type.startsWith("image/")) { + formData.append("images", file); + } + }); + try { + dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting CCA claim..." })); + const response = await apiRequest("POST", "/api/claims/cca-claim", formData); + const result = await response.json(); + if (result.error) throw new Error(result.error); + // Track job so the completion useEffect fires when Selenium finishes + pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" }; + setPendingClaimJobId(result.jobId); + dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "CCA claim queued. Awaiting Selenium..." })); + toast({ title: "CCA Claim queued", description: "Selenium is processing the claim.", variant: "default" }); + } catch (error: any) { + dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "CCA claim failed" })); + toast({ title: "CCA Claim error", description: error.message || "An error occurred.", variant: "destructive" }); + } + }; + // 5. selenium pdf download handler const handleMHSeleniumPdfDownload = async ( data: any, @@ -620,6 +665,7 @@ export default function ClaimsPage() { onHandleUpdatePatient={handleUpdatePatient} onHandleForMHSeleniumClaim={handleMHClaimSubmitSelenium} onHandleForMHSeleniumClaimPreAuth={handleMHClaimPreAuthSubmitSelenium} + onHandleForCCASeleniumClaim={handleCCAClaimSubmitSelenium} /> )} diff --git a/apps/Frontend/src/utils/procedureCombosMapping.js b/apps/Frontend/src/utils/procedureCombosMapping.js new file mode 100644 index 00000000..f81814d1 --- /dev/null +++ b/apps/Frontend/src/utils/procedureCombosMapping.js @@ -0,0 +1,241 @@ +import Decimal from "decimal.js"; +import rawCodeTable from "@/assets/data/procedureCodesMH.json"; +import { PROCEDURE_COMBOS } from "./procedureCombos"; +const CODE_TABLE = rawCodeTable; +/* ----------------------------- Helpers ----------------------------- */ +export const COMBO_BUTTONS = Object.values(PROCEDURE_COMBOS).map((c) => ({ + id: c.id, + label: c.label, +})); +// Build a fast lookup map keyed by normalized code +const normalizeCode = (code) => code.replace(/\s+/g, "").toUpperCase(); +const CODE_MAP = (() => { + const m = new Map(); + for (const r of CODE_TABLE) { + const k = normalizeCode(String(r["Procedure Code"] || "")); + if (k && !m.has(k)) + m.set(k, r); + } + return m; +})(); +// this function is solely for abbrevations feature in claim-form +export function getDescriptionForCode(code) { + if (!code) + return undefined; + const row = CODE_MAP.get(normalizeCode(code)); + return row?.Description; +} +const isBlankPrice = (v) => { + if (v == null) + return true; + const s = String(v).trim().toUpperCase(); + return s === "" || s === "IC" || s === "NC"; +}; +const toDecimalOrZero = (v) => { + if (isBlankPrice(v)) + return new Decimal(0); + const n = typeof v === "string" ? parseFloat(v) : v; + return new Decimal(Number.isFinite(n) ? n : 0); +}; +const parseDate = (d) => { + if (d instanceof Date) + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); + const s = String(d).trim(); + // MM/DD/YYYY + const mdy = /^(\d{2})\/(\d{2})\/(\d{4})$/; + const m1 = mdy.exec(s); + if (m1) { + const mm = Number(m1[1]); + const dd = Number(m1[2]); + const yyyy = Number(m1[3]); + return new Date(yyyy, mm - 1, dd); + } + // YYYY-MM-DD + const ymd = /^(\d{4})-(\d{2})-(\d{2})$/; + const m2 = ymd.exec(s); + if (m2) { + const yyyy = Number(m2[1]); + const mm = Number(m2[2]); + const dd = Number(m2[3]); + return new Date(yyyy, mm - 1, dd); + } + // Fallback + return new Date(s); +}; +const ageOnDate = (dob, on) => { + const birth = parseDate(dob); + const ref = parseDate(on); + let age = ref.getFullYear() - birth.getFullYear(); + const hadBirthday = ref.getMonth() > birth.getMonth() || + (ref.getMonth() === birth.getMonth() && ref.getDate() >= birth.getDate()); + if (!hadBirthday) + age -= 1; + return age; +}; +/** + * we can implement per-code age buckets without changing the JSON. + * + * Behavior: + * - Default: same as before: age <= 21 -> PriceLTEQ21, else PriceGT21 + * - Fallback to Price if tiered field is blank/IC/NC + * - Special-cases D1110 and D1120 according to MH rules + */ +export function pickPriceForRowByAge(row, age, normalizedCode) { + // Special-case rules (add more codes here if needed) + if (normalizedCode) { + // D1110: only valid for age >=14 (14..21 => PriceLTEQ21, >21 => PriceGT21) + if (normalizedCode === "D1110") { + if (age < 14) { + // D1110 not applicable to children <14 (those belong to D1120) + return new Decimal(0); + } + if (age >= 14 && age <= 21) { + // use PriceLTEQ21 only if present + if (!isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + return new Decimal(0); + } + // age > 21 + if (!isBlankPrice(row.PriceGT21)) + return toDecimalOrZero(row.PriceGT21); + return new Decimal(0); + } + // D1120: child 0-13 => PriceLTEQ21, otherwise no price (NC) + if (normalizedCode === "D1120") { + if (age < 14) { + if (!isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + return new Decimal(0); + } + // age >= 14 => NC / no price + return new Decimal(0); + } + } + // Generic/default behavior (unchanged) + if (age <= 21) { + if (!isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + } + else { + if (!isBlankPrice(row.PriceGT21)) + return toDecimalOrZero(row.PriceGT21); + } + // Fallback to Price if tiered not available/blank + if (!isBlankPrice(row.Price)) + return toDecimalOrZero(row.Price); + return new Decimal(0); +} +/** + * Gets price for a code using age & code table. + */ +function getPriceForCodeWithAgeFromMap(map, code, age) { + const norm = normalizeCode(code); + const row = map.get(norm); + return row ? pickPriceForRowByAge(row, age, norm) : new Decimal(0); +} +// helper keeping lines empty, +export const makeEmptyLine = (lineDate) => ({ + procedureCode: "", + procedureDate: lineDate, + quad: "", + arch: "", + toothNumber: "", + toothSurface: "", + totalBilled: new Decimal(0), + totalAdjusted: new Decimal(0), + totalPaid: new Decimal(0), +}); +// Ensure the array has at least `min` lines; append blank ones if needed. +const ensureCapacity = (lines, min, lineDate) => { + while (lines.length < min) { + lines.push(makeEmptyLine(lineDate)); + } +}; +/* ------------------------- Main entry points ------------------------- */ +/** + * Map prices for ALL existing lines in a form (your "Map Price" button), + * using patient's DOB and the form's serviceDate (or per-line procedureDate). + * Returns a NEW form object (immutable). + */ +export function mapPricesForForm(params) { + const { form, patientDOB } = params; + return { + ...form, + serviceLines: form.serviceLines.map((ln) => { + const age = ageOnDate(patientDOB, form.serviceDate); + const code = normalizeCode(ln.procedureCode || ""); + if (!code) + return { ...ln }; + const price = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age); + return { ...ln, procedureCode: code, totalBilled: price }; + }), + }; +} +/** + * Apply a preset combo (fills codes & prices) using patientDOB and serviceDate. + * Returns a NEW form object (immutable). + */ +export function applyComboToForm(form, comboId, patientDOB, options = {}) { + const preset = PROCEDURE_COMBOS[String(comboId)]; + if (!preset) + return form; + const { append = true, startIndex, lineDate = form.serviceDate, clearTrailing = false, replaceAll = false, // NEW + } = options; + const next = { ...form, serviceLines: [...form.serviceLines] }; + // Replace-all: blank all existing and start from 0 + if (replaceAll) { + for (let i = 0; i < next.serviceLines.length; i++) { + next.serviceLines[i] = makeEmptyLine(lineDate); + } + } + // determine insertion index + let insertAt = 0; + if (!replaceAll) { + if (append) { + let last = -1; + next.serviceLines.forEach((ln, i) => { + if (ln.procedureCode?.trim()) + last = i; + }); + insertAt = Math.max(0, last + 1); + } + else if (typeof startIndex === "number") { + insertAt = Math.max(0, Math.min(startIndex, next.serviceLines.length - 1)); + } + } // if replaceAll, insertAt stays 0 + // Make sure we have enough rows for the whole combo + ensureCapacity(next.serviceLines, insertAt + preset.codes.length, lineDate); + // Age on the specific line date we will set + const age = ageOnDate(patientDOB, lineDate); + for (let j = 0; j < preset.codes.length; j++) { + const i = insertAt + j; + if (i >= next.serviceLines.length) + break; + const codeRaw = preset.codes[j]; + if (!codeRaw) + continue; + const code = normalizeCode(codeRaw); + const price = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age); + const original = next.serviceLines[i]; + next.serviceLines[i] = { + ...original, + procedureCode: code, + procedureDate: lineDate, + quad: original?.quad ?? "", + arch: original?.arch ?? "", + toothNumber: preset.toothNumbers?.[j] ?? original?.toothNumber ?? "", + toothSurface: original?.toothSurface ?? "", + totalBilled: price, + totalAdjusted: new Decimal(0), + totalPaid: new Decimal(0), + }; + } + if (replaceAll || clearTrailing) { + const after = insertAt + preset.codes.length; + for (let i = after; i < next.serviceLines.length; i++) { + next.serviceLines[i] = makeEmptyLine(lineDate); + } + } + return next; +} +export { CODE_MAP, getPriceForCodeWithAgeFromMap }; diff --git a/apps/Frontend/src/utils/procedureCombosMapping.ts b/apps/Frontend/src/utils/procedureCombosMapping.ts index 95158c76..f02b4ddc 100755 --- a/apps/Frontend/src/utils/procedureCombosMapping.ts +++ b/apps/Frontend/src/utils/procedureCombosMapping.ts @@ -1,6 +1,7 @@ import { InputServiceLine } from "@repo/db/types"; import Decimal from "decimal.js"; -import rawCodeTable from "@/assets/data/procedureCodes.json"; +import rawCodeTable from "@/assets/data/procedureCodesMH.json"; +import rawCCACodeTable from "@/assets/data/procedureCodesCCA.json"; import { PROCEDURE_COMBOS } from "./procedureCombos"; /* ----------------------------- Types ----------------------------- */ @@ -13,6 +14,7 @@ export type CodeRow = { [k: string]: unknown; }; const CODE_TABLE = rawCodeTable as CodeRow[]; +const CCA_CODE_TABLE = rawCCACodeTable as CodeRow[]; export type ClaimFormLike = { serviceDate: string; // form-level service date @@ -45,6 +47,21 @@ const CODE_MAP: Map = (() => { return m; })(); +const CCA_CODE_MAP: Map = (() => { + const m = new Map(); + for (const r of CCA_CODE_TABLE) { + const k = normalizeCode(String(r["Procedure Code"] || "")); + if (k && !m.has(k)) m.set(k, r); + } + return m; +})(); + +/** Return the correct fee-schedule map for the given insurance type. */ +function getCodeMap(insuranceSiteKey?: string): Map { + if (insuranceSiteKey === "CCA") return CCA_CODE_MAP; + return CODE_MAP; // default: MassHealth +} + // this function is solely for abbrevations feature in claim-form export function getDescriptionForCode( code: string | undefined @@ -209,15 +226,17 @@ const ensureCapacity = ( export function mapPricesForForm(params: { form: T; patientDOB: DateInput; + insuranceSiteKey?: string; }): T { - const { form, patientDOB } = params; + const { form, patientDOB, insuranceSiteKey } = params; + const map = getCodeMap(insuranceSiteKey); return { ...form, serviceLines: form.serviceLines.map((ln) => { const age = ageOnDate(patientDOB, form.serviceDate); const code = normalizeCode(ln.procedureCode || ""); if (!code) return { ...ln }; - const price = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age); + const price = getPriceForCodeWithAgeFromMap(map, code, age); return { ...ln, procedureCode: code, totalBilled: price }; }), }; @@ -231,7 +250,8 @@ export function applyComboToForm( form: T, comboId: keyof typeof PROCEDURE_COMBOS, patientDOB: DateInput, - options: ApplyOptions = {} + options: ApplyOptions = {}, + insuranceSiteKey?: string ): T { const preset = PROCEDURE_COMBOS[String(comboId)]; if (!preset) return form; @@ -275,6 +295,7 @@ export function applyComboToForm( // Age on the specific line date we will set const age = ageOnDate(patientDOB, lineDate); + const map = getCodeMap(insuranceSiteKey); for (let j = 0; j < preset.codes.length; j++) { const i = insertAt + j; @@ -283,7 +304,7 @@ export function applyComboToForm( const codeRaw = preset.codes[j]; if (!codeRaw) continue; const code = normalizeCode(codeRaw); - const price = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age); + const price = getPriceForCodeWithAgeFromMap(map, code, age); const original = next.serviceLines[i]; @@ -312,4 +333,4 @@ export function applyComboToForm( } -export { CODE_MAP, getPriceForCodeWithAgeFromMap }; \ No newline at end of file +export { CODE_MAP, CCA_CODE_MAP, getCodeMap, getPriceForCodeWithAgeFromMap }; \ No newline at end of file diff --git a/apps/SeleniumService/agent.py b/apps/SeleniumService/agent.py index a4ad8aef..1428e2fe 100755 --- a/apps/SeleniumService/agent.py +++ b/apps/SeleniumService/agent.py @@ -18,6 +18,7 @@ import helpers_deltains_eligibility as hdeltains import helpers_unitedsco_eligibility as hunitedsco import helpers_dentaquest_eligibility as hdentaquest import helpers_cca_eligibility as hcca +import helpers_cca_claim as hcca_claim # Import startup session-clear functions from ddma_browser_manager import clear_ddma_session_on_startup @@ -542,6 +543,47 @@ async def cca_eligibility(request: Request): return {"status": "started", "session_id": sid} +async def _cca_claim_worker_wrapper(sid: str, data: dict, url: str): + """Background worker for CCA claim submission.""" + global active_jobs, waiting_jobs + async with semaphore: + async with lock: + waiting_jobs -= 1 + active_jobs += 1 + try: + await hcca_claim.start_cca_claim_run(sid, data, url) + finally: + async with lock: + active_jobs -= 1 + + +@app.post("/cca-claim") +async def cca_claim(request: Request): + """ + Starts a CCA claim submission session in the background. + Logs in, navigates Claims > Submit Claims, opens claim entry page. + Body: { "claim": { "cca_username": "...", "cca_password": "...", ... } } + Returns: { status: "started", session_id: "" } + """ + global waiting_jobs + + body = await request.json() + + sid = hcca_claim.make_session_entry() + hcca_claim.sessions[sid]["type"] = "cca_claim" + hcca_claim.sessions[sid]["last_activity"] = time.time() + + async with lock: + waiting_jobs += 1 + + asyncio.create_task(_cca_claim_worker_wrapper( + sid, body, + url="https://pwp.sciondental.com/PWP/Landing" + )) + + return {"status": "started", "session_id": sid} + + @app.post("/submit-otp") async def submit_otp(request: Request): """ @@ -584,6 +626,8 @@ async def session_status(sid: str): s = hdentaquest.get_session_status(sid) elif sid in hcca.sessions: s = hcca.get_session_status(sid) + elif sid in hcca_claim.sessions: + s = hcca_claim.get_session_status(sid) else: s = {"status": "not_found"} if s.get("status") == "not_found": diff --git a/apps/SeleniumService/helpers_cca_claim.py b/apps/SeleniumService/helpers_cca_claim.py new file mode 100644 index 00000000..5a559a23 --- /dev/null +++ b/apps/SeleniumService/helpers_cca_claim.py @@ -0,0 +1,216 @@ +import os +import time +import asyncio +from typing import Dict, Any +from selenium.common.exceptions import WebDriverException + +from selenium_CCA_claimSubmitWorker import AutomationCCAClaimSubmit +from cca_browser_manager import get_browser_manager + +sessions: Dict[str, Dict[str, Any]] = {} + + +def make_session_entry() -> str: + import uuid + sid = str(uuid.uuid4()) + sessions[sid] = { + "status": "created", + "created_at": time.time(), + "last_activity": time.time(), + "bot": None, + "driver": None, + "result": None, + "message": None, + } + return sid + + +async def cleanup_session(sid: str, message: str | None = None): + s = sessions.get(sid) + if not s: + return + try: + if s.get("status") not in ("completed", "error"): + s["status"] = "error" + if message: + s["message"] = message + finally: + sessions.pop(sid, None) + + +async def _remove_session_later(sid: str, delay: int = 30): + await asyncio.sleep(delay) + await cleanup_session(sid) + + +def _close_browser(bot): + try: + bm = get_browser_manager() + try: + bm.save_cookies() + except Exception: + pass + try: + bm.quit_driver() + print("[CCA Claim] Browser closed") + except Exception: + pass + except Exception as e: + print(f"[CCA Claim] Could not close browser: {e}") + + +async def start_cca_claim_run(sid: str, data: dict, url: str): + """ + Run the CCA claim submission workflow: + 1. Login to ScionDental portal + 2. Navigate Claims > Submit Claims to open claim entry page + """ + s = sessions.get(sid) + if not s: + return {"status": "error", "message": "session not found"} + + s["status"] = "running" + s["last_activity"] = time.time() + bot = None + + try: + bot = AutomationCCAClaimSubmit(data) + bot.config_driver() + + s["bot"] = bot + s["driver"] = bot.driver + s["last_activity"] = time.time() + + try: + bot.driver.maximize_window() + except Exception: + pass + + # --- Login --- + try: + login_result = bot.login(url) + except WebDriverException as wde: + s["status"] = "error" + s["message"] = f"Selenium driver error during login: {wde}" + s["result"] = {"status": "error", "message": s["message"]} + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": s["message"]} + except Exception as e: + s["status"] = "error" + s["message"] = f"Unexpected error during login: {e}" + s["result"] = {"status": "error", "message": s["message"]} + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": s["message"]} + + if isinstance(login_result, str) and login_result == "ALREADY_LOGGED_IN": + print("[CCA Claim] Session persisted - skipping login") + get_browser_manager().save_cookies() + + elif isinstance(login_result, str) and login_result.startswith("ERROR"): + s["status"] = "error" + s["message"] = login_result + s["result"] = {"status": "error", "message": login_result} + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": login_result} + + elif isinstance(login_result, str) and login_result == "SUCCESS": + print("[CCA Claim] Login succeeded") + get_browser_manager().save_cookies() + + # --- Navigate to Submit Claims --- + step1_result = bot.step1_navigate_to_submit_claims() + print(f"[CCA Claim] step1 result: {step1_result}") + + if isinstance(step1_result, str) and step1_result.startswith("ERROR"): + s["status"] = "error" + s["message"] = step1_result + s["result"] = {"status": "error", "message": step1_result} + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": step1_result} + + # --- Fill patient eligibility form & verify --- + step2_result = bot.step2_fill_patient_eligibility() + print(f"[CCA Claim] step2 result: {step2_result}") + + if isinstance(step2_result, str) and step2_result.startswith("ERROR"): + # Keep browser open — log page state for debugging + try: + url = bot.driver.current_url + body = bot.driver.find_element(__import__('selenium').webdriver.common.by.By.TAG_NAME, "body").text[:600] + print(f"[CCA Claim] step2 error — URL: {url}") + print(f"[CCA Claim] step2 error — page text: {body}") + except Exception: + pass + s["status"] = "error" + s["message"] = step2_result + s["result"] = {"status": "error", "message": step2_result} + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": step2_result} + + # --- Fill Services grid, attach docs, submit --- + step3_result = bot.step3_fill_services_and_submit() + print(f"[CCA Claim] step3 result: {step3_result}") + + if isinstance(step3_result, str) and step3_result.startswith("ERROR"): + try: + url = bot.driver.current_url + body = bot.driver.find_element(__import__('selenium').webdriver.common.by.By.TAG_NAME, "body").text[:600] + print(f"[CCA Claim] step3 error — URL: {url}") + print(f"[CCA Claim] step3 error — page text: {body}") + except Exception: + pass + s["status"] = "error" + s["message"] = step3_result + s["result"] = {"status": "error", "message": step3_result} + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": step3_result} + + # --- Capture confirmation PDF + claim number --- + step4_result = bot.step4_capture_confirmation() + print(f"[CCA Claim] step4 claimNumber={step4_result.get('claimNumber')}, " + f"pdfLen={len(step4_result.get('pdfBase64',''))}") + + _close_browser(bot) + + result = { + "status": "success", + "message": "CCA claim submitted successfully", + "claimNumber": step4_result.get("claimNumber"), + "pdfBase64": step4_result.get("pdfBase64", ""), + "pdfFilename": step4_result.get("pdfFilename", ""), + } + s["status"] = "completed" + s["result"] = result + s["message"] = "completed" + asyncio.create_task(_remove_session_later(sid, 60)) + return result + + except Exception as e: + if s: + s["status"] = "error" + s["message"] = f"worker exception: {e}" + s["result"] = {"status": "error", "message": s["message"]} + if bot: + _close_browser(bot) + asyncio.create_task(_remove_session_later(sid, 30)) + return {"status": "error", "message": f"worker exception: {e}"} + + +def get_session_status(sid: str) -> Dict[str, Any]: + s = sessions.get(sid) + if not s: + return {"status": "not_found"} + return { + "session_id": sid, + "status": s.get("status"), + "message": s.get("message"), + "created_at": s.get("created_at"), + "last_activity": s.get("last_activity"), + "result": s.get("result") if s.get("status") in ("completed", "error") else None, + } diff --git a/apps/SeleniumService/selenium_CCA_claimSubmitWorker.py b/apps/SeleniumService/selenium_CCA_claimSubmitWorker.py new file mode 100644 index 00000000..0b94a978 --- /dev/null +++ b/apps/SeleniumService/selenium_CCA_claimSubmitWorker.py @@ -0,0 +1,977 @@ +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import time +import os +import re +import json +import base64 +import tempfile +from datetime import date + +from cca_browser_manager import get_browser_manager + +LANDING_URL = "https://pwp.sciondental.com/PWP/Landing" +CLAIM_ENTRY_URL = "https://pwp.sciondental.com/PWP/Dental/ClaimEntry" + + +class AutomationCCAClaimSubmit: + def __init__(self, data): + self.headless = False + self.driver = None + + raw = data if isinstance(data, dict) else {} + claim = raw.get("claim", {}) or {} + + patient_name = (claim.get("patientName") or "").strip() + parts = patient_name.split() + first_name = parts[0] if parts else "" + last_name = " ".join(parts[1:]) if len(parts) > 1 else "" + + self.cca_username = claim.get("cca_username", "") or raw.get("cca_username", "") + self.cca_password = claim.get("cca_password", "") or raw.get("cca_password", "") + self.memberId = claim.get("memberId", "") + self.dateOfBirth = claim.get("dateOfBirth", "") + self.serviceDate = claim.get("serviceDate", "") + self.firstName = claim.get("firstName", "") or first_name + self.lastName = claim.get("lastName", "") or last_name + self.serviceLines = claim.get("serviceLines", []) or [] + # Files: list of {originalname, bufferBase64, mimetype} + files_raw = raw.get("files", []) or [] + self.uploadFiles = [f for f in files_raw if f.get("bufferBase64")] + + print(f"[CCA Claim] Init — member={self.memberId}, " + f"patient={self.firstName} {self.lastName}, " + f"lines={len(self.serviceLines)}, files={len(self.uploadFiles)}") + + def config_driver(self): + self.driver = get_browser_manager().get_driver(self.headless) + + # ------------------------------------------------------------------ # + # Login (same logic as eligibility worker) # + # ------------------------------------------------------------------ # + def _page_has_logged_in_content(self): + try: + body = self.driver.find_element(By.TAG_NAME, "body").text + return any(x in body for x in [ + "Verify Patient Eligibility", "Patient Management", + "Submit a Claim", "Claim Inquiry", "Submit Claims", + ]) + except Exception: + return False + + def login(self, url): + browser_manager = get_browser_manager() + try: + if self.cca_username and browser_manager.credentials_changed(self.cca_username): + try: + self.driver.delete_all_cookies() + except Exception: + pass + browser_manager.clear_credentials_hash() + self.driver.get(url) + time.sleep(2) + + try: + current_url = self.driver.current_url + if ("sciondental.com" in current_url + and "login" not in current_url.lower() + and self._page_has_logged_in_content()): + print("[CCA Claim login] Already logged in") + return "ALREADY_LOGGED_IN" + except Exception: + pass + + print("[CCA Claim login] Checking session at landing page...") + self.driver.get(LANDING_URL) + try: + WebDriverWait(self.driver, 10).until( + lambda d: "sciondental.com" in d.current_url + and d.execute_script("return document.readyState") == "complete" + ) + except TimeoutException: + pass + + if self._page_has_logged_in_content(): + print("[CCA Claim login] Session still valid") + return "ALREADY_LOGGED_IN" + + print("[CCA Claim login] Session expired — logging in...") + self.driver.get(url) + try: + WebDriverWait(self.driver, 15).until( + EC.presence_of_element_located((By.XPATH, "//input"))) + except TimeoutException: + pass + + # Username + username_field = None + for sel in [ + (By.ID, "Username"), + (By.NAME, "Username"), + (By.XPATH, "//input[@type='text']"), + (By.XPATH, "//input[@type='email']"), + ]: + try: + f = WebDriverWait(self.driver, 6).until(EC.element_to_be_clickable(sel)) + username_field = f + break + except Exception: + continue + + if not username_field: + if self._page_has_logged_in_content(): + return "ALREADY_LOGGED_IN" + return "ERROR: Could not find username field" + + username_field.click() + username_field.send_keys(Keys.CONTROL + "a") + username_field.send_keys(Keys.DELETE) + username_field.send_keys(self.cca_username) + time.sleep(0.3) + + # Password + pw_field = None + for sel in [ + (By.ID, "Password"), + (By.NAME, "Password"), + (By.XPATH, "//input[@type='password']"), + ]: + try: + f = WebDriverWait(self.driver, 6).until(EC.element_to_be_clickable(sel)) + pw_field = f + break + except Exception: + continue + + if not pw_field: + return "ERROR: Password field not found" + + pw_field.click() + pw_field.send_keys(Keys.CONTROL + "a") + pw_field.send_keys(Keys.DELETE) + pw_field.send_keys(self.cca_password) + time.sleep(0.3) + + # Submit + submitted = False + for sel in [ + (By.XPATH, "//button[@type='submit']"), + (By.XPATH, "//input[@type='submit']"), + (By.XPATH, "//button[contains(text(),'Sign In') or contains(text(),'Log In') or contains(text(),'Login')]"), + ]: + try: + btn = self.driver.find_element(*sel) + if btn.is_displayed(): + btn.click() + submitted = True + break + except Exception: + continue + + if not submitted: + pw_field.send_keys(Keys.RETURN) + + if self.cca_username: + browser_manager.save_credentials_hash(self.cca_username) + + try: + WebDriverWait(self.driver, 20).until( + lambda d: any(x in d.find_element(By.TAG_NAME, "body").text + for x in ["Verify Patient Eligibility", "Patient Management", + "Submit a Claim", "Claim Inquiry", "Submit Claims"])) + print("[CCA Claim login] Login succeeded") + return "SUCCESS" + except TimeoutException: + pass + + body_text = self.driver.find_element(By.TAG_NAME, "body").text + if "invalid" in body_text.lower(): + return "ERROR: Invalid username or password" + return "ERROR: Login did not succeed — portal content not found after submit" + + except Exception as e: + print(f"[CCA Claim login] Exception: {e}") + return f"ERROR:LOGIN FAILED: {e}" + + # ------------------------------------------------------------------ # + # Step 1 — Navigate to Claims > Submit Claims # + # ------------------------------------------------------------------ # + def step1_navigate_to_submit_claims(self): + """ + Click the 'Claims' navbar dropdown then choose 'Submit Claims'. + Returns 'SUCCESS' when the claim-entry page is loaded, else 'ERROR:...'. + """ + try: + # Navigate directly to the claim entry URL (no menu navigation needed) + print(f"[CCA Claim step1] Navigating directly to {CLAIM_ENTRY_URL}") + self.driver.get(CLAIM_ENTRY_URL) + + # Wait for the Subscriber ID field — confirms we're on the right page + try: + WebDriverWait(self.driver, 20).until( + EC.presence_of_element_located((By.ID, "tbSubscriberId")) + ) + print(f"[CCA Claim step1] Claim entry page loaded — URL: {self.driver.current_url}") + except TimeoutException: + # May have been redirected to login — check and re-login if needed + print(f"[CCA Claim step1] tbSubscriberId not found, URL: {self.driver.current_url}") + if "login" in self.driver.current_url.lower() or not self._page_has_logged_in_content(): + return "ERROR: Session expired — redirected to login" + print("[CCA Claim step1] Continuing despite timeout") + + time.sleep(1) + return "SUCCESS" + + except Exception as e: + print(f"[CCA Claim step1] Exception: {e}") + return f"ERROR: step1 failed: {e}" + + def _set_ng_value(self, field, value: str): + """Set value on an AngularJS ng-model input and trigger digest.""" + try: + self.driver.execute_script( + """ + var el = arguments[0], val = arguments[1]; + var setter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, 'value').set; + setter.call(el, val); + el.dispatchEvent(new Event('input', {bubbles:true})); + el.dispatchEvent(new Event('change', {bubbles:true})); + """, + field, value + ) + except Exception: + field.click() + field.send_keys(Keys.CONTROL + "a") + field.send_keys(Keys.DELETE) + field.send_keys(value) + + # ------------------------------------------------------------------ # + # Step 2 — Expand panel, fill Patient/Provider info, Verify Eligibility + # ------------------------------------------------------------------ # + def _js_set(self, element_id: str, value: str) -> bool: + """Set an input value via JavaScript — works regardless of CSS visibility.""" + try: + el = self.driver.find_element(By.ID, element_id) + self.driver.execute_script( + "var el=arguments[0], v=arguments[1];" + "var s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;" + "s.call(el,v);" + "el.dispatchEvent(new Event('input',{bubbles:true}));" + "el.dispatchEvent(new Event('change',{bubbles:true}));", + el, value + ) + actual = el.get_attribute("value") + print(f"[CCA Claim step2] JS set {element_id!r} = {actual!r}") + return True + except Exception as e: + print(f"[CCA Claim step2] JS set {element_id!r} failed: {e}") + return False + + def _keys_set(self, element_id: str, value: str) -> bool: + """Set an input via send_keys — needed for masked date fields.""" + try: + el = self.driver.find_element(By.ID, element_id) + el.click() + el.send_keys(Keys.CONTROL + "a") + el.send_keys(Keys.DELETE) + el.send_keys(value) + time.sleep(0.3) + actual = el.get_attribute("value") + print(f"[CCA Claim step2] keys set {element_id!r} = {actual!r}") + return True + except Exception as e: + print(f"[CCA Claim step2] keys set {element_id!r} failed: {e}") + return False + + def step2_fill_patient_eligibility(self): + """ + Fill Subscriber ID, Date of Birth, Date of Service then click Verify Eligibility. + All panels and tabs are already open on the claim entry page. + Returns 'SUCCESS' or 'ERROR:...'. + """ + try: + formatted_dob = self._format_dob(self.dateOfBirth) + formatted_dos = self._format_dob(self.serviceDate) if self.serviceDate else "" + print(f"[CCA Claim step2] memberId={self.memberId!r}, DOB={formatted_dob!r}, DOS={formatted_dos!r}") + + # Wait for form to be ready + print("[CCA Claim step2] Waiting for tbSubscriberId in DOM...") + try: + WebDriverWait(self.driver, 20).until( + EC.presence_of_element_located((By.ID, "tbSubscriberId")) + ) + print("[CCA Claim step2] tbSubscriberId present in DOM") + except Exception: + print("[CCA Claim step2] tbSubscriberId not in DOM after 20s") + return "ERROR: Subscriber ID field not found in DOM" + + time.sleep(1) + + # --- Subscriber ID (JS set) --- + if not self._js_set("tbSubscriberId", self.memberId): + return "ERROR: Could not set Subscriber ID" + + # --- Date of Birth (send_keys for date mask) --- + if not self._keys_set("tbDateOfBirth", formatted_dob): + return "ERROR: Could not set Date of Birth" + + # --- Date of Service (send_keys for date mask) --- + if formatted_dos: + self._keys_set("tbDateOfService", formatted_dos) + + # --- Click Verify Eligibility --- + print("[CCA Claim step2] Clicking 'Verify Eligibility'...") + verified = False + for sel in [ + (By.ID, "ButtonVerifyMemberEligibility"), + (By.XPATH, "//button[@id='ButtonVerifyMemberEligibility']"), + (By.XPATH, "//button[contains(text(),'Verify Eligibility')]"), + ]: + try: + btn = WebDriverWait(self.driver, 8).until(EC.element_to_be_clickable(sel)) + btn.click() + verified = True + print(f"[CCA Claim step2] Clicked 'Verify Eligibility' via {sel}") + break + except Exception: + continue + + if not verified: + return "ERROR: 'Verify Eligibility' button not found" + + # Wait for eligibility result + print("[CCA Claim step2] Waiting for eligibility result...") + try: + WebDriverWait(self.driver, 25).until( + lambda d: any(x in d.find_element(By.TAG_NAME, "body").text.lower() for x in [ + "eligible", "not eligible", "ineligible", + "patient name", "member name", "verified", + ]) + ) + print("[CCA Claim step2] Eligibility result appeared") + except TimeoutException: + print(f"[CCA Claim step2] Timed out waiting for eligibility result — URL: {self.driver.current_url}") + + time.sleep(1) + return "SUCCESS" + + except Exception as e: + print(f"[CCA Claim step2] Exception: {e}") + return f"ERROR: step2 failed: {e}" + + # ------------------------------------------------------------------ # + # Step 4 — Extract claim number + capture confirmation PDF # + # ------------------------------------------------------------------ # + def extract_claim_number(self): + """Extract claim/reference number from the CCA submission confirmation page.""" + try: + WebDriverWait(self.driver, 15).until( + lambda d: d.execute_script("return document.readyState") == "complete" + ) + time.sleep(1) + + body_text = self.driver.find_element(By.TAG_NAME, "body").text + print(f"[CCA Claim step4] Confirmation page text (first 600): {body_text[:600]}") + + # Try DOM selectors first + for sel in [ + (By.XPATH, "//*[contains(text(),'Claim Number') or contains(text(),'claim number')]/following-sibling::*[1]"), + (By.XPATH, "//*[contains(text(),'Claim Number') or contains(text(),'claim number')]/following::*[1]"), + (By.XPATH, "//*[contains(text(),'Reference Number') or contains(text(),'Confirmation Number')]/following-sibling::*[1]"), + (By.XPATH, "//*[contains(text(),'assigned the number')]/following::*[1]"), + (By.XPATH, "//*[@id and contains(@id,'ClaimNumber')]"), + (By.XPATH, "//*[@id and contains(@id,'ConfirmationNumber')]"), + ]: + try: + el = self.driver.find_element(*sel) + text = el.text.strip() + if text and re.search(r'\d{6,}', text): + print(f"[CCA Claim step4] Claim number via DOM: {text}") + return re.search(r'[\w\-]{6,}', text).group(0) + except Exception: + continue + + # Regex scan over full page text + # Pattern: "assigned the number XXXXXXXXX" or "Claim Number: XXXXXXX" + for pattern in [ + r'assigned\s+the\s+number\s+([\w\-]{6,30})', + r'[Cc]laim\s+[Nn]umber[:\s]+([A-Z0-9\-]{6,30})', + r'[Cc]onfirmation\s+[Nn]umber[:\s]+([A-Z0-9\-]{6,30})', + r'[Rr]eference\s+[Nn]umber[:\s]+([A-Z0-9\-]{6,30})', + r'(\d{15})', # MassHealth-style 15-digit + r'(\d{9,14})', # 9-14 digit fallback + ]: + m = re.search(pattern, body_text) + if m: + print(f"[CCA Claim step4] Claim number via regex ({pattern}): {m.group(1)}") + return m.group(1) + + # JavaScript fallback + claim_number = self.driver.execute_script(r""" + var els = document.querySelectorAll('body, p, div, span, td, label, h1, h2, h3, strong, b'); + for (var i = 0; i < els.length; i++) { + var t = (els[i].textContent || '').trim(); + var m = t.match(/assigned\s+the\s+number\s+([\w\-]{6,30})/i) + || t.match(/[Cc]laim\s+[Nn]umber[:\s]+([\w\-]{6,30})/) + || t.match(/(\d{15})/) + || t.match(/(\d{9,14})/); + if (m) return m[1]; + } + return null; + """) + if claim_number: + print(f"[CCA Claim step4] Claim number via JS: {claim_number}") + return claim_number + + print("[CCA Claim step4] Could not extract claim number") + return None + except Exception as e: + print(f"[CCA Claim step4] extract_claim_number exception: {e}") + return None + + def step4_capture_confirmation(self): + """ + On the Claims Dashboard page, extract the Encounter ID from the first row + and capture the page as PDF. + Returns dict: { claimNumber, pdfBase64, pdfFilename }. + """ + try: + WebDriverWait(self.driver, 15).until( + lambda d: d.execute_script("return document.readyState") == "complete" + ) + time.sleep(1) + print(f"[CCA Claim step4] Capturing dashboard — URL: {self.driver.current_url}") + + # Extract Encounter ID from the LAST row (newest claim is at the bottom) + claim_number = None + + # Try table cells first (Encounter ID is the first column) + for sel in [ + (By.XPATH, "//table//tbody//tr[last()]/td[1]"), + (By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[last()]/td[1]"), + (By.XPATH, "//table//tbody//tr[last()-0]/td[1]"), + ]: + try: + cell = WebDriverWait(self.driver, 8).until( + EC.presence_of_element_located(sel)) + text = cell.text.strip() + if re.match(r'\d{10,}', text): + claim_number = text + print(f"[CCA Claim step4] Encounter ID from last row: {claim_number}") + break + except Exception: + continue + + # Fallback: regex scan on full page text + if not claim_number: + claim_number = self.extract_claim_number() + + 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") + safe_claim = ("_" + re.sub(r'[^\w\-]', '', str(claim_number))[:20]) if claim_number else "" + pdf_filename = f"cca_claim_{safe_member}{safe_claim}_{timestamp}.pdf" + + print(f"[CCA Claim step4] Capturing confirmation PDF — {self.driver.current_url}") + + # Primary: CDP printToPDF + try: + pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", { + "printBackground": True, + "paperWidth": 8.5, + "paperHeight": 11, + "marginTop": 0.4, + "marginBottom": 0.4, + "marginLeft": 0.4, + "marginRight": 0.4, + }) + pdf_base64 = pdf_data.get("data", "") + if pdf_base64 and len(pdf_base64) > 500: + print(f"[CCA Claim step4] PDF captured via CDP ({len(pdf_base64)} b64 chars)") + return { + "claimNumber": claim_number, + "pdfBase64": pdf_base64, + "pdfFilename": pdf_filename, + } + except Exception as e: + print(f"[CCA Claim step4] CDP PDF failed: {e}") + + # Fallback: screenshot as PNG + try: + png_filename = pdf_filename.replace(".pdf", ".png") + total_height = self.driver.execute_script("return document.body.scrollHeight") + self.driver.set_window_size(1280, max(total_height, 900)) + time.sleep(0.5) + png_base64 = self.driver.get_screenshot_as_base64() + print(f"[CCA Claim step4] Screenshot fallback captured") + return { + "claimNumber": claim_number, + "pdfBase64": png_base64, + "pdfFilename": png_filename, + } + except Exception as e2: + print(f"[CCA Claim step4] Screenshot fallback failed: {e2}") + + return {"claimNumber": claim_number, "pdfBase64": "", "pdfFilename": ""} + + except Exception as e: + print(f"[CCA Claim step4] Exception: {e}") + return {"claimNumber": None, "pdfBase64": "", "pdfFilename": ""} + + def _format_dob(self, dob_str): + """Normalize any common date format to MM/DD/YYYY.""" + if not dob_str: + return dob_str + s = str(dob_str).strip() + # Already MM/DD/YYYY or M/D/YYYY + if re.match(r'^\d{1,2}/\d{1,2}/\d{4}$', s): + return s + # YYYY-MM-DD or MM-DD-YYYY (detect by length of first segment) + if "-" in s: + parts = s.split("-") + if len(parts) == 3: + if len(parts[0]) == 4: + # YYYY-MM-DD → MM/DD/YYYY + return f"{parts[1]}/{parts[2]}/{parts[0]}" + if len(parts[2]) == 4: + # MM-DD-YYYY → MM/DD/YYYY + return f"{parts[0]}/{parts[1]}/{parts[2]}" + return s + + # ------------------------------------------------------------------ # + # Fee schedule helpers # + # ------------------------------------------------------------------ # + def _load_cca_fee_schedule(self): + base = os.path.dirname(os.path.abspath(__file__)) + json_path = os.path.join( + base, "..", "Frontend", "src", "assets", "data", "procedureCodesCCA.json" + ) + try: + with open(json_path) as f: + rows = json.load(f) + fee_map = {} + for row in rows: + code = str(row.get("Procedure Code", "")).strip().upper() + if code: + fee_map[code] = row + print(f"[CCA Claim] Loaded {len(fee_map)} CCA fee codes") + return fee_map + except Exception as e: + print(f"[CCA Claim] Could not load CCA fee schedule: {e}") + return {} + + def _get_patient_age(self): + if not self.dateOfBirth: + return None + try: + parts = self.dateOfBirth.split("-") + dob = date(int(parts[0]), int(parts[1]), int(parts[2])) + today = date.today() + return today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day)) + except Exception: + return None + + def _get_fee(self, code, fee_map, age): + row = fee_map.get(str(code).strip().upper(), {}) + if not row: + return "" + if "Price" in row: + val = str(row["Price"]) + elif age is not None and age <= 21: + val = str(row.get("PriceLTEQ21") or row.get("PriceGT21") or "") + else: + val = str(row.get("PriceGT21") or row.get("PriceLTEQ21") or "") + return "" if val in ("NC", "IC", "None", "") else val + + # ------------------------------------------------------------------ # + # EJ Grid cell fill helper # + # ------------------------------------------------------------------ # + def _build_col_map(self): + """ + Map column names to 1-based td positions using ej-mappingname divs. + Falls back to hardcoded positions based on the known PDF column order. + """ + # Hardcoded based on PDF column order: + # rownum(1), Code(2), Desc(3), Tooth(4), + # Surf1-5(5-9), OralCavity1-4(10-13), DiagPtr1-4(14-17), + # EPSDT(18), Qty(19), Auth(20), ServiceDate(21), BilledAmt(22) + col_map = { + "CODE": 2, + "TOOTH": 4, + "SURF1": 5, "S1": 5, + "SURF2": 6, "S2": 6, + "SURF3": 7, "S3": 7, + "SURF4": 8, "S4": 8, + "SURF5": 9, "S5": 9, + "QUANTITY": 19, "QTY": 19, + "DOS": 21, "SERVICE DATE": 21, "SERVICEDATE": 21, + "BILLED_AMOUNT": 22, "BILLED AMT": 22, "BILLED A": 22, + } + + # Try to confirm/override positions using ej-mappingname divs in header + try: + divs = self.driver.find_elements( + By.XPATH, + "//div[@id='Services']//div[@ej-mappingname] | " + "//div[@id='ServicesGrid']//div[@ej-mappingname]" + ) + print(f"[CCA Claim grid] Found {len(divs)} ej-mappingname header divs") + for div in divs: + mapping = div.get_attribute("ej-mappingname") or "" + text = div.text.strip() + print(f"[CCA Claim grid] Header: mapping={mapping!r} text={text!r}") + except Exception as e: + print(f"[CCA Claim grid] Header scan error: {e}") + + return col_map + + def _dbl_click_col(self, row_num, col_idx): + """Double-click the cell at (row_num, col_idx) — both 1-based. + row_num is 1-based (first data row = 1), col_idx is 1-based td position.""" + from selenium.webdriver.common.action_chains import ActionChains + row_idx = row_num - 1 # EJ Grid aria-rowindex is 0-based + for xpath in [ + # aria-rowindex is most reliable in EJ Grid + f"//tr[@aria-rowindex='{row_idx}']/td[{col_idx}]", + # Scoped to Services grid, position-based + f"(//div[@id='Services']//tr[.//td[contains(@class,'e-rowcell')]])[{row_num}]/td[{col_idx}]", + f"(//div[@id='Services']//tbody//tr)[{row_num}]/td[{col_idx}]", + f"(//tr[contains(@class,'e-row')])[{row_num}]/td[{col_idx}]", + ]: + try: + cell = self.driver.find_element(By.XPATH, xpath) + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});", cell) + time.sleep(0.2) + ActionChains(self.driver).double_click(cell).perform() + time.sleep(0.5) + visible_inputs = self.driver.execute_script( + "return Array.from(document.querySelectorAll('input[id]'))" + ".filter(function(i){return i.offsetParent!==null})" + ".map(function(i){return i.id})") + print(f"[CCA Claim grid] Dbl-clicked row={row_num} col={col_idx}, " + f"visible inputs: {visible_inputs[:8]}") + return True + except Exception as e: + print(f"[CCA Claim grid] dbl_click_col({row_num},{col_idx}) failed: {e}") + continue + return False + + def _fill_active_input(self, input_id, value): + """Fill the input that appeared after double-clicking a cell.""" + try: + field = WebDriverWait(self.driver, 5).until( + EC.presence_of_element_located((By.ID, input_id)) + ) + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});", field) + field.click() + field.send_keys(Keys.CONTROL + "a") + field.send_keys(Keys.DELETE) + if value: + field.send_keys(str(value)) + time.sleep(0.2) + print(f"[CCA Claim grid] Filled {input_id}={value!r}") + return True + except Exception as e: + print(f"[CCA Claim grid] Could not fill {input_id}: {e}") + return False + + def _fill_cell(self, input_id, value, tab_after=True): + """Legacy helper kept for compatibility.""" + return self._fill_active_input(input_id, value) + + # ------------------------------------------------------------------ # + # Step 3 — Fill Services grid, attach docs, submit # + # ------------------------------------------------------------------ # + def step3_fill_services_and_submit(self): + """ + 1. Expand Services panel and add one grid row per service line. + Per row: CODE → TOOTH → SURF1-5 → QTY(1) → DOS → BILLED_AMOUNT + 2. Expand Attached Documents, upload files if present. + 3. Click Submit Claim. + """ + try: + fee_map = self._load_cca_fee_schedule() + age = self._get_patient_age() + formatted_dos = self._format_dob(self.serviceDate) if self.serviceDate else "" + + active_lines = [ + ln for ln in self.serviceLines + if str(ln.get("procedureCode") or "").strip() + ] + print(f"[CCA Claim step3] {len(active_lines)} active service line(s)") + + # Services panel is already open on the claim entry page + # Build column position map from grid headers + col_map = self._build_col_map() + print(f"[CCA Claim step3] Column map: {col_map}") + + # ---- Add each service line ---- + for idx, line in enumerate(active_lines): + code = str(line.get("procedureCode") or "").strip() + tooth = str(line.get("toothNumber") or "").strip() + surface_raw = str(line.get("toothSurface") or "").strip() + surface_chars = [c for c in surface_raw.upper() if c.isalpha()] + + # Determine billed amount: prefer value already on the line + total_billed = line.get("totalBilled") or line.get("fee") or line.get("billedAmount") + if total_billed: + billed_str = str(total_billed) + else: + billed_str = self._get_fee(code, fee_map, age) + + print(f"[CCA Claim step3] Line {idx+1}: code={code}, tooth={tooth}, " + f"surf={surface_raw!r}, billed={billed_str}") + + # --- Activate inline edit for row (idx) --- + row_idx = idx # 0-based for EJ Grid API + row_num = idx + 1 # 1-based for XPath + cell_clicked = False + + # --- Single-click cell to activate (yellow), then fill input --- + def click_cell_and_fill(col_expr, input_id, value): + """ + Click the cell at (row_num, col_expr) to activate it (turns yellow), + then fill the pre-rendered input by ID. + col_expr can be a number like 2 or an XPath expression like 'last()' or 'last()-1'. + """ + # Find the cell in the grid CONTENT rows (not headers) + cell = None + for xpath in [ + f"(//div[contains(@class,'e-gridcontent')]//tr)[{row_num}]/td[{col_expr}]", + f"(//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]", + f"(//div[@id='Services']//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]", + f"(//tr[@aria-rowindex='{row_num-1}'])/td[{col_expr}]", + ]: + try: + cell = self.driver.find_element(By.XPATH, xpath) + break + except Exception: + continue + + if cell: + try: + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});", + cell) + cell.click() + time.sleep(0.3) + print(f"[CCA Claim grid] Clicked row={row_num} col={col_idx}") + except Exception as e: + print(f"[CCA Claim grid] Cell click failed: {e}") + else: + print(f"[CCA Claim grid] Cell not found row={row_num} col={col_idx}") + + # Fill the pre-rendered input + try: + field = WebDriverWait(self.driver, 4).until( + EC.presence_of_element_located((By.ID, input_id))) + self.driver.execute_script( + "arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});", + field) + field.click() + field.send_keys(Keys.CONTROL + "a") + field.send_keys(Keys.DELETE) + if value: + field.send_keys(str(value)) + time.sleep(0.2) + print(f"[CCA Claim grid] Filled {input_id}={value!r}") + return True + except Exception as e: + print(f"[CCA Claim grid] Fill {input_id} failed: {e}") + return False + + # CODE (col 2) + click_cell_and_fill(2, "ServicesCODE", code) + + # TOOTH (col 4) — skip if empty + if tooth: + click_cell_and_fill(4, "ServicesTOOTH", tooth) + + # Surfaces (col 5-9) — skip if empty + if surface_chars: + for si, char in enumerate(surface_chars[:5]): + surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3", + "ServicesSURF4","ServicesSURF5"] + click_cell_and_fill(5 + si, surf_ids[si], char) + + # Billed Amount — last column + # QTY and Service Date auto-fill after CODE is entered and this cell is clicked + click_cell_and_fill("last()", "ServicesBILLED_AMOUNT", billed_str) + + time.sleep(0.3) + + # ---- Attached Documents ---- + if self.uploadFiles: + print(f"[CCA Claim step3] Attaching {len(self.uploadFiles)} file(s)...") + + # Expand Attached Documents panel if collapsed + for sel in [ + (By.XPATH, "//h4[contains(@class,'panel-title') and contains(.,'Attached Documents')]"), + (By.XPATH, "//*[contains(@class,'panel-title') and contains(.,'Attached Documents')]"), + ]: + try: + el = WebDriverWait(self.driver, 6).until(EC.element_to_be_clickable(sel)) + try: + chevron = el.find_element(By.XPATH, ".//*[contains(@class,'fa-chevron-down')]") + if chevron.is_displayed(): + el.click() + time.sleep(0.8) + print("[CCA Claim step3] Attached Documents panel expanded") + except Exception: + pass + break + except Exception: + continue + + # Write files preserving the original filename so the portal sees the correct name + tmp_dir = tempfile.mkdtemp() + temp_paths = [] + for f in self.uploadFiles: + try: + original_name = f.get("originalname", "attachment.pdf") + # Sanitise filename — keep extension, replace unsafe chars + safe_name = re.sub(r'[^\w.\-]', '_', original_name) + tmp_path = os.path.join(tmp_dir, safe_name) + with open(tmp_path, "wb") as fh: + fh.write(base64.b64decode(f["bufferBase64"])) + temp_paths.append(tmp_path) + print(f"[CCA Claim step3] Wrote attachment: {tmp_path}") + except Exception as fe: + print(f"[CCA Claim step3] Failed to write attachment: {fe}") + + if temp_paths: + # Make the hidden file input interactable and send paths + file_input_found = False + for sel in [ + (By.XPATH, "//input[@type='file']"), + (By.XPATH, "//button[@id='AttachDocumentButton']/preceding::input[@type='file'][1]"), + (By.XPATH, "//button[@id='AttachDocumentButton']/following::input[@type='file'][1]"), + ]: + try: + file_input = self.driver.find_element(*sel) + self.driver.execute_script( + "arguments[0].style.display='block'; " + "arguments[0].style.visibility='visible'; " + "arguments[0].style.opacity='1';", + file_input + ) + file_input.send_keys("\n".join(temp_paths)) + file_input_found = True + print(f"[CCA Claim step3] Files sent to input, waiting for upload...") + break + except Exception: + continue + + if not file_input_found: + try: + btn = WebDriverWait(self.driver, 6).until( + EC.element_to_be_clickable((By.ID, "AttachDocumentButton"))) + btn.click() + print("[CCA Claim step3] Clicked AttachDocumentButton") + except Exception: + print("[CCA Claim step3] AttachDocumentButton not found") + + # Wait for the attachment to appear in the Attached Documents section + print("[CCA Claim step3] Waiting for attachment to upload...") + try: + # The panel shows file names once uploaded; wait for any filename to appear + WebDriverWait(self.driver, 30).until( + lambda d: any( + re.sub(r'[^\w.\-]', '_', f.get("originalname", ""))[:10].lower() + in d.find_element(By.TAG_NAME, "body").text.lower() + for f in self.uploadFiles + ) or "Attached Documents (1)" in d.find_element(By.TAG_NAME, "body").text + or "Attached Documents (2)" in d.find_element(By.TAG_NAME, "body").text + ) + print("[CCA Claim step3] Attachment confirmed on page") + except TimeoutException: + print("[CCA Claim step3] Attachment upload wait timed out — proceeding") + + # Clean up temp files + try: + import shutil + shutil.rmtree(tmp_dir, ignore_errors=True) + except Exception: + pass + + # ---- Submit Claim ---- + print("[CCA Claim step3] Clicking 'Submit Claim'...") + submitted = False + for sel in [ + (By.ID, "SaveClaimButton"), + (By.XPATH, "//button[@id='SaveClaimButton']"), + (By.XPATH, "//button[@ng-click and contains(@ng-click,'SubmitClaim')]"), + (By.XPATH, "//button[contains(text(),'Submit Claim')]"), + ]: + try: + btn = WebDriverWait(self.driver, 8).until(EC.element_to_be_clickable(sel)) + btn.click() + submitted = True + print(f"[CCA Claim step3] Clicked Submit Claim via {sel}") + break + except Exception: + continue + + if not submitted: + return "ERROR: Submit Claim button not found or not clickable" + + # ---- Handle confirmation modal popup ---- + print("[CCA Claim step3] Waiting for confirmation modal...") + time.sleep(1.5) + modal_confirmed = False + for sel in [ + # Most specific: Submit/Confirm/Yes button inside an active modal + (By.XPATH, "//div[contains(@class,'modal') and contains(@class,'in')]//button[contains(text(),'Submit') or contains(text(),'Confirm') or contains(text(),'Yes')]"), + (By.XPATH, "//div[@class='modal-footer']//button[contains(text(),'Submit') or contains(text(),'Confirm') or contains(text(),'Yes')]"), + (By.XPATH, "//button[@ng-click and (contains(@ng-click,'confirm') or contains(@ng-click,'Confirm') or contains(@ng-click,'submit') or contains(@ng-click,'Submit'))]"), + (By.XPATH, "//div[contains(@class,'modal')]//button[not(contains(@class,'cancel')) and not(contains(text(),'Cancel')) and not(contains(text(),'No'))]"), + ]: + try: + btn = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable(sel)) + btn.click() + modal_confirmed = True + print(f"[CCA Claim step3] Clicked modal confirm via {sel}") + break + except Exception: + continue + + if not modal_confirmed: + print("[CCA Claim step3] No confirmation modal found — may have auto-submitted") + + # Wait for final confirmation page + print("[CCA Claim step3] Waiting for submission confirmation...") + try: + WebDriverWait(self.driver, 30).until( + lambda d: any(x in d.find_element(By.TAG_NAME, "body").text.lower() for x in [ + "claim submitted", "claim number", "confirmation", "success", + "has been submitted", "claim id", "assigned the number", + ]) + ) + print("[CCA Claim step3] Claim submission confirmed") + except TimeoutException: + print(f"[CCA Claim step3] Confirmation not detected — URL: {self.driver.current_url}") + + # Navigate to Claims Dashboard to get Encounter ID and save as PDF + print("[CCA Claim step3] Navigating to Claims Dashboard...") + time.sleep(2) + self.driver.get("https://pwp.sciondental.com/PWP/Dental/ClaimDashboard") + try: + WebDriverWait(self.driver, 20).until( + lambda d: "Dashboard" in d.find_element(By.TAG_NAME, "body").text + or "Encounter" in d.find_element(By.TAG_NAME, "body").text + ) + print(f"[CCA Claim step3] Dashboard loaded — URL: {self.driver.current_url}") + except TimeoutException: + print("[CCA Claim step3] Dashboard load timed out") + + time.sleep(2) + return "SUCCESS" + + except Exception as e: + print(f"[CCA Claim step3] Exception: {e}") + return f"ERROR: step3 failed: {e}" diff --git a/apps/SeleniumService/selenium_CCA_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_CCA_eligibilityCheckWorker.py index 7e018da3..68eb3284 100644 --- a/apps/SeleniumService/selenium_CCA_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_CCA_eligibilityCheckWorker.py @@ -131,10 +131,10 @@ class AutomationCCAEligibilityCheck: try: WebDriverWait(self.driver, 10).until( lambda d: "sciondental.com" in d.current_url + and d.execute_script("return document.readyState") == "complete" ) except TimeoutException: pass - time.sleep(2) current_url = self.driver.current_url print(f"[CCA login] After landing nav URL: {current_url}") @@ -147,14 +147,13 @@ class AutomationCCAEligibilityCheck: print("[CCA login] Session not valid, navigating to login page...") self.driver.get(url) - # Wait up to 15s for ANY input to appear, then snapshot the page + # Wait for login inputs to appear try: WebDriverWait(self.driver, 15).until( EC.presence_of_element_located((By.XPATH, "//input")) ) except TimeoutException: pass - time.sleep(1) current_url = self.driver.current_url print(f"[CCA login] After login nav URL: {current_url}")