diff --git a/apps/Backend/src/routes/payments.ts b/apps/Backend/src/routes/payments.ts index 6c2266e9..91da2484 100755 --- a/apps/Backend/src/routes/payments.ts +++ b/apps/Backend/src/routes/payments.ts @@ -16,6 +16,51 @@ import { prisma } from "@repo/db/client"; import { PaymentStatusSchema } from "@repo/db/types"; import * as paymentService from "../services/paymentService"; import { callPythonSync } from "../queue/processors/_shared"; +import fs from "fs"; +import path from "path"; +import axios from "axios"; +import FormData from "form-data"; + +const VOUCHER_DIR = path.join(__dirname, "..", "..", "uploads", "MHVoucher"); +const IMPORTED_LOG = path.join(VOUCHER_DIR, "imported_vouchers.json"); +const OCR_BASE_URL = process.env.OCR_SERVICE_BASE_URL || "http://localhost:5003"; + +function loadImportedVouchers(): Set { + try { + if (!fs.existsSync(IMPORTED_LOG)) return new Set(); + const data = JSON.parse(fs.readFileSync(IMPORTED_LOG, "utf-8")); + return new Set(data.imported ?? []); + } catch { + return new Set(); + } +} + +function saveImportedVoucher(voucher: string) { + const existing = loadImportedVouchers(); + existing.add(voucher); + fs.writeFileSync(IMPORTED_LOG, JSON.stringify({ imported: [...existing].sort() }, null, 2)); +} + +async function extractAndImportVoucherPdf(filePath: string, userId: number): Promise<{ paymentIds: number[]; rowCount: number }> { + const buffer = fs.readFileSync(filePath); + const filename = path.basename(filePath); + + const form = new FormData(); + form.append("files", buffer, { filename, contentType: "application/pdf", knownLength: buffer.length }); + + const resp = await axios.post<{ rows: any[] }>( + `${OCR_BASE_URL}/extract/pdf/json`, + form, + { headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity, timeout: 120_000 } + ); + + const rows = resp.data?.rows ?? []; + console.log(`[mh-batch] Extracted ${rows.length} rows from ${filename}`); + if (rows.length === 0) return { paymentIds: [], rowCount: 0 }; + + const paymentIds = await paymentService.pdfImportService.importRows(rows, userId); + return { paymentIds, rowCount: rows.length }; +} const paymentFilterSchema = z.object({ from: z.string().datetime(), @@ -192,6 +237,76 @@ router.post( } ); +// POST /api/payments/mh-batch-payment-check +router.post( + "/mh-batch-payment-check", + async (req: Request, res: Response): Promise => { + try { + const userId = req.user?.id; + if (!userId) return res.status(401).json({ message: "Unauthorized" }); + + const { fromDate, toDate } = req.body; + + const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(userId, "MH"); + if (!credentials) { + return res.status(404).json({ + message: "No MassHealth credentials found. Please add them in Settings.", + }); + } + + const seleniumResult = await callPythonSync("/mh-batch-payment-check", { + data: { + massdhpUsername: credentials.username, + massdhpPassword: credentials.password, + fromDate, + toDate, + }, + }); + + // --- PDF import phase --- + const alreadyImported = loadImportedVouchers(); + const allPdfs = fs.existsSync(VOUCHER_DIR) + ? fs.readdirSync(VOUCHER_DIR).filter((f) => f.endsWith(".pdf") && !f.startsWith("remittance_search_")) + : []; + const newPdfs = allPdfs.filter((f) => !alreadyImported.has(f.replace(".pdf", ""))); + + console.log(`[mh-batch] ${allPdfs.length} voucher PDFs total, ${newPdfs.length} new to import`); + + const importResults: { voucher: string; paymentIds: number[]; rowCount: number; error?: string }[] = []; + + for (const pdfFile of newPdfs) { + const voucher = pdfFile.replace(".pdf", ""); + const filePath = path.join(VOUCHER_DIR, pdfFile); + console.log(`[mh-batch] Extracting and importing: ${voucher}`); + try { + const { paymentIds, rowCount } = await extractAndImportVoucherPdf(filePath, userId); + saveImportedVoucher(voucher); + importResults.push({ voucher, paymentIds, rowCount }); + console.log(`[mh-batch] ✓ ${voucher}: ${rowCount} rows → ${paymentIds.length} payment(s)`); + } catch (err: any) { + const errMsg = err?.response?.data?.detail ?? err.message ?? "Unknown error"; + console.error(`[mh-batch] ✗ ${voucher}: ${errMsg}`); + importResults.push({ voucher, paymentIds: [], rowCount: 0, error: errMsg }); + } + } + + const succeeded = importResults.filter((r) => !r.error); + const failed = importResults.filter((r) => r.error); + + return res.json({ + ...seleniumResult, + importResults, + importSummary: newPdfs.length === 0 + ? "All PDFs already imported." + : `${succeeded.length} of ${newPdfs.length} PDFs imported successfully${failed.length ? `, ${failed.length} failed` : ""}.`, + }); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "MH batch payment check failed"; + return res.status(500).json({ message }); + } + } +); + // POST /api/payments/:claimId router.post("/:claimId", async (req: Request, res: Response): Promise => { try { diff --git a/apps/Backend/src/services/paymentService.ts b/apps/Backend/src/services/paymentService.ts index 1d6de48c..313a8c59 100755 --- a/apps/Backend/src/services/paymentService.ts +++ b/apps/Backend/src/services/paymentService.ts @@ -261,27 +261,44 @@ export const pdfImportService = { data: { firstName, lastName, - insuranceId: memberNo || null, + insuranceId: memberNo || null, + insuranceProvider: "MassHealth", dateOfBirth: new Date(Date.UTC(1900, 0, 1)), gender: "", phone: "", userId, }, }); + } else { + const updates: Record = {}; + if (patientName && !patient.firstName && !patient.lastName) { + const { firstName, lastName } = parsePdfName(patientName); + updates.firstName = firstName; + updates.lastName = lastName; + } + if (!patient.insuranceProvider) { + updates.insuranceProvider = "MassHealth"; + } + if (Object.keys(updates).length > 0) { + patient = await tx.patient.update({ where: { id: patient.id }, data: updates }); + } } + const isMassHealth = patient.insuranceProvider?.startsWith("MassHealth") ?? false; + const paidInFull = isMassHealth && totalMhPaid.gte(totalBilled); + // 2. Create payment const payment = await tx.payment.create({ data: { - patientId: patient.id, + patientId: patient.id, userId, totalBilled, - totalPaid: new Decimal(0), + totalPaid: paidInFull ? totalMhPaid : new Decimal(0), totalAdjusted: new Decimal(0), - totalDue: totalBilled, - mhPaidAmount: totalMhPaid, + totalDue: paidInFull ? new Decimal(0) : totalBilled, + mhPaidAmount: totalMhPaid, adjustment, - status: "PENDING", + status: paidInFull ? "PAID" : "PENDING", notes: `PDF import from ${first["Source File"] ?? "unknown file"}`, }, }); @@ -290,7 +307,8 @@ export const pdfImportService = { for (const row of patientRows) { const billed = new Decimal(row["Submitted Amount"] || 0); const mhPaid = new Decimal(row["Paid Amount"] || 0); - const due = Decimal.max(0, billed.minus(mhPaid)); + const linePaidInFull = isMassHealth && mhPaid.gte(billed); + const due = linePaidInFull ? new Decimal(0) : Decimal.max(0, billed.minus(mhPaid)); const { toothNumber, toothSurface } = parseTooth(row["Tooth"]); const allowed = new Decimal(row["Allowed Amount"] || 0); @@ -309,7 +327,7 @@ export const pdfImportService = { totalPaid: mhPaid, totalAdjusted: new Decimal(0), totalDue: due, - status: mhPaid.gt(0) ? "PARTIALLY_PAID" : "UNPAID", + status: linePaidInFull ? "PAID" : mhPaid.gt(0) ? "PARTIALLY_PAID" : "UNPAID", }, }); } diff --git a/apps/Frontend/src/App.jsx b/apps/Frontend/src/App.jsx index 54d5c67e..0a259b16 100755 --- a/apps/Frontend/src/App.jsx +++ b/apps/Frontend/src/App.jsx @@ -24,8 +24,10 @@ 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 AiInputAgentPage = lazy(() => import("./pages/ai-input-agent-page")); const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page")); const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page")); +const ActivationPage = lazy(() => import("./pages/activation-page")); const NotFound = lazy(() => import("./pages/not-found")); function Router() { return ( @@ -46,8 +48,10 @@ function Router() { } adminOnly/> }/> }/> + }/> }/> }/> + } adminOnly/> } adminOnly/> }/> }/> diff --git a/apps/Frontend/src/components/patients/patient-form.jsx b/apps/Frontend/src/components/patients/patient-form.jsx index 130ecd3f..5d7190b1 100644 --- a/apps/Frontend/src/components/patients/patient-form.jsx +++ b/apps/Frontend/src/components/patients/patient-form.jsx @@ -18,22 +18,38 @@ export const PatientForm = forwardRef(({ patient, extractedInfo, onSubmit }, ref : insertPatientSchema.extend({ userId: z.number().optional() }), [isEditing]); const normalizeInsuranceProvider = (val) => { 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"; + 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(() => { @@ -87,15 +103,16 @@ export const PatientForm = forwardRef(({ patient, extractedInfo, onSubmit }, ref useEffect(() => { if (patient) { const { id, userId, createdAt, ...sanitizedPatient } = patient; + const normalized = normalizeInsuranceProvider(patient.insuranceProvider); const resetValues = { ...sanitizedPatient, dateOfBirth: patient.dateOfBirth ? formatLocalDate(new Date(patient.dateOfBirth)) : null, - insuranceProvider: normalizeInsuranceProvider(patient.insuranceProvider), + insuranceProvider: normalized, }; - const normalized = normalizeInsuranceProvider(patient.insuranceProvider); form.reset(resetValues); + // Explicit setValue ensures the controlled Select re-renders with the new value form.setValue("insuranceProvider", normalized); } else { diff --git a/apps/Frontend/src/components/payments/payments-recent-table.tsx b/apps/Frontend/src/components/payments/payments-recent-table.tsx index 2867fe9e..07327d92 100755 --- a/apps/Frontend/src/components/payments/payments-recent-table.tsx +++ b/apps/Frontend/src/components/payments/payments-recent-table.tsx @@ -579,7 +579,7 @@ export default function PaymentsRecentTable({ } }} > - {isMhChecking ? "Checking..." : "Check MH Payment"} + {isMhChecking ? "Checking..." : "Check Single MH Payment"} - - - { - setMhFromDate(d); - setFromCalendarOpen(false); - }} - onClose={() => setFromCalendarOpen(false)} - /> - - +
+ { + const formatted = formatDateInput(e.target.value); + setMhFromText(formatted); + if (formatted === "") { setMhFromDate(undefined); return; } + if (/^\d{2}\/\d{2}\/\d{4}$/.test(formatted)) { + const parsed = new Date(formatted); + if (!isNaN(parsed.getTime())) setMhFromDate(parsed); + } + }} + /> + + + + + + { + setMhFromDate(d); + setMhFromText(d ? d.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }) : ""); + setFromCalendarOpen(false); + }} + onClose={() => setFromCalendarOpen(false)} + /> + + +
{/* To date picker */}
To: - - - - - - { - setMhToDate(d); - setToCalendarOpen(false); - }} - onClose={() => setToCalendarOpen(false)} - /> - - +
+ { + const formatted = formatDateInput(e.target.value); + setMhToText(formatted); + if (formatted === "") { setMhToDate(undefined); return; } + if (/^\d{2}\/\d{2}\/\d{4}$/.test(formatted)) { + const parsed = new Date(formatted); + if (!isNaN(parsed.getTime())) setMhToDate(parsed); + } + }} + /> + + + + + + { + setMhToDate(d); + setMhToText(d ? d.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }) : ""); + setToCalendarOpen(false); + }} + onClose={() => setToCalendarOpen(false)} + /> + + +
- {/* Check All MH Payment button */} + {/* MH Batch Payment Check button */} diff --git a/apps/Frontend/src/utils/dateUtils.js b/apps/Frontend/src/utils/dateUtils.js index bfb79694..8b6d4c80 100644 --- a/apps/Frontend/src/utils/dateUtils.js +++ b/apps/Frontend/src/utils/dateUtils.js @@ -110,7 +110,7 @@ function isDateOnlyString(s) { } // ---------- formatDateToHumanReadable ---------- /** - * Frontend-safe human readable formatter. + * Frontend-safe date formatter. Output format: "MM/DD/YYYY" (e.g. "03/01/1980"). * * Rules: * - If input is a date-only string "YYYY-MM-DD", format it directly (no TZ math). @@ -118,37 +118,35 @@ function isDateOnlyString(s) { * - If input is any other string (ISO/timestamp), DO NOT call new Date(isoString) directly * for display. Instead, use parseLocalDate(dateInput) to extract the local calendar day * (strip time portion) and render that. This prevents off-by-one day drift. - * - * Output example: "Oct 7, 2025" */ export function formatDateToHumanReadable(dateInput) { if (!dateInput) return "N/A"; - // date-only string -> show as-is using MONTH_SHORT + // date-only string "YYYY-MM-DD" -> m and d are already zero-padded if (typeof dateInput === "string" && isDateOnlyString(dateInput)) { const [y, m, d] = dateInput.split("-"); if (!y || !m || !d) return "Invalid Date"; - return `${MONTH_SHORT[parseInt(m, 10) - 1]} ${d}, ${y}`; + return `${m}/${d}/${y}`; } // Date object -> use local calendar fields if (dateInput instanceof Date) { if (isNaN(dateInput.getTime())) return "Invalid Date"; - const dd = String(dateInput.getDate()); - const mm = MONTH_SHORT[dateInput.getMonth()]; + const dd = String(dateInput.getDate()).padStart(2, "0"); + const mm = String(dateInput.getMonth() + 1).padStart(2, "0"); const yy = dateInput.getFullYear(); - return `${mm} ${dd}, ${yy}`; + return `${mm}/${dd}/${yy}`; } // Other string (likely ISO/timestamp) -> normalize via parseLocalDate // This preserves the calendar day the user expects (no timezone drift). if (typeof dateInput === "string") { try { const localDate = parseLocalDate(dateInput); - const dd = String(localDate.getDate()); - const mm = MONTH_SHORT[localDate.getMonth()]; + const dd = String(localDate.getDate()).padStart(2, "0"); + const mm = String(localDate.getMonth() + 1).padStart(2, "0"); const yy = localDate.getFullYear(); - return `${mm} ${dd}, ${yy}`; + return `${mm}/${dd}/${yy}`; } catch (err) { console.error("Invalid date input provided:", dateInput, err); diff --git a/apps/Frontend/src/utils/procedureCombos.js b/apps/Frontend/src/utils/procedureCombos.js index 6b9767ef..9a270eae 100644 --- a/apps/Frontend/src/utils/procedureCombos.js +++ b/apps/Frontend/src/utils/procedureCombos.js @@ -220,6 +220,27 @@ export const PROCEDURE_COMBOS = { label: "PL (Cast)", codes: ["D5214"], }, + // Implants + implantFull: { + id: "implantFull", + label: "Implant/Abut/Crown", + codes: ["D6010", "D6057", "D6058"], + }, + implantFixture: { + id: "implantFixture", + label: "Implant Fixture", + codes: ["D6010"], + }, + implantAbutment: { + id: "implantAbutment", + label: "Abutment", + codes: ["D6057"], + }, + implantCrown: { + id: "implantCrown", + label: "Implant Crown", + codes: ["D6058"], + }, // Endodontics rctAnterior: { id: "rctAnterior", @@ -353,6 +374,7 @@ export const COMBO_CATEGORIES = { "plResin", "plCast", ], + Implants: ["implantFull", "implantFixture", "implantAbutment", "implantCrown"], Endodontics: ["rctAnterior", "rctAnteriorPostCrown", "rctPremolar", "rctPremolarPostCrown", "rctMolar", "rctMolarPostCrown", "postCore", "coreBU"], Prosthodontics: ["crown"], Periodontics: ["deepCleaning"], diff --git a/apps/Frontend/src/utils/procedureCombosMapping.js b/apps/Frontend/src/utils/procedureCombosMapping.js index f81814d1..860467fb 100644 --- a/apps/Frontend/src/utils/procedureCombosMapping.js +++ b/apps/Frontend/src/utils/procedureCombosMapping.js @@ -1,7 +1,15 @@ import Decimal from "decimal.js"; import rawCodeTable from "@/assets/data/procedureCodesMH.json"; +import rawCCACodeTable from "@/assets/data/procedureCodesCCA.json"; +import rawDDMACodeTable from "@/assets/data/procedureCodesDDMA.json"; +import rawUnitedDHCodeTable from "@/assets/data/procedureCodesUnitedDH.json"; +import rawTuftsSCOCodeTable from "@/assets/data/procedureCodesTuftsSCO.json"; import { PROCEDURE_COMBOS } from "./procedureCombos"; const CODE_TABLE = rawCodeTable; +const CCA_CODE_TABLE = rawCCACodeTable; +const DDMA_CODE_TABLE = rawDDMACodeTable; +const UNITEDDH_CODE_TABLE = rawUnitedDHCodeTable; +const TUFTSSCO_CODE_TABLE = rawTuftsSCOCodeTable; /* ----------------------------- Helpers ----------------------------- */ export const COMBO_BUTTONS = Object.values(PROCEDURE_COMBOS).map((c) => ({ id: c.id, @@ -18,6 +26,55 @@ const CODE_MAP = (() => { } return m; })(); +const CCA_CODE_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; +})(); +const DDMA_CODE_MAP = (() => { + const m = new Map(); + for (const r of DDMA_CODE_TABLE) { + const k = normalizeCode(String(r["Procedure Code"] || "")); + if (k && !m.has(k)) + m.set(k, r); + } + return m; +})(); +const UNITEDDH_CODE_MAP = (() => { + const m = new Map(); + for (const r of UNITEDDH_CODE_TABLE) { + const k = normalizeCode(String(r["Procedure Code"] || "")); + if (k && !m.has(k)) + m.set(k, r); + } + return m; +})(); +const TUFTSSCO_CODE_MAP = (() => { + const m = new Map(); + for (const r of TUFTSSCO_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) { + const k = (insuranceSiteKey ?? "").replace(/_/g, "").toLowerCase(); + if (k === "cca") + return CCA_CODE_MAP; + if (k === "ddma") + return DDMA_CODE_MAP; + if (k === "unitedsco" || k === "uniteddh" || k === "dentalhub") + return UNITEDDH_CODE_MAP; + if (k === "tuftssco" || k === "tufts") + return TUFTSSCO_CODE_MAP; + return CODE_MAP; // default: MassHealth +} // this function is solely for abbrevations feature in claim-form export function getDescriptionForCode(code) { if (!code) @@ -83,44 +140,35 @@ const ageOnDate = (dob, on) => { 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) + // D1110: only valid for age >=14 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)) + if (age < 14) + return new Decimal(0); // D1110 not for children <14 + // age >= 14: use age-split if present, then flat Price + if (age <= 21 && !isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + if (age > 21 && !isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21); + if (!isBlankPrice(row.Price)) + return toDecimalOrZero(row.Price); return new Decimal(0); } - // D1120: child 0-13 => PriceLTEQ21, otherwise no price (NC) + // D1120: valid for child 0-13 only if (normalizedCode === "D1120") { - if (age < 14) { - if (!isBlankPrice(row.PriceLTEQ21)) - return toDecimalOrZero(row.PriceLTEQ21); - return new Decimal(0); - } - // age >= 14 => NC / no price + if (age >= 14) + return new Decimal(0); // NC for adults + if (!isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + if (!isBlankPrice(row.Price)) + return toDecimalOrZero(row.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 + // Generic/default: age-split first, flat Price as fallback + if (age <= 21 && !isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + if (age > 21 && !isBlankPrice(row.PriceGT21)) + return toDecimalOrZero(row.PriceGT21); if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price); return new Decimal(0); @@ -158,7 +206,8 @@ const ensureCapacity = (lines, min, lineDate) => { * Returns a NEW form object (immutable). */ export function mapPricesForForm(params) { - const { form, patientDOB } = params; + const { form, patientDOB, insuranceSiteKey } = params; + const map = getCodeMap(insuranceSiteKey); return { ...form, serviceLines: form.serviceLines.map((ln) => { @@ -166,7 +215,7 @@ export function mapPricesForForm(params) { 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 }; }), }; @@ -175,7 +224,7 @@ export function mapPricesForForm(params) { * Apply a preset combo (fills codes & prices) using patientDOB and serviceDate. * Returns a NEW form object (immutable). */ -export function applyComboToForm(form, comboId, patientDOB, options = {}) { +export function applyComboToForm(form, comboId, patientDOB, options = {}, insuranceSiteKey) { const preset = PROCEDURE_COMBOS[String(comboId)]; if (!preset) return form; @@ -205,8 +254,8 @@ export function applyComboToForm(form, comboId, patientDOB, options = {}) { } // 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); + const age = options.skipPrice ? 0 : ageOnDate(patientDOB, lineDate); + const map = options.skipPrice ? CODE_MAP : getCodeMap(insuranceSiteKey); for (let j = 0; j < preset.codes.length; j++) { const i = insertAt + j; if (i >= next.serviceLines.length) @@ -215,7 +264,9 @@ export function applyComboToForm(form, comboId, patientDOB, options = {}) { if (!codeRaw) continue; const code = normalizeCode(codeRaw); - const price = getPriceForCodeWithAgeFromMap(CODE_MAP, code, age); + const price = options.skipPrice + ? new Decimal(0) + : getPriceForCodeWithAgeFromMap(map, code, age); const original = next.serviceLines[i]; next.serviceLines[i] = { ...original, @@ -238,4 +289,32 @@ export function applyComboToForm(form, comboId, patientDOB, options = {}) { } return next; } -export { CODE_MAP, getPriceForCodeWithAgeFromMap }; +export { CODE_MAP, CCA_CODE_MAP, DDMA_CODE_MAP, UNITEDDH_CODE_MAP, TUFTSSCO_CODE_MAP, getCodeMap, getPriceForCodeWithAgeFromMap }; +/** Compare each service line's totalBilled against the fee schedule. + * Returns lines where the entered price differs from the schedule price. + * Returns empty array if the siteKey has no schedule (United, Tufts, etc.). */ +export function findPriceMismatches(serviceLines, insuranceSiteKey, patientDOB, serviceDate) { + const supported = ["MH", "MASSHEALTH", "CCA", "DDMA", "UNITEDDH", "UNITEDSCO", "TUFTSSCO"]; + if (!insuranceSiteKey || !supported.includes(insuranceSiteKey.toUpperCase())) + return []; + const map = getCodeMap(insuranceSiteKey); + const mismatches = []; + for (const line of serviceLines) { + const code = normalizeCode(line.procedureCode || ""); + if (!code) + continue; + const enteredPrice = new Decimal(Number(line.totalBilled) || 0); + if (enteredPrice.isZero()) + continue; + const age = ageOnDate(patientDOB, serviceDate); + const schedulePrice = getPriceForCodeWithAgeFromMap(map, code, age); + if (!schedulePrice.isZero() && !enteredPrice.equals(schedulePrice)) { + mismatches.push({ + procedureCode: code, + enteredPrice: enteredPrice.toNumber(), + schedulePrice: schedulePrice.toNumber(), + }); + } + } + return mismatches; +} diff --git a/apps/Frontend/tailwind.config.js b/apps/Frontend/tailwind.config.js index 983ad579..d2ba8848 100644 --- a/apps/Frontend/tailwind.config.js +++ b/apps/Frontend/tailwind.config.js @@ -78,10 +78,22 @@ export default { height: "0", }, }, + blob: { + "0%, 100%": { transform: "translate(0, 0) scale(1)", borderRadius: "40% 60% 70% 30% / 40% 50% 60% 50%" }, + "25%": { transform: "translate(80px, -60px) scale(1.25)", borderRadius: "60% 40% 30% 70% / 60% 30% 70% 40%" }, + "50%": { transform: "translate(-50px, 50px) scale(0.85)", borderRadius: "30% 60% 40% 70% / 50% 60% 30% 60%" }, + "75%": { transform: "translate(40px, 30px) scale(1.1)", borderRadius: "50% 40% 60% 30% / 40% 70% 50% 60%" }, + }, + "blob-spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + blob: "blob 10s ease-in-out infinite", + "blob-spin": "blob-spin 20s linear infinite", }, }, }, diff --git a/apps/Frontend/tsconfig.tsbuildinfo b/apps/Frontend/tsconfig.tsbuildinfo index a5266133..87965c32 100755 --- a/apps/Frontend/tsconfig.tsbuildinfo +++ b/apps/Frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./tailwind.config.ts","./vite.config.ts","./src/theme-init.ts","./src/vite-env.d.ts","./src/hooks/use-extractPdfData.ts","./src/hooks/use-job-status.ts","./src/hooks/use-toast.ts","./src/lib/queryClient.ts","./src/lib/socket.ts","./src/lib/utils.ts","./src/lib/api/documents.ts","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/seleniumTaskSlice.ts","./src/utils/dateUtils.ts","./src/utils/pageNumberGenerator.ts","./src/utils/procedureCombos.ts","./src/utils/procedureCombosMapping.ts","./src/App.tsx","./src/main.tsx","./src/components/analytics/appointments-by-day.tsx","./src/components/analytics/new-patients.tsx","./src/components/appointment-procedures/appointment-procedures-dialog.tsx","./src/components/appointments/add-appointment-modal.tsx","./src/components/appointments/appointment-form.tsx","./src/components/appointments/patient-status-badge.tsx","./src/components/chart/lab-management-tab.tsx","./src/components/chart/prescription-tab.tsx","./src/components/chart/teeth-chart.tsx","./src/components/chart/treatment-plan-tab.tsx","./src/components/claims/claim-document-upload-modal.tsx","./src/components/claims/claim-edit-modal.tsx","./src/components/claims/claim-form.tsx","./src/components/claims/claim-view-modal.tsx","./src/components/claims/claims-of-patient-table.tsx","./src/components/claims/claims-recent-table.tsx","./src/components/claims/claims-ui.tsx","./src/components/claims/tooth-ui.tsx","./src/components/cloud-storage/bread-crumb.tsx","./src/components/cloud-storage/file-preview-modal.tsx","./src/components/cloud-storage/files-section.tsx","./src/components/cloud-storage/folder-panel.tsx","./src/components/cloud-storage/folder-section.tsx","./src/components/cloud-storage/new-folder-modal.tsx","./src/components/cloud-storage/recent-top-level-folder-modal.tsx","./src/components/cloud-storage/search-bar.tsx","./src/components/database-management/backup-destination-manager.tsx","./src/components/database-management/folder-browser-modal.tsx","./src/components/database-management/import-database-section.tsx","./src/components/documents/file-preview-modal.tsx","./src/components/file-upload/file-upload-zone.tsx","./src/components/file-upload/multiple-file-upload-zone.tsx","./src/components/insurance/credentials-modal.tsx","./src/components/insurance-status/cca-button-modal.tsx","./src/components/insurance-status/ddma-buton-modal.tsx","./src/components/insurance-status/deltains-button-modal.tsx","./src/components/insurance-status/dual-pdf-preview-modal.tsx","./src/components/insurance-status/pdf-preview-modal.tsx","./src/components/insurance-status/tufts-sco-button-modal.tsx","./src/components/insurance-status/united-sco-button-modal.tsx","./src/components/layout/app-layout.tsx","./src/components/layout/chatbot.tsx","./src/components/layout/notification-bell.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/top-app-bar.tsx","./src/components/patient-connection/message-thread.tsx","./src/components/patient-connection/sms-template-diaog.tsx","./src/components/patients/add-patient-modal.tsx","./src/components/patients/patient-financial-modal.tsx","./src/components/patients/patient-form.tsx","./src/components/patients/patient-search.tsx","./src/components/patients/patient-table.tsx","./src/components/payments/payment-edit-modal.tsx","./src/components/payments/payment-ocr-block.tsx","./src/components/payments/payment-upload-documents-block.tsx","./src/components/payments/payments-of-patient-table.tsx","./src/components/payments/payments-recent-table.tsx","./src/components/procedure/procedure-combo-buttons.tsx","./src/components/reports/collections-by-doctor-report.tsx","./src/components/reports/commission-section.tsx","./src/components/reports/export-button.tsx","./src/components/reports/pagination-controls.tsx","./src/components/reports/patients-balances-list.tsx","./src/components/reports/patients-with-balance-report.tsx","./src/components/reports/report-config.tsx","./src/components/reports/summary-cards.tsx","./src/components/settings/InsuranceCredForm.tsx","./src/components/settings/ai-chat-settings-card.tsx","./src/components/settings/ai-chat-templates-card.tsx","./src/components/settings/ai-settings-card.tsx","./src/components/settings/insurance-contact-card.tsx","./src/components/settings/insuranceCredTable.tsx","./src/components/settings/npiProviderForm.tsx","./src/components/settings/npiProviderTable.tsx","./src/components/settings/office-contact-card.tsx","./src/components/settings/office-hours-card.tsx","./src/components/settings/procedure-timeslot-card.tsx","./src/components/settings/program-bridge-table.tsx","./src/components/settings/twilio-settings-card.tsx","./src/components/staffs/staff-form.tsx","./src/components/staffs/staff-table.tsx","./src/components/ui/LoadingScreen.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/confirmationDialog.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/data-table.tsx","./src/components/ui/dateInput.tsx","./src/components/ui/dateInputField.tsx","./src/components/ui/deleteDialog.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/selenium-task-banner.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/stat-card.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-auth.tsx","./src/hooks/use-mobile.tsx","./src/lib/protected-route.tsx","./src/pages/appointments-page.tsx","./src/pages/auth-page.tsx","./src/pages/chart-page.tsx","./src/pages/claims-page.tsx","./src/pages/cloud-storage-page.tsx","./src/pages/dashboard.tsx","./src/pages/database-management-page.tsx","./src/pages/dental-shopping-login-info-page.tsx","./src/pages/dental-shopping-search-tag-page.tsx","./src/pages/documents-page.tsx","./src/pages/insurance-status-page.tsx","./src/pages/job-monitor-page.tsx","./src/pages/not-found.tsx","./src/pages/patient-connection-page.tsx","./src/pages/patients-page.tsx","./src/pages/payments-page.tsx","./src/pages/reports-page.tsx","./src/pages/settings-page.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file +{"root":["./tailwind.config.ts","./vite.config.ts","./src/theme-init.ts","./src/vite-env.d.ts","./src/hooks/use-extractPdfData.ts","./src/hooks/use-job-status.ts","./src/hooks/use-license.ts","./src/hooks/use-toast.ts","./src/lib/chatbotFileStore.ts","./src/lib/queryClient.ts","./src/lib/socket.ts","./src/lib/utils.ts","./src/lib/api/documents.ts","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/seleniumTaskSlice.ts","./src/utils/appointmentTypeUtils.ts","./src/utils/dateUtils.ts","./src/utils/pageNumberGenerator.ts","./src/utils/procedureCombos.ts","./src/utils/procedureCombosMapping.ts","./src/App.tsx","./src/main.tsx","./src/components/analytics/appointments-by-day.tsx","./src/components/analytics/new-patients.tsx","./src/components/appointment-procedures/appointment-procedures-dialog.tsx","./src/components/appointments/add-appointment-modal.tsx","./src/components/appointments/appointment-form.tsx","./src/components/appointments/patient-status-badge.tsx","./src/components/chart/lab-management-tab.tsx","./src/components/chart/prescription-tab.tsx","./src/components/chart/teeth-chart.tsx","./src/components/chart/treatment-plan-tab.tsx","./src/components/claims/claim-document-upload-modal.tsx","./src/components/claims/claim-edit-modal.tsx","./src/components/claims/claim-form.tsx","./src/components/claims/claim-view-modal.tsx","./src/components/claims/claims-of-patient-table.tsx","./src/components/claims/claims-recent-table.tsx","./src/components/claims/claims-ui.tsx","./src/components/claims/tooth-ui.tsx","./src/components/cloud-storage/bread-crumb.tsx","./src/components/cloud-storage/file-preview-modal.tsx","./src/components/cloud-storage/files-section.tsx","./src/components/cloud-storage/folder-panel.tsx","./src/components/cloud-storage/folder-section.tsx","./src/components/cloud-storage/new-folder-modal.tsx","./src/components/cloud-storage/recent-top-level-folder-modal.tsx","./src/components/cloud-storage/search-bar.tsx","./src/components/database-management/backup-destination-manager.tsx","./src/components/database-management/folder-browser-modal.tsx","./src/components/database-management/import-database-section.tsx","./src/components/database-management/network-backup-manager.tsx","./src/components/documents/file-preview-modal.tsx","./src/components/file-upload/file-upload-zone.tsx","./src/components/file-upload/multiple-file-upload-zone.tsx","./src/components/insurance/credentials-modal.tsx","./src/components/insurance-status/bcbs-ma-button-modal.tsx","./src/components/insurance-status/cca-button-modal.tsx","./src/components/insurance-status/ddma-buton-modal.tsx","./src/components/insurance-status/deltains-button-modal.tsx","./src/components/insurance-status/dual-pdf-preview-modal.tsx","./src/components/insurance-status/pdf-preview-modal.tsx","./src/components/insurance-status/tufts-sco-button-modal.tsx","./src/components/insurance-status/united-sco-button-modal.tsx","./src/components/layout/app-layout.tsx","./src/components/layout/chatbot.tsx","./src/components/layout/notification-bell.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/top-app-bar.tsx","./src/components/patient-connection/dial-pad.tsx","./src/components/patient-connection/message-thread.tsx","./src/components/patient-connection/sms-template-diaog.tsx","./src/components/patients/add-patient-modal.tsx","./src/components/patients/patient-financial-modal.tsx","./src/components/patients/patient-form.tsx","./src/components/patients/patient-search.tsx","./src/components/patients/patient-table.tsx","./src/components/payments/payment-edit-modal.tsx","./src/components/payments/payment-ocr-block.tsx","./src/components/payments/payment-upload-documents-block.tsx","./src/components/payments/payments-of-patient-table.tsx","./src/components/payments/payments-recent-table.tsx","./src/components/procedure/procedure-combo-buttons.tsx","./src/components/reports/collections-by-doctor-report.tsx","./src/components/reports/commission-section.tsx","./src/components/reports/export-button.tsx","./src/components/reports/pagination-controls.tsx","./src/components/reports/patients-balances-list.tsx","./src/components/reports/patients-with-balance-report.tsx","./src/components/reports/report-config.tsx","./src/components/reports/summary-cards.tsx","./src/components/settings/InsuranceCredForm.tsx","./src/components/settings/ai-chat-settings-card.tsx","./src/components/settings/ai-chat-templates-card.tsx","./src/components/settings/ai-settings-card.tsx","./src/components/settings/insurance-contact-card.tsx","./src/components/settings/insuranceCredTable.tsx","./src/components/settings/npiProviderForm.tsx","./src/components/settings/npiProviderTable.tsx","./src/components/settings/office-contact-card.tsx","./src/components/settings/office-hours-card.tsx","./src/components/settings/procedure-timeslot-card.tsx","./src/components/settings/program-bridge-table.tsx","./src/components/settings/twilio-settings-card.tsx","./src/components/staffs/staff-form.tsx","./src/components/staffs/staff-table.tsx","./src/components/ui/LoadingScreen.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/confirmationDialog.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/data-table.tsx","./src/components/ui/dateInput.tsx","./src/components/ui/dateInputField.tsx","./src/components/ui/deleteDialog.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/selenium-task-banner.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/stat-card.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/hooks/use-auth.tsx","./src/hooks/use-mobile.tsx","./src/lib/protected-route.tsx","./src/pages/activation-page.tsx","./src/pages/ai-input-agent-page.tsx","./src/pages/appointments-page.tsx","./src/pages/auth-page.tsx","./src/pages/chart-page.tsx","./src/pages/claims-page.tsx","./src/pages/cloud-storage-page.tsx","./src/pages/dashboard.tsx","./src/pages/database-management-page.tsx","./src/pages/dental-shopping-login-info-page.tsx","./src/pages/dental-shopping-search-tag-page.tsx","./src/pages/documents-page.tsx","./src/pages/insurance-status-page.tsx","./src/pages/job-monitor-page.tsx","./src/pages/not-found.tsx","./src/pages/patient-connection-page.tsx","./src/pages/patients-page.tsx","./src/pages/payments-page.tsx","./src/pages/reports-page.tsx","./src/pages/settings-page.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file diff --git a/apps/Frontend/vite.config.js b/apps/Frontend/vite.config.js index 0eff6699..ea296511 100644 --- a/apps/Frontend/vite.config.js +++ b/apps/Frontend/vite.config.js @@ -11,7 +11,10 @@ export default defineConfig(({ mode }) => { fs: { allow: [".."], }, - allowedHosts: ["communitydentistsoflowell.mydentalofficemanagement.com"], + allowedHosts: [ + ...(env.VITE_CLOUDFLARE_HOST ? [env.VITE_CLOUDFLARE_HOST] : []), + "192.168.0.94", + ], proxy: { "/api": { target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000", diff --git a/apps/SeleniumService/agent.py b/apps/SeleniumService/agent.py index 9bb0a03c..3915aaab 100755 --- a/apps/SeleniumService/agent.py +++ b/apps/SeleniumService/agent.py @@ -10,7 +10,8 @@ from selenium_MH_eligibilityHistoryCheckWorker import AutomationMassHealthEligib from selenium_CMSP_eligibilityHistoryRemainingCheckWorker import AutomationCMSPEligibilityHistoryRemainingCheck from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck from selenium_preAuthWorker import AutomationMassHealthPreAuth -from selenium_MHPaymentCheckWorker import AutomationMassHealthPaymentCheck +from selenium_MHSinglePaymentCheckWorker import AutomationMassHealthSinglePaymentCheck +from selenium_MHBatchPaymentCheckWorker import AutomationMassHealthBatchPaymentCheck import os import time import helpers_ddma_eligibility as hddma @@ -290,7 +291,35 @@ async def mh_payment_check(request: Request): waiting_jobs -= 1 active_jobs += 1 try: - bot = AutomationMassHealthPaymentCheck(data) + bot = AutomationMassHealthSinglePaymentCheck(data) + result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login") + + if result.get("status") != "success": + return {"status": "error", "message": result.get("message")} + + return result + except Exception as e: + return {"status": "error", "message": str(e)} + finally: + async with lock: + active_jobs -= 1 + + +# Endpoint: 6 — Check MassHealth payments in batch by date range +@app.post("/mh-batch-payment-check") +async def mh_batch_payment_check(request: Request): + global active_jobs, waiting_jobs + data = await request.json() + + async with lock: + waiting_jobs += 1 + + async with semaphore: + async with lock: + waiting_jobs -= 1 + active_jobs += 1 + try: + bot = AutomationMassHealthBatchPaymentCheck(data) result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login") if result.get("status") != "success": diff --git a/apps/SeleniumService/selenium_MHPaymentCheckWorker.py b/apps/SeleniumService/selenium_MHPaymentCheckWorker.py deleted file mode 100644 index 11f3117d..00000000 --- a/apps/SeleniumService/selenium_MHPaymentCheckWorker.py +++ /dev/null @@ -1,218 +0,0 @@ -from selenium import webdriver -from selenium.webdriver.chrome.service import Service -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from webdriver_manager.chrome import ChromeDriverManager -import time -import os -import base64 - - -class AutomationMassHealthPaymentCheck: - def __init__(self, data): - self.headless = False - self.driver = None - self.extracted_data = {} - - self.data = data.get("data") - - self.massdhp_username = self.data.get("massdhpUsername", "") - self.massdhp_password = self.data.get("massdhpPassword", "") - self.claim_number = self.data.get("claimNumber", "") - - self.download_dir = os.path.abspath("downloads") - os.makedirs(self.download_dir, exist_ok=True) - - def config_driver(self): - options = webdriver.ChromeOptions() - if self.headless: - options.add_argument("--headless") - - prefs = { - "download.default_directory": self.download_dir, - "plugins.always_open_pdf_externally": False, - "download.prompt_for_download": False, - "download.directory_upgrade": True, - } - options.add_experimental_option("prefs", prefs) - - s = Service(ChromeDriverManager().install()) - self.driver = webdriver.Chrome(service=s, options=options) - - def login(self): - wait = WebDriverWait(self.driver, 30) - - try: - # Step 1: Click the SIGN IN button on the initial page - signin_button = wait.until( - EC.element_to_be_clickable( - ( - By.CSS_SELECTOR, - "a.btn.btn-block.btn-primary[href='https://connectsso.masshealth-dental.org/mhprovider/index.html']", - ) - ) - ) - signin_button.click() - - time.sleep(3) - - # Step 2: Enter username - email_field = wait.until(EC.presence_of_element_located((By.ID, "User"))) - email_field.clear() - email_field.send_keys(self.massdhp_username) - - # Step 3: Enter password - password_field = wait.until( - EC.presence_of_element_located((By.ID, "Password")) - ) - password_field.clear() - password_field.send_keys(self.massdhp_password) - - # Step 4: Click login button - login_button = wait.until( - EC.element_to_be_clickable( - ( - By.CSS_SELECTOR, - "input[type='submit'][name='submit'][value='Login']", - ) - ) - ) - login_button.click() - - return "Success" - except Exception as e: - print(f"Error while logging in: {e}") - return "ERROR:LOGIN FAILED" - - def navigate_to_payments(self): - """ - TODO: Navigate to the payments / remittance section after login. - Inspect the portal and fill in the correct selectors below. - """ - wait = WebDriverWait(self.driver, 30) - substep = "init" - - try: - print(f"[navigate_to_payments] URL after login: {self.driver.current_url}") - - substep = "financial_menu" - financial_menu = wait.until( - EC.presence_of_element_located( - (By.XPATH, '//*[@id="navbar-desktop"]/div/div[3]/div/strong') - ) - ) - self.driver.execute_script("arguments[0].scrollIntoView(true);", financial_menu) - self.driver.execute_script("arguments[0].click();", financial_menu) - time.sleep(2) - - substep = "payments_link" - payments_link = wait.until( - EC.presence_of_element_located( - (By.XPATH, '//*[@id="navbar-desktop"]/div/div[3]/div/div/div[2]/div/a') - ) - ) - self.driver.execute_script("arguments[0].click();", payments_link) - time.sleep(2) - - return "Success" - except Exception as e: - print(f"[navigate_to_payments] FAILED at substep='{substep}': {e}") - print(f"[navigate_to_payments] URL at failure: {self.driver.current_url}") - return f"ERROR:NAVIGATE_TO_PAYMENTS:{substep}" - - def step1_search_claim(self): - """Enter claim number and click SEARCH on the Search Claims/Prior Authorizations page.""" - wait = WebDriverWait(self.driver, 30) - substep = "init" - - try: - print(f"[step1] URL: {self.driver.current_url}") - - substep = "claim_number_input" - claim_input = wait.until( - EC.presence_of_element_located( - (By.XPATH, "/html/body/div[1]/div/div/div/form/fieldset/div[4]/div[2]/input") - ) - ) - claim_input.clear() - claim_input.send_keys(self.claim_number) - print(f"[step1] entered claim number: {self.claim_number}") - - substep = "search_button" - search_button = wait.until( - EC.element_to_be_clickable( - (By.XPATH, "/html/body/div[1]/div/div/div/form/fieldset/div[7]/div/button[2]") - ) - ) - self.driver.execute_script("arguments[0].click();", search_button) - print("[step1] clicked SEARCH") - time.sleep(3) - - return "Success" - except Exception as e: - print(f"[step1] FAILED at substep='{substep}': {e}") - print(f"[step1] URL at failure: {self.driver.current_url}") - return f"ERROR:STEP1:{substep}" - - def step2_extract_paid_amount(self): - """Read the totalPaidAmount from the search results table.""" - wait = WebDriverWait(self.driver, 30) - substep = "init" - - try: - substep = "wait_results_table" - wait.until( - EC.presence_of_element_located( - (By.XPATH, '//td[@ng-bind="item.totalPaidAmount | currency"]') - ) - ) - - substep = "read_paid_amount" - paid_cell = self.driver.find_element( - By.XPATH, '//td[@ng-bind="item.totalPaidAmount | currency"]' - ) - raw_text = paid_cell.text.strip() - print(f"[step2] raw paid amount text: '{raw_text}'") - - # Strip currency symbol and commas, e.g. "$1,234.56" → 1234.56 - numeric_str = raw_text.replace("$", "").replace(",", "").strip() - try: - paid_amount = float(numeric_str) - except ValueError: - paid_amount = 0.0 - print(f"[step2] could not parse '{raw_text}' as float, defaulting to 0.0") - - return {"status": "success", "mhPaidAmount": paid_amount, "mhPaidAmountRaw": raw_text} - except Exception as e: - print(f"[step2] FAILED at substep='{substep}': {e}") - return f"ERROR:STEP2:{substep}" - - def main_workflow(self, url): - try: - self.config_driver() - self.driver.maximize_window() - self.driver.get(url) - time.sleep(3) - - login_result = self.login() - if login_result.startswith("ERROR"): - return {"status": "error", "message": login_result} - - nav_result = self.navigate_to_payments() - if nav_result.startswith("ERROR"): - return {"status": "error", "message": nav_result} - - step1_result = self.step1_search_claim() - if step1_result.startswith("ERROR"): - return {"status": "error", "message": step1_result} - - step2_result = self.step2_extract_paid_amount() - if isinstance(step2_result, str) and step2_result.startswith("ERROR"): - return {"status": "error", "message": step2_result} - - return step2_result - except Exception as e: - return {"status": "error", "message": str(e)} - finally: - self.driver.quit() diff --git a/package-lock.json b/package-lock.json index a15f8470..0b2078d5 100755 --- a/package-lock.json +++ b/package-lock.json @@ -38,8 +38,10 @@ "license": "ISC", "dependencies": { "@google/generative-ai": "^0.24.1", + "@langchain/anthropic": "^1.4.0", "@langchain/google-genai": "^2.1.30", "@langchain/langgraph": "^1.2.9", + "@langchain/openai": "^1.4.7", "archiver": "^7.0.1", "axios": "^1.9.0", "bcrypt": "^5.1.1", @@ -220,6 +222,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.95.2", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.2.tgz", + "integrity": "sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -251,7 +274,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -536,7 +558,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@chevrotain/cst-dts-gen": { "version": "10.5.0", @@ -605,8 +628,7 @@ "version": "0.3.15", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", @@ -636,7 +658,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -653,7 +674,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -670,7 +690,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -687,7 +706,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -704,7 +722,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -721,7 +738,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -738,7 +754,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -755,7 +770,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -772,7 +786,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -789,7 +802,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -806,7 +818,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -823,7 +834,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -840,7 +850,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -857,7 +866,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -874,7 +882,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -891,7 +898,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -908,7 +914,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -925,7 +930,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -942,7 +946,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -959,7 +962,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -976,7 +978,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -993,7 +994,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1010,7 +1010,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1027,7 +1026,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1044,7 +1042,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1061,7 +1058,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1416,18 +1412,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/anthropic": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.0.tgz", + "integrity": "sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.95.1", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.47" + } + }, "node_modules/@langchain/core": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.44.tgz", - "integrity": "sha512-RePW1IjGCHr9ua2vcby3aE8mOOz3EnwDZxMEGbNDT91kf14eqkJqxDXvaZFviGdcN9DTrxM5RPQNAHmwSm4tbg==", + "version": "1.1.48", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.48.tgz", + "integrity": "sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==", "license": "MIT", "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", - "ansi-styles": "^5.0.0", - "camelcase": "6", - "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", @@ -1438,18 +1447,6 @@ "node": ">=20" } }, - "node_modules/@langchain/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@langchain/google-genai": { "version": "2.1.30", "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-2.1.30.tgz", @@ -1616,6 +1613,23 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@langchain/openai": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.4.7.tgz", + "integrity": "sha512-i1YLV4pWbGC6W8m0ZNpLObJuf1nyU4o8aWyX4AF9fHn7eM67HfIJWQ5n5XzcCpuSa41otrxA9jvH5XRKwI1qDA==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^6.37.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -4338,6 +4352,12 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4966,7 +4986,6 @@ "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "hoist-non-react-statics": "^3.3.0" }, @@ -5019,7 +5038,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -5084,7 +5102,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5095,7 +5112,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5234,7 +5250,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5543,7 +5558,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6089,7 +6103,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6263,18 +6276,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -6946,15 +6947,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -7239,8 +7231,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -7515,7 +7506,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8002,6 +7992,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -8733,7 +8729,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -9127,6 +9122,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9245,6 +9253,7 @@ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.0.tgz", "integrity": "sha512-GGaj5IMRfLv2HXXFzGk9diISMYLTpSTh6fzCZGKxWYW/NqEztIFtnXLq6G/RVhzFRmCykLap1fuC67LVKoQLcg==", "license": "MIT", + "peer": true, "dependencies": { "p-queue": "6.6.2" }, @@ -10096,6 +10105,7 @@ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "license": "MIT", + "peer": true, "bin": { "mustache": "bin/mustache" } @@ -10435,6 +10445,24 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "6.42.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.42.0.tgz", + "integrity": "sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==", + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10499,6 +10527,7 @@ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", "license": "MIT", + "peer": true, "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" @@ -10530,6 +10559,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "license": "MIT", + "peer": true, "dependencies": { "p-finally": "^1.0.0" }, @@ -10704,7 +10734,6 @@ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18" }, @@ -10737,7 +10766,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -10902,7 +10930,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11132,7 +11159,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11825,7 +11851,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11917,7 +11942,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11930,7 +11954,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -11962,7 +11985,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12243,8 +12265,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12933,6 +12954,16 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -13166,7 +13197,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13460,7 +13490,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13505,6 +13534,12 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -13732,7 +13767,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -13914,7 +13948,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14205,7 +14238,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14754,7 +14786,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14993,7 +15024,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -15116,7 +15146,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }