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
This commit is contained in:
2026-07-17 00:30:03 -04:00
parent 5979d27c57
commit f29064883c
2 changed files with 76 additions and 0 deletions

View File

@@ -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,

View File

@@ -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: [],