diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 1da7fa9b..9954a2fd 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -1038,7 +1038,7 @@ router.post( ); // 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. router.get( "/filled-forms", @@ -1073,11 +1073,15 @@ router.get( } for (const [patientId, claimIds] of claimIdsByPatient) { - // Filled forms are saved as ADA_Claim___.pdf in the patient's "Claims" folder; - // keep the newest file per claim + // Filled forms are saved as ADA_Claim___.pdf in the patient's "Claims" + // folder, and preauths as ADA_PreAuth_….pdf in "PreAuth"; keep the newest file per claim const newestByClaim = new Map(); - for (const cf of await storage.listPatientCategoryCloudFiles(patientId, "Claims")) { - const m = /^ADA_Claim_.*_(\d+)\.pdf$/.exec(cf.name ?? ""); + const files = [ + ...(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; const claimId = Number(m[1]); if (!claimIds.has(claimId)) continue; @@ -1095,10 +1099,10 @@ router.get( } 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-Disposition", `attachment; filename="${zipFilename}"`); @@ -1134,7 +1138,7 @@ router.get( } catch (err: any) { console.error("[filled-forms] error:", err); 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 { 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, "_"); 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 = req.user.id, patient.id, `${patient.firstName} ${patient.lastName}`, - "Claims", + result.claimStatus === "PREAUTH" ? "PreAuth" : "Claims", result.filename, result.pdf, ); @@ -1449,8 +1454,9 @@ router.post("/", async (req: Request, res: Response): Promise => { await syncAppointmentType(claim.appointmentId, rawCodes); // 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"; - if (!isDraft) { + if (!isDraft && parsedClaim.status !== "PREAUTH") { await storage.createPayment({ claimId: claim.id, patientId: claim.patientId, diff --git a/apps/Backend/src/services/adaClaimForm.ts b/apps/Backend/src/services/adaClaimForm.ts index ec1f0dd4..852eb62f 100644 --- a/apps/Backend/src/services/adaClaimForm.ts +++ b/apps/Backend/src/services/adaClaimForm.ts @@ -123,6 +123,18 @@ function fmtDate(d: Date | string | null | undefined): string { 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). */ function fmtLocalDate(d: Date | string | null | undefined): string { if (!d) return ""; @@ -313,12 +325,17 @@ async function fillOnePage( setText(form, "21 Date of Birth MMDDCCYY", dob); 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; lines.forEach((line, i) => { const s = lineSuffix(i, "other"); 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, `27 Tooth Numbers or Letters${s}`, tooth); setText(form, `28 Tooth Surface${s}`, (line.toothSurface ?? "").trim()); diff --git a/apps/Backend/src/storage/claims-storage.ts b/apps/Backend/src/storage/claims-storage.ts index a064903c..cdaf5d5f 100755 --- a/apps/Backend/src/storage/claims-storage.ts +++ b/apps/Backend/src/storage/claims-storage.ts @@ -142,11 +142,16 @@ export const claimsStorage: IStorage = { async getAppointmentIdsWithPreauth(ids: number[]): Promise> { if (!ids.length) return new Set(); 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: { appointmentId: { in: ids }, status: "PREAUTH" as ClaimStatus, - OR: [{ preAuthNumber: { not: null } }, { claimNumber: { not: null } }], + OR: [ + { preAuthNumber: { not: null } }, + { claimNumber: { not: null } }, + { insuranceProvider: "BCBS MA" }, + ], }, select: { appointmentId: true }, distinct: ["appointmentId"], diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 1536b6b8..bbc4d913 100755 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -1899,10 +1899,13 @@ export function ClaimForm({ // BCBS MA has no online claim submission: save the claim, then open the filled // 2024 ADA paper form to print, sign and mail. // `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 ( 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 missingFields: string[] = []; if (!f.memberId?.trim()) missingFields.push("Member ID"); @@ -1923,7 +1926,7 @@ export function ClaimForm({ if (filteredServiceLines.length === 0) { toast({ 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", }); return; @@ -1953,7 +1956,8 @@ export function ClaimForm({ appointmentId: appointmentIdToUse, claimFiles: claimFilesMeta, ...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}), - }); + ...(preauth ? { status: "PREAUTH" } : {}), + } as any); if (auto) { await saveAdaClaimForm(createdClaim.id!); @@ -1963,15 +1967,17 @@ export function ClaimForm({ // Saving the form marks the claim submitted — show the appointment as claimed queryClient.invalidateQueries({ queryKey: ["appointments", "day"] }); toast({ - title: "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.", + title: preauth ? "BCBS preauth form ready" : "BCBS claim form ready", + 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(); } catch (err: any) { tab?.close(); toast({ - title: "BCBS claim failed", - description: err?.message ?? "Failed to create the ADA claim form.", + title: preauth ? "BCBS preauth failed" : "BCBS claim failed", + description: err?.message ?? `Failed to create the ADA ${kind} form.`, variant: "destructive", }); } @@ -2144,6 +2150,13 @@ export function ClaimForm({ handleCCAPreAuth(); } else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") { 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 { // 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, @@ -3331,6 +3344,13 @@ export function ClaimForm({ > Tufts PreAuth + diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index 6827c95a..52d5dbb1 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -119,6 +119,8 @@ const PREAUTH_SITE_KEYS: Record = { CCA: "CCA", TUFTSSCO: "TUFTS_SCO", UNITEDSCO: "UNITED_SCO", + BCBS: "BCBS", // paper ADA form, filled and saved to Documents > PreAuth + BCBSMA: "BCBS", }; const toPreauthSiteKey = (siteKey: string): string | undefined => PREAUTH_SITE_KEYS[siteKey.replace(/_/g, "").toUpperCase()]; @@ -144,7 +146,8 @@ function deriveInsuranceSiteKey(provider: string | null | undefined): 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"; return "bg-slate-100 text-gray-700 border-slate-300"; } @@ -1537,12 +1540,12 @@ export default function AppointmentsPage() { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `filled_claim_forms_${formattedSelectedDate}.zip`; + a.download = `filled_claim_preauth_forms_${formattedSelectedDate}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); 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) { toast({ title: "Download failed", description: err?.message ?? String(err), variant: "destructive" }); } finally { @@ -1736,7 +1739,7 @@ export default function AppointmentsPage() { })); } // 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"); setAiClaimModalOpen(false); setLocation(`/claims?appointmentId=${appointmentId}`); @@ -2129,7 +2132,7 @@ export default function AppointmentsPage() { - {/* 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 */}