feat: BCBS preauth via ADA form; preauth shows dark gray; claim/preauth download

- Preauth column: BCBS appointments fill the 2024 ADA form with box 1
  "Request for Predetermination/Preauthorization" (claim saved as PREAUTH),
  saved to Documents > PreAuth as ADA_PreAuth_*.pdf, then back to schedule
- Claim form PreAuth tab: "BCBS PreAuth (ADA Form)" button
- Preauth forms list procedure dates 6 days after the preauth date
- PREAUTH claims create no payment; saved BCBS preauths count as preauthorized
- Schedule: appointments with a completed preauth show dark gray
- "Download Filled Claim/Preauth Form" also zips the BCBS preauth forms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 23:51:27 -04:00
co-authored by Claude Opus 5.5
parent 44a1a41efa
commit 2ca42bb15f
5 changed files with 79 additions and 28 deletions
+17 -11
View File
@@ -1038,7 +1038,7 @@ router.post(
); );
// GET /api/claims/filled-forms?date=YYYY-MM-DD // GET /api/claims/filled-forms?date=YYYY-MM-DD
// ZIP of the claim forms the app fills itself (ADA paper forms, e.g. BCBS) for // ZIP of the claim/preauth forms the app fills itself (ADA paper forms, e.g. BCBS) for
// appointments on that schedule date. Claims submitted through Selenium are not included. // appointments on that schedule date. Claims submitted through Selenium are not included.
router.get( router.get(
"/filled-forms", "/filled-forms",
@@ -1073,11 +1073,15 @@ router.get(
} }
for (const [patientId, claimIds] of claimIdsByPatient) { for (const [patientId, claimIds] of claimIdsByPatient) {
// Filled forms are saved as ADA_Claim_<Last>_<First>_<claimId>.pdf in the patient's "Claims" folder; // Filled forms are saved as ADA_Claim_<Last>_<First>_<claimId>.pdf in the patient's "Claims"
// keep the newest file per claim // folder, and preauths as ADA_PreAuth_….pdf in "PreAuth"; keep the newest file per claim
const newestByClaim = new Map<number, { id: number; name: string }>(); const newestByClaim = new Map<number, { id: number; name: string }>();
for (const cf of await storage.listPatientCategoryCloudFiles(patientId, "Claims")) { const files = [
const m = /^ADA_Claim_.*_(\d+)\.pdf$/.exec(cf.name ?? ""); ...(await storage.listPatientCategoryCloudFiles(patientId, "Claims")),
...(await storage.listPatientCategoryCloudFiles(patientId, "PreAuth")),
];
for (const cf of files) {
const m = /^ADA_(?:Claim|PreAuth)_.*_(\d+)\.pdf$/.exec(cf.name ?? "");
if (!m || cf.id == null) continue; if (!m || cf.id == null) continue;
const claimId = Number(m[1]); const claimId = Number(m[1]);
if (!claimIds.has(claimId)) continue; if (!claimIds.has(claimId)) continue;
@@ -1095,10 +1099,10 @@ router.get(
} }
if (pdfEntries.length === 0) { if (pdfEntries.length === 0) {
return res.status(404).json({ error: `No filled claim forms for ${date}` }); return res.status(404).json({ error: `No filled claim/preauth forms for ${date}` });
} }
const zipFilename = `filled_claim_forms_${date}.zip`; const zipFilename = `filled_claim_preauth_forms_${date}.zip`;
res.setHeader("Content-Type", "application/zip"); res.setHeader("Content-Type", "application/zip");
res.setHeader("Content-Disposition", `attachment; filename="${zipFilename}"`); res.setHeader("Content-Disposition", `attachment; filename="${zipFilename}"`);
@@ -1134,7 +1138,7 @@ router.get(
} catch (err: any) { } catch (err: any) {
console.error("[filled-forms] error:", err); console.error("[filled-forms] error:", err);
if (!res.headersSent) { if (!res.headersSent) {
return res.status(500).json({ error: err?.message ?? "Filled claim form download failed" }); return res.status(500).json({ error: err?.message ?? "Filled claim/preauth form download failed" });
} }
} }
} }
@@ -1268,7 +1272,8 @@ async function buildAdaFormForRequest(req: Request): Promise<AdaFormResult> {
appointmentNotes: claim.appointment?.notes ?? null, appointmentNotes: claim.appointment?.notes ?? null,
}); });
const filename = `ADA_Claim_${claim.patient.lastName}_${claim.patient.firstName}_${claimId}.pdf` const formKind = claim.status === "PREAUTH" ? "PreAuth" : "Claim";
const filename = `ADA_${formKind}_${claim.patient.lastName}_${claim.patient.firstName}_${claimId}.pdf`
.replace(/[^\w.-]+/g, "_"); .replace(/[^\w.-]+/g, "_");
return { ok: true, pdf: Buffer.from(pdf), filename, claimId, claimStatus: claim.status, patient: claim.patient }; return { ok: true, pdf: Buffer.from(pdf), filename, claimId, claimStatus: claim.status, patient: claim.patient };
} }
@@ -1300,7 +1305,7 @@ router.post("/:id/ada-form", async (req: Request, res: Response): Promise<any> =
req.user.id, req.user.id,
patient.id, patient.id,
`${patient.firstName} ${patient.lastName}`, `${patient.firstName} ${patient.lastName}`,
"Claims", result.claimStatus === "PREAUTH" ? "PreAuth" : "Claims",
result.filename, result.filename,
result.pdf, result.pdf,
); );
@@ -1449,8 +1454,9 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
await syncAppointmentType(claim.appointmentId, rawCodes); await syncAppointmentType(claim.appointmentId, rawCodes);
// Step 3: Create payment only for real submissions (not draft saves) // Step 3: Create payment only for real submissions (not draft saves)
// (a paper preauth request isn't billed, so it gets no payment either)
const isDraft = req.query.draft === "true"; const isDraft = req.query.draft === "true";
if (!isDraft) { if (!isDraft && parsedClaim.status !== "PREAUTH") {
await storage.createPayment({ await storage.createPayment({
claimId: claim.id, claimId: claim.id,
patientId: claim.patientId, patientId: claim.patientId,
+19 -2
View File
@@ -123,6 +123,18 @@ function fmtDate(d: Date | string | null | undefined): string {
return `${mm}/${dd}/${date.getUTCFullYear()}`; return `${mm}/${dd}/${date.getUTCFullYear()}`;
} }
/** Preauth forms show procedures as planned this many days after the preauth (appointment) date. */
const PREAUTH_PROCEDURE_DAYS_AFTER = 6;
/** Shifts a date-only value (stored as UTC midnight) by whole days. */
function addDays(d: Date | string | null | undefined, days: number): Date | null {
if (!d) return null;
const date = new Date(d instanceof Date ? d.getTime() : d);
if (isNaN(date.getTime())) return null;
date.setUTCDate(date.getUTCDate() + days);
return date;
}
/** A timestamp as the office's local calendar date (fmtDate is for date-only values stored as UTC midnight). */ /** A timestamp as the office's local calendar date (fmtDate is for date-only values stored as UTC midnight). */
function fmtLocalDate(d: Date | string | null | undefined): string { function fmtLocalDate(d: Date | string | null | undefined): string {
if (!d) return ""; if (!d) return "";
@@ -313,12 +325,17 @@ async function fillOnePage(
setText(form, "21 Date of Birth MMDDCCYY", dob); setText(form, "21 Date of Birth MMDDCCYY", dob);
check(form, `22 ${genderBox}`); check(form, `22 ${genderBox}`);
// 24-31. Service lines // 24-31. Service lines (a preauth lists the planned date: 6 days after the preauth date)
const isPreauth = claim.status === "PREAUTH";
let total = 0; let total = 0;
lines.forEach((line, i) => { lines.forEach((line, i) => {
const s = lineSuffix(i, "other"); const s = lineSuffix(i, "other");
const tooth = (line.toothNumber ?? "").trim(); const tooth = (line.toothNumber ?? "").trim();
setText(form, `24 Procedure Date MMDDCCYY${s}`, fmtDate(line.procedureDate)); setText(
form,
`24 Procedure Date MMDDCCYY${s}`,
fmtDate(isPreauth ? addDays(line.procedureDate, PREAUTH_PROCEDURE_DAYS_AFTER) : line.procedureDate),
);
setText(form, `25 Area of Oral Cavity${s}`, oralCavityArea(line.quad, line.arch)); setText(form, `25 Area of Oral Cavity${s}`, oralCavityArea(line.quad, line.arch));
setText(form, `27 Tooth Numbers or Letters${s}`, tooth); setText(form, `27 Tooth Numbers or Letters${s}`, tooth);
setText(form, `28 Tooth Surface${s}`, (line.toothSurface ?? "").trim()); setText(form, `28 Tooth Surface${s}`, (line.toothSurface ?? "").trim());
+7 -2
View File
@@ -142,11 +142,16 @@ export const claimsStorage: IStorage = {
async getAppointmentIdsWithPreauth(ids: number[]): Promise<Set<number>> { async getAppointmentIdsWithPreauth(ids: number[]): Promise<Set<number>> {
if (!ids.length) return new Set(); if (!ids.length) return new Set();
const rows = await db.claim.findMany({ const rows = await db.claim.findMany({
// PREAUTH records are created before the Selenium run, so only count ones that got a number back // PREAUTH records are created before the Selenium run, so only count ones that got a number back.
// BCBS preauths are paper ADA forms (no number) saved only after the form is made.
where: { where: {
appointmentId: { in: ids }, appointmentId: { in: ids },
status: "PREAUTH" as ClaimStatus, status: "PREAUTH" as ClaimStatus,
OR: [{ preAuthNumber: { not: null } }, { claimNumber: { not: null } }], OR: [
{ preAuthNumber: { not: null } },
{ claimNumber: { not: null } },
{ insuranceProvider: "BCBS MA" },
],
}, },
select: { appointmentId: true }, select: { appointmentId: true },
distinct: ["appointmentId"], distinct: ["appointmentId"],
@@ -1899,10 +1899,13 @@ export function ClaimForm({
// BCBS MA has no online claim submission: save the claim, then open the filled // BCBS MA has no online claim submission: save the claim, then open the filled
// 2024 ADA paper form to print, sign and mail. // 2024 ADA paper form to print, sign and mail.
// `auto` (AI claim queue): no tab — just save the filled form to Documents > Claims. // `auto` (AI claim queue): no tab — just save the filled form to Documents > Claims.
// `preauth`: same ADA form with box 1 "Request for Predetermination/Preauthorization"
// ticked (claim saved as PREAUTH), saved to Documents > PreAuth.
const handleBCBSClaim = async ( const handleBCBSClaim = async (
formToUse?: ClaimFormData & { uploadedFiles?: File[] }, formToUse?: ClaimFormData & { uploadedFiles?: File[] },
{ auto = false }: { auto?: boolean } = {}, { auto = false, preauth = false }: { auto?: boolean; preauth?: boolean } = {},
) => { ) => {
const kind = preauth ? "preauth" : "claim";
const f = formToUse ?? form; const f = formToUse ?? form;
const missingFields: string[] = []; const missingFields: string[] = [];
if (!f.memberId?.trim()) missingFields.push("Member ID"); if (!f.memberId?.trim()) missingFields.push("Member ID");
@@ -1923,7 +1926,7 @@ export function ClaimForm({
if (filteredServiceLines.length === 0) { if (filteredServiceLines.length === 0) {
toast({ toast({
title: "No procedure codes", title: "No procedure codes",
description: "Please add at least one procedure code before creating the claim form.", description: `Please add at least one procedure code before creating the ${kind} form.`,
variant: "destructive", variant: "destructive",
}); });
return; return;
@@ -1953,7 +1956,8 @@ export function ClaimForm({
appointmentId: appointmentIdToUse, appointmentId: appointmentIdToUse,
claimFiles: claimFilesMeta, claimFiles: claimFilesMeta,
...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}), ...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}),
}); ...(preauth ? { status: "PREAUTH" } : {}),
} as any);
if (auto) { if (auto) {
await saveAdaClaimForm(createdClaim.id!); await saveAdaClaimForm(createdClaim.id!);
@@ -1963,15 +1967,17 @@ export function ClaimForm({
// Saving the form marks the claim submitted — show the appointment as claimed // Saving the form marks the claim submitted — show the appointment as claimed
queryClient.invalidateQueries({ queryKey: ["appointments", "day"] }); queryClient.invalidateQueries({ queryKey: ["appointments", "day"] });
toast({ toast({
title: "BCBS claim form ready", title: preauth ? "BCBS preauth form ready" : "BCBS claim form ready",
description: "Claim saved and ADA form added to the patient's Documents > Claims. Print, sign, and mail it to BCBS MA.", description: preauth
? "PreAuth saved and ADA form added to the patient's Documents > PreAuth. Print, sign, and mail it to BCBS MA."
: "Claim saved and ADA form added to the patient's Documents > Claims. Print, sign, and mail it to BCBS MA.",
}); });
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
tab?.close(); tab?.close();
toast({ toast({
title: "BCBS claim failed", title: preauth ? "BCBS preauth failed" : "BCBS claim failed",
description: err?.message ?? "Failed to create the ADA claim form.", description: err?.message ?? `Failed to create the ADA ${kind} form.`,
variant: "destructive", variant: "destructive",
}); });
} }
@@ -2144,6 +2150,13 @@ export function ClaimForm({
handleCCAPreAuth(); handleCCAPreAuth();
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") { } else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
handleUnitedDHPreAuth(); handleUnitedDHPreAuth();
} else if (key === "bcbs" || key === "bcbs_ma") {
const pricedForm = mapPricesForForm({
form: { ...form, insuranceSiteKey: "bcbs" },
patientDOB: patient?.dateOfBirth ?? "",
insuranceSiteKey: "bcbs",
});
handleBCBSClaim(pricedForm, { auto: true, preauth: true });
} else { } else {
// Re-apply fee-schedule prices with the now-loaded patient DOB before submitting — // Re-apply fee-schedule prices with the now-loaded patient DOB before submitting —
// the earlier chatbot-prefill pass may have run before `patient` finished loading, // the earlier chatbot-prefill pass may have run before `patient` finished loading,
@@ -3331,6 +3344,13 @@ export function ClaimForm({
> >
Tufts PreAuth Tufts PreAuth
</Button> </Button>
<Button
className="w-44 bg-sky-700 hover:bg-sky-800 text-white"
onClick={() => handleBCBSClaim(undefined, { preauth: true })}
title="Save the preauth and open a filled ADA form (Request for Predetermination/Preauthorization) to print and mail"
>
BCBS PreAuth (ADA Form)
</Button>
</div> </div>
</div> </div>
</div> </div>
@@ -119,6 +119,8 @@ const PREAUTH_SITE_KEYS: Record<string, string> = {
CCA: "CCA", CCA: "CCA",
TUFTSSCO: "TUFTS_SCO", TUFTSSCO: "TUFTS_SCO",
UNITEDSCO: "UNITED_SCO", UNITEDSCO: "UNITED_SCO",
BCBS: "BCBS", // paper ADA form, filled and saved to Documents > PreAuth
BCBSMA: "BCBS",
}; };
const toPreauthSiteKey = (siteKey: string): string | undefined => const toPreauthSiteKey = (siteKey: string): string | undefined =>
PREAUTH_SITE_KEYS[siteKey.replace(/_/g, "").toUpperCase()]; PREAUTH_SITE_KEYS[siteKey.replace(/_/g, "").toUpperCase()];
@@ -144,7 +146,8 @@ function deriveInsuranceSiteKey(provider: string | null | undefined): string {
} }
function appointmentCardColor(apt: ScheduledAppointment): string { function appointmentCardColor(apt: ScheduledAppointment): string {
if (apt.hasClaimWithNumber) return "bg-gray-500 text-white"; // Claimed, or preauth done (e.g. BCBS preauth ADA form saved)
if (apt.hasClaimWithNumber || apt.hasPreauth) return "bg-gray-500 text-white";
if (apt.hasProcedures) return "bg-blue-500 text-white"; if (apt.hasProcedures) return "bg-blue-500 text-white";
return "bg-slate-100 text-gray-700 border-slate-300"; return "bg-slate-100 text-gray-700 border-slate-300";
} }
@@ -1537,12 +1540,12 @@ export default function AppointmentsPage() {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = `filled_claim_forms_${formattedSelectedDate}.zip`; a.download = `filled_claim_preauth_forms_${formattedSelectedDate}.zip`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
toast({ title: "Download started", description: `Filled claim forms for ${formattedSelectedDate} saved to your Downloads folder.` }); toast({ title: "Download started", description: `Filled claim/preauth forms for ${formattedSelectedDate} saved to your Downloads folder.` });
} catch (err: any) { } catch (err: any) {
toast({ title: "Download failed", description: err?.message ?? String(err), variant: "destructive" }); toast({ title: "Download failed", description: err?.message ?? String(err), variant: "destructive" });
} finally { } finally {
@@ -1736,7 +1739,7 @@ export default function AppointmentsPage() {
})); }));
} }
// BCBS is a paper form filled and saved in the background — come straight back here // BCBS is a paper form filled and saved in the background — come straight back here
if (!isPreauth && /^bcbs/i.test(siteKey)) sessionStorage.setItem("ai_claim_return_to_schedule", "1"); if (/^bcbs/i.test(siteKey)) sessionStorage.setItem("ai_claim_return_to_schedule", "1");
else sessionStorage.removeItem("ai_claim_return_to_schedule"); else sessionStorage.removeItem("ai_claim_return_to_schedule");
setAiClaimModalOpen(false); setAiClaimModalOpen(false);
setLocation(`/claims?appointmentId=${appointmentId}`); setLocation(`/claims?appointmentId=${appointmentId}`);
@@ -2129,7 +2132,7 @@ export default function AppointmentsPage() {
</div> </div>
</div> </div>
{/* Download Filled Claim Form: ADA forms (e.g. BCBS) for the selected date */} {/* Download Filled Claim/Preauth Form: ADA forms (e.g. BCBS) for the selected date */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm"> <div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button <Button
onClick={() => handleDownloadClaimPdfs()} onClick={() => handleDownloadClaimPdfs()}
@@ -2144,7 +2147,7 @@ export default function AppointmentsPage() {
) : ( ) : (
<> <>
<Download className="h-4 w-4 mr-1" /> <Download className="h-4 w-4 mr-1" />
Download Filled Claim Form Download Filled Claim/Preauth Form
</> </>
)} )}
</Button> </Button>