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:
@@ -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_<Last>_<First>_<claimId>.pdf in the patient's "Claims" folder;
|
||||
// keep the newest file per claim
|
||||
// Filled forms are saved as ADA_Claim_<Last>_<First>_<claimId>.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<number, { id: number; name: string }>();
|
||||
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<AdaFormResult> {
|
||||
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<any> =
|
||||
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<any> => {
|
||||
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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -142,11 +142,16 @@ export const claimsStorage: IStorage = {
|
||||
async getAppointmentIdsWithPreauth(ids: number[]): Promise<Set<number>> {
|
||||
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"],
|
||||
|
||||
Reference in New Issue
Block a user