feat: save insurance PDFs (eligibility/claims/preauth) to Cloud Storage instead of Postgres blobs
New saves go into the patient's Cloud Storage folder under category subfolders (Eligibility/Claims/PreAuth/Claim Status/Attachments) via a new storage.savePdfToCloudStorage helper, instead of the legacy PdfGroup/PdfFile Postgres blob tables. Old records and their read paths are left untouched for backward compatibility; PDF list/content/delete endpoints and viewers now transparently handle both legacy numeric ids and new "cloud:<id>" ids. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,11 @@ router.post(
|
||||
|
||||
try {
|
||||
const folder = await storage.getOrCreatePatientFolder(req.user.id, patientId, patientName);
|
||||
const attachmentsFolder = await storage.getOrCreateSubfolder(
|
||||
req.user.id,
|
||||
(folder as any).id,
|
||||
"Attachments"
|
||||
);
|
||||
const result: { filename: string; mimeType: string; filePath: string }[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
@@ -126,14 +131,14 @@ router.post(
|
||||
file.buffer
|
||||
);
|
||||
|
||||
// Save to cloud storage patient folder (Cloud Storage page)
|
||||
// Save to cloud storage patient's Attachments subfolder (Cloud Storage page)
|
||||
const cloudFile = await storage.initializeFileUpload(
|
||||
req.user.id,
|
||||
file.originalname,
|
||||
file.mimetype,
|
||||
BigInt(file.size),
|
||||
1,
|
||||
(folder as any).id
|
||||
(attachmentsFolder as any).id
|
||||
);
|
||||
await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer);
|
||||
const finalized = await storage.finalizeFileUpload((cloudFile as any).id);
|
||||
@@ -337,36 +342,28 @@ router.post(
|
||||
);
|
||||
}
|
||||
|
||||
const GROUP_TITLES: Record<GroupKey, string> = {
|
||||
const CLOUD_CATEGORIES: Record<GroupKey, "Claims" | "PreAuth"> = {
|
||||
INSURANCE_CLAIM: "Claims",
|
||||
INSURANCE_CLAIM_PREAUTH: "Preauth",
|
||||
INSURANCE_CLAIM_PREAUTH: "PreAuth",
|
||||
};
|
||||
const groupTitle = GROUP_TITLES[groupTitleKey];
|
||||
const cloudCategory = CLOUD_CATEGORIES[groupTitleKey];
|
||||
|
||||
// ✅ Find or create PDF group for this claim
|
||||
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||
const patient = await storage.getPatient(parsedPatientId);
|
||||
const patientName = patient
|
||||
? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
|
||||
: "";
|
||||
|
||||
// ✅ Save PDF file into the patient's Cloud Storage category subfolder
|
||||
const cloudFile = await storage.savePdfToCloudStorage(
|
||||
req.user.id,
|
||||
parsedPatientId,
|
||||
groupTitleKey
|
||||
patientName || `Patient ${parsedPatientId}`,
|
||||
cloudCategory,
|
||||
filename,
|
||||
Buffer.from(pdfResponse.data)
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
group = await storage.createPdfGroup(
|
||||
parsedPatientId,
|
||||
groupTitle,
|
||||
groupTitleKey
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Save PDF file into that group
|
||||
const created = await storage.createPdfFile(group.id!, filename, pdfResponse.data);
|
||||
|
||||
// Extract the PDF file ID for opening the viewer
|
||||
let pdfFileId: number | null = null;
|
||||
if (created && typeof created === "object" && "id" in created) {
|
||||
pdfFileId = Number(created.id);
|
||||
} else if (typeof created === "number") {
|
||||
pdfFileId = created;
|
||||
}
|
||||
const pdfFileId: string = `cloud:${cloudFile.id}`;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
@@ -942,6 +939,20 @@ router.get(
|
||||
const safeName = (f.filename || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf";
|
||||
pdfEntries.push({ filename: safeName, data: buf });
|
||||
}
|
||||
|
||||
// New claim PDFs are saved to the patient's Cloud Storage "Claims" subfolder
|
||||
const cloudFiles = await storage.listPatientCategoryCloudFiles(patientId, "Claims");
|
||||
for (const cf of cloudFiles) {
|
||||
if (cf.id == null) continue;
|
||||
const full = await storage.getFile(cf.id);
|
||||
if (!full?.diskPath) continue;
|
||||
const abs = path.join(process.cwd(), full.diskPath);
|
||||
if (!fs.existsSync(abs)) continue;
|
||||
const buf = fs.readFileSync(abs);
|
||||
if (!buf.length) continue;
|
||||
const safeName = (cf.name || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf";
|
||||
pdfEntries.push({ filename: safeName, data: buf });
|
||||
}
|
||||
}
|
||||
|
||||
if (pdfEntries.length === 0) {
|
||||
|
||||
@@ -150,6 +150,34 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
// Maps a legacy PdfGroup.titleKey to the matching Cloud Storage subfolder name
|
||||
// so new (cloud-backed) PDFs show up alongside old (DB-blob) PDFs for the same category.
|
||||
const TITLE_KEY_TO_CLOUD_CATEGORY: Record<string, string> = {
|
||||
ELIGIBILITY_STATUS: "Eligibility",
|
||||
INSURANCE_CLAIM: "Claims",
|
||||
INSURANCE_CLAIM_PREAUTH: "PreAuth",
|
||||
CLAIM_STATUS: "Claim Status",
|
||||
OTHER: "Attachments",
|
||||
};
|
||||
|
||||
async function listCloudPdfsForGroup(groupId: number) {
|
||||
const group = await storage.getPdfGroupById(groupId);
|
||||
if (!group) return [];
|
||||
const category = TITLE_KEY_TO_CLOUD_CATEGORY[group.titleKey as string];
|
||||
if (!category) return [];
|
||||
|
||||
const cloudFiles = await storage.listPatientCategoryCloudFiles(
|
||||
group.patientId,
|
||||
category
|
||||
);
|
||||
return cloudFiles.map((f) => ({
|
||||
id: `cloud:${f.id}`,
|
||||
filename: f.name,
|
||||
uploadedAt: f.createdAt,
|
||||
mimeType: f.mimeType,
|
||||
}));
|
||||
}
|
||||
|
||||
router.get(
|
||||
"/pdf-files/group/:groupId",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
@@ -160,7 +188,8 @@ router.get(
|
||||
}
|
||||
const groupId = parseInt(idParam);
|
||||
const files = await storage.getPdfFilesByGroupId(groupId);
|
||||
res.json(files);
|
||||
const cloudFiles = await listCloudPdfsForGroup(groupId);
|
||||
res.json([...cloudFiles, ...(files as any[])]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
@@ -203,6 +232,8 @@ router.get(
|
||||
// Decide whether client asked for paginated response
|
||||
const wantsPagination = typeof limit === "number";
|
||||
|
||||
const cloudFiles = await listCloudPdfsForGroup(groupId);
|
||||
|
||||
if (wantsPagination) {
|
||||
// storage.getPdfFilesByGroupId with pagination should return { total, data }
|
||||
const result = await storage.getPdfFilesByGroupId(groupId, {
|
||||
@@ -214,14 +245,17 @@ router.get(
|
||||
// result should be { total, data }, but handle unexpected shapes defensively
|
||||
if (Array.isArray(result)) {
|
||||
// fallback: storage returned full array; compute total
|
||||
return res.json({ total: result.length, data: result });
|
||||
const merged = [...cloudFiles, ...result];
|
||||
return res.json({ total: merged.length, data: merged });
|
||||
}
|
||||
|
||||
return res.json(result);
|
||||
const merged = [...cloudFiles, ...(result as any).data];
|
||||
return res.json({ total: (result as any).total + cloudFiles.length, data: merged });
|
||||
} else {
|
||||
// no limit requested -> return all files for the group
|
||||
const all = (await storage.getPdfFilesByGroupId(groupId)) as PdfFile[];
|
||||
return res.json({ total: all.length, data: all });
|
||||
const merged = [...cloudFiles, ...(all as any[])];
|
||||
return res.json({ total: merged.length, data: merged });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("GET /pdf-files/group/:groupId error:", err);
|
||||
@@ -238,6 +272,31 @@ router.get(
|
||||
if (!idParam) {
|
||||
return res.status(400).json({ error: "Missing ID" });
|
||||
}
|
||||
|
||||
// Cloud-Storage-backed PDF (new saves): id looks like "cloud:123"
|
||||
const idStr = String(idParam);
|
||||
if (idStr.startsWith("cloud:")) {
|
||||
const cloudId = parseInt(idStr.slice("cloud:".length), 10);
|
||||
if (Number.isNaN(cloudId)) {
|
||||
return res.status(400).json({ error: "Invalid ID" });
|
||||
}
|
||||
const cloudFile = await storage.getFile(cloudId);
|
||||
if (!cloudFile) return res.status(404).json({ error: "PDF not found" });
|
||||
|
||||
res.setHeader("Content-Type", cloudFile.mimeType || "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${cloudFile.name}"; filename*=UTF-8''${encodeURIComponent(cloudFile.name)}`
|
||||
);
|
||||
try {
|
||||
await storage.streamFileTo(res, cloudId);
|
||||
} catch (streamErr) {
|
||||
console.error("Error streaming cloud PDF file:", streamErr);
|
||||
if (!res.headersSent) res.status(500).json({ error: "Failed to stream PDF" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseInt(idParam, 10);
|
||||
if (Number.isNaN(id)) {
|
||||
return res.status(400).json({ error: "Invalid ID" });
|
||||
@@ -409,7 +468,18 @@ router.delete(
|
||||
if (!idParam) {
|
||||
return res.status(400).json({ error: "Missing ID" });
|
||||
}
|
||||
const id = parseInt(idParam);
|
||||
|
||||
const idStr = String(idParam);
|
||||
if (idStr.startsWith("cloud:")) {
|
||||
const cloudId = parseInt(idStr.slice("cloud:".length), 10);
|
||||
if (Number.isNaN(cloudId)) {
|
||||
return res.status(400).json({ error: "Invalid ID" });
|
||||
}
|
||||
const success = await storage.deleteFile(cloudId);
|
||||
return res.json({ success });
|
||||
}
|
||||
|
||||
const id = parseInt(idStr);
|
||||
|
||||
const success = await storage.deletePdfFile(id);
|
||||
res.json({ success });
|
||||
|
||||
@@ -521,36 +521,18 @@ router.post(
|
||||
) {
|
||||
try {
|
||||
const pdfBuf = await fs.readFile(seleniumResult.pdf_path);
|
||||
const groupTitle = "Eligibility Status";
|
||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||
const patientName = `${updatedPatient.firstName ?? ""} ${updatedPatient.lastName ?? ""}`.trim();
|
||||
|
||||
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||
const cloudFile = await storage.savePdfToCloudStorage(
|
||||
req.user.id,
|
||||
updatedPatient.id,
|
||||
groupTitleKey
|
||||
);
|
||||
if (!group) {
|
||||
group = await storage.createPdfGroup(
|
||||
updatedPatient.id,
|
||||
groupTitle,
|
||||
groupTitleKey
|
||||
);
|
||||
}
|
||||
if (!group?.id)
|
||||
throw new Error("Failed to create/find pdf group");
|
||||
|
||||
const created = await storage.createPdfFile(
|
||||
group.id,
|
||||
patientName || `Patient ${updatedPatient.id}`,
|
||||
"Eligibility",
|
||||
path.basename(seleniumResult.pdf_path),
|
||||
pdfBuf
|
||||
);
|
||||
|
||||
if (created && typeof created === "object" && "id" in created) {
|
||||
resultItem.pdfFileId = Number(created.id);
|
||||
} else if (typeof created === "number") {
|
||||
resultItem.pdfFileId = created;
|
||||
} else if (created && (created as any).id) {
|
||||
resultItem.pdfFileId = (created as any).id;
|
||||
}
|
||||
resultItem.pdfFileId = `cloud:${cloudFile.id}`;
|
||||
|
||||
resultItem.processed = true;
|
||||
} catch (pdfErr: any) {
|
||||
|
||||
Reference in New Issue
Block a user