From f29064883ca145fadd1d6ec3d46b4394f60a8c72 Mon Sep 17 00:00:00 2001 From: Gitead Date: Fri, 17 Jul 2026 00:30:03 -0400 Subject: [PATCH] feat: fall back to saved procedures for no-notes appointments in AI claim queue, and include appointment attachments in MH selenium submission - AI claim queue now uses previously saved AppointmentProcedure data (with a derived insurance site key) when an appointment has no notes but has saved procedures, instead of skipping with "No notes on this appointment" - selenium-claim route now reads saved AppointmentFile attachments from disk when they weren't re-uploaded in the browser, so images/PDFs already attached via "Select Procedures" reach the MassHealth submission --- apps/Backend/src/routes/claims.ts | 18 ++++++ apps/Frontend/src/pages/appointments-page.tsx | 58 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 5125cf71..a63fa5c8 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -346,6 +346,24 @@ router.post( mimetype: f.mimetype, })); + // Include attachments already saved on the appointment (e.g. via "Select Procedures") + // that weren't re-uploaded in this request — read straight from disk by filePath. + if (claimData.appointmentId) { + const seenNames = new Set(filesForQueue.map((f) => f.originalname)); + const apptFiles = await storage.getAppointmentFiles(Number(claimData.appointmentId)); + for (const f of apptFiles) { + if (!f.filePath || seenNames.has(f.filename)) continue; + const absPath = path.join(process.cwd(), f.filePath); + if (!fs.existsSync(absPath)) continue; + filesForQueue.push({ + originalname: f.filename, + bufferBase64: fs.readFileSync(absPath).toString("base64"), + mimetype: f.mimeType ?? "application/octet-stream", + }); + seenNames.add(f.filename); + } + } + const job = await seleniumQueue.add("claim-submit", { jobType: "claim-submit", userId: req.user.id, diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index d779cc09..cd8e2398 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -76,6 +76,7 @@ import { PatientStatusBadge } from "@/components/appointments/patient-status-bad import type { OfficeHoursData } from "@/components/settings/office-hours-card"; import { MessageThread } from "@/components/patient-connection/message-thread"; import { getAppointmentTypeLabel } from "@/utils/appointmentTypeUtils"; +import { getDescriptionForCode } from "@/utils/procedureCombosMapping"; // Define types for scheduling interface TimeSlot { @@ -110,6 +111,27 @@ interface ScheduledAppointment { procedureCodes?: string[]; } +function 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") || p.includes("delta dental ma")) 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") && p.includes("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 ""; +} + function appointmentCardColor(apt: ScheduledAppointment): string { if (apt.hasClaimWithNumber) return "bg-gray-500 text-white"; if (apt.hasProcedures) return "bg-blue-500 text-white"; @@ -1515,6 +1537,42 @@ export default function AppointmentsPage() { const apt = queue[index]!; const aptDate = typeof apt.date === "string" ? apt.date : formatLocalDate(apt.date as Date); if (!apt.notes?.trim()) { + if (apt.hasProcedures) { + setIsAiClaimProcessing(true); + setAiClaimCurrentData(null); + try { + const res = await apiRequest("GET", `/api/appointment-procedures/prefill-from-appointment/${apt.id}`); + const data = await res.json(); + const matchedCodes = (data.procedures ?? []).map((p: any) => ({ + code: p.procedureCode, + description: getDescriptionForCode(p.procedureCode) ?? "", + toothNumber: p.toothNumber ?? undefined, + toothSurface: p.toothSurface ?? undefined, + })); + setAiClaimCurrentData({ + matchedCodes, + siteKey: deriveInsuranceSiteKey(apt.patientInsuranceProvider), + serviceDate: aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: "", + reply: "No notes on this appointment — using previously saved procedures instead.", + }); + } catch { + setAiClaimCurrentData({ + matchedCodes: [], + siteKey: "", + serviceDate: aptDate, + appointmentId: Number(apt.id), + patientName: apt.patientName, + notes: "", + reply: "No notes on this appointment, and failed to load saved procedures.", + }); + } finally { + setIsAiClaimProcessing(false); + } + return; + } setIsAiClaimProcessing(false); setAiClaimCurrentData({ matchedCodes: [],