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:
2026-07-08 00:20:37 -04:00
parent 1190bc0494
commit 99cdfa702a
36 changed files with 496 additions and 296 deletions

View File

@@ -73,6 +73,7 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
return runClaimStatusProcessor({
enrichedPayload: data.enrichedPayload,
insuranceId: data.insuranceId!,
userId: data.userId,
});
}
if (jobType === "claim-submit" || jobType === "claim-pre-auth") {
@@ -82,6 +83,7 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
claimId: data.claimId,
variant: jobType === "claim-pre-auth" ? "claim-pre-auth" : "claimsubmit",
socketId: data.socketId,
userId: data.userId,
});
}
if (jobType === "ddma-eligibility-check") {

View File

@@ -37,7 +37,7 @@ export interface BcbsMaEligibilityProcessorInput {
export interface BcbsMaEligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
}
@@ -50,7 +50,7 @@ async function processBcbsMaResult(
seleniumResult: any
): Promise<BcbsMaEligibilityProcessorResult> {
const output: BcbsMaEligibilityProcessorResult = {};
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
try {
// Prefer names extracted from the BCBS MA results page (Demographic Information section)
@@ -133,16 +133,17 @@ async function processBcbsMaResult(
}
if (pdfBuffer && pdfFilename) {
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
pdfFilename,
pdfBuffer
);
createdPdfFileId = `cloud:${cloudFile.id}`;
output.pdfUploadStatus = "PDF saved to Eligibility folder";
output.pdfFilename = pdfFilename;
}

View File

@@ -79,7 +79,7 @@ export interface CCAClaimProcessorInput {
export async function runCCAClaimProcessor(
input: CCAClaimProcessorInput,
jobId: string
): Promise<{ status: string; claimNumber?: string | null; pdfFileId?: number | null }> {
): Promise<{ status: string; claimNumber?: string | null; pdfFileId?: number | string | null }> {
const { enrichedPayload, userId, claimId, socketId } = input;
log("cca-claim-processor", "starting Python agent session", { claimId });
@@ -106,19 +106,22 @@ export async function runCCAClaimProcessor(
seleniumResult?.pdfFilename || `cca_claim_${claimId ?? "unknown"}_${Date.now()}.pdf`;
// Save PDF to patient's Claims document group
let pdfFileId: number | null = null;
let pdfFileId: number | string | null = null;
if (pdfBase64 && enrichedPayload?.claim?.patientId) {
try {
const patientId = Number(enrichedPayload.claim.patientId);
const pdfBuffer = Buffer.from(pdfBase64, "base64");
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) {
group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
}
const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
pdfFileId = Number((created as any).id);
}
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
"Claims",
pdfFilename,
pdfBuffer
);
pdfFileId = `cloud:${cloudFile.id}`;
log("cca-claim-processor", "PDF saved", { pdfFilename, pdfFileId, patientId });
} catch (e: any) {
log("cca-claim-processor", "failed to save PDF", { error: e?.message });

View File

@@ -59,7 +59,7 @@ export interface CCAEligibilityProcessorInput {
export interface CCAEligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
}
@@ -74,7 +74,7 @@ async function processCCAResult(
seleniumResult: any
): Promise<CCAEligibilityProcessorResult> {
const output: CCAEligibilityProcessorResult = {};
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
try {
// 1) Resolve patient name
@@ -143,18 +143,17 @@ async function processCCAResult(
// 6) Save PDF to patient document group
if (pdfBuffer && pdfFilename) {
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
pdfFilename,
pdfBuffer
);
createdPdfFileId = `cloud:${cloudFile.id}`;
output.pdfUploadStatus = "PDF saved to Eligibility folder";
output.pdfFilename = pdfFilename;
}

View File

@@ -73,7 +73,7 @@ export interface CCAPreAuthProcessorInput {
export async function runCCAPreAuthProcessor(
input: CCAPreAuthProcessorInput,
jobId: string
): Promise<{ status: string; authNumber?: string | null; pdfFileId?: number | null }> {
): Promise<{ status: string; authNumber?: string | null; pdfFileId?: number | string | null }> {
const { enrichedPayload, userId, claimId, socketId } = input;
log("cca-preauth-processor", "starting Python agent session", { claimId });
@@ -110,19 +110,22 @@ export async function runCCAPreAuthProcessor(
}
// Save PDF to patient's PreAuth document group
let pdfFileId: number | null = null;
let pdfFileId: number | string | null = null;
if (pdfBase64 && enrichedPayload?.claim?.patientId) {
try {
const patientId = Number(enrichedPayload.claim.patientId);
const pdfBuffer = Buffer.from(pdfBase64, "base64");
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH");
if (!group) {
group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH");
}
const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
pdfFileId = Number((created as any).id);
}
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
"PreAuth",
pdfFilename,
pdfBuffer
);
pdfFileId = `cloud:${cloudFile.id}`;
log("cca-preauth-processor", "PDF saved", { pdfFilename, pdfFileId, patientId });
} catch (e: any) {
log("cca-preauth-processor", "failed to save PDF", { error: e?.message });

View File

@@ -15,17 +15,18 @@ import {
export interface ClaimStatusProcessorInput {
enrichedPayload: any;
insuranceId: string; // memberId used to look up the patient
userId: number;
}
export interface ClaimStatusProcessorResult {
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
}
export async function runClaimStatusProcessor(
input: ClaimStatusProcessorInput
): Promise<ClaimStatusProcessorResult> {
const { enrichedPayload, insuranceId } = input;
const { enrichedPayload, insuranceId, userId } = input;
// 1) Call the Python service synchronously (BullMQ worker handles async)
const result = await callPythonSync("/claim-status-check", {
@@ -61,22 +62,19 @@ export async function runClaimStatusProcessor(
}
if (pdfBuffer && generatedPdfPath) {
const groupTitleKey = "CLAIM_STATUS";
const groupTitle = "Claim Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
const basename = path.basename(generatedPdfPath);
const created = await storage.createPdfFile(group.id, basename, pdfBuffer);
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName || `Patient ${patient.id}`,
"Claim Status",
basename,
pdfBuffer
);
const createdPdfFileId: number | string | null = `cloud:${cloudFile.id}`;
let createdPdfFileId: number | null = null;
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`;
outputResult.pdfUploadStatus = `PDF saved to Claim Status`;
outputResult.pdfFileId = createdPdfFileId;
}
} else {

View File

@@ -15,6 +15,7 @@ export interface ClaimSubmitProcessorInput {
variant?: "claimsubmit" | "claim-pre-auth";
/** When set, the frontend socket listener will handle the PDF download — skip auto-save */
socketId?: string;
userId: number;
}
export interface ClaimSubmitProcessorResult {
@@ -28,7 +29,8 @@ export interface ClaimSubmitProcessorResult {
async function savePdfFromSelenium(
pdf_url: string,
patientId: number,
variant: "claimsubmit" | "claim-pre-auth"
variant: "claimsubmit" | "claim-pre-auth",
userId: number
) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
@@ -37,15 +39,19 @@ async function savePdfFromSelenium(
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
const groupTitleKey = variant === "claim-pre-auth" ? "INSURANCE_CLAIM_PREAUTH" : "INSURANCE_CLAIM";
const groupTitle = variant === "claim-pre-auth" ? "Preauth" : "Claims";
const category = variant === "claim-pre-auth" ? "PreAuth" : "Claims";
let group = await storage.findPdfGroupByPatientTitleKey(patientId, groupTitleKey);
if (!group) {
group = await storage.createPdfGroup(patientId, groupTitle, groupTitleKey);
}
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
await storage.createPdfFile(group.id!, filename, resp.data);
await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
category,
filename,
Buffer.from(resp.data)
);
console.log(`[claimSubmitProcessor] PDF saved for patient ${patientId}: ${filename}`);
} catch (err: any) {
// Non-fatal — claim was submitted; just log the PDF failure
@@ -56,7 +62,7 @@ async function savePdfFromSelenium(
export async function runClaimSubmitProcessor(
input: ClaimSubmitProcessorInput
): Promise<ClaimSubmitProcessorResult> {
const { enrichedPayload, files, claimId } = input;
const { enrichedPayload, files, claimId, userId } = input;
const variant = input.variant ?? "claimsubmit";
// Build the same payload shape the Python /claimsubmit endpoint expects
@@ -97,7 +103,8 @@ export async function runClaimSubmitProcessor(
await savePdfFromSelenium(
result.pdf_url,
Number(enrichedPayload.patientId),
variant
variant,
userId
);
}

View File

@@ -26,13 +26,13 @@ export interface CmspEligibilityHistoryRemainingProcessorInput {
export interface CmspEligibilityHistoryRemainingProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
memberDetailsPdfFileId?: number | null;
memberDetailsPdfFileId?: number | string | null;
memberDetailsPdfFilename?: string | null;
historyPdfFileId?: number | null;
historyPdfFileId?: number | string | null;
historyPdfFilename?: string | null;
accumulatorPdfFileId?: number | null;
accumulatorPdfFileId?: number | string | null;
accumulatorPdfFilename?: string | null;
}
@@ -108,20 +108,20 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// Helper: save a single PDF to the patient's ELIGIBILITY_STATUS group
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
// Helper: save a single PDF to the patient's Cloud Storage Eligibility folder
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
const saveToGroup = async (pdfPath: string): Promise<number | string | null> => {
try {
const buffer = await fs.readFile(pdfPath);
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
if (created && typeof created === "object" && "id" in created) {
return Number(created.id);
}
return null;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient!.id,
patientName,
"Eligibility",
path.basename(pdfPath),
buffer
);
return `cloud:${cloudFile.id}`;
} catch (e: any) {
console.error("[cmspProcessor] saveToGroup failed:", e.message);
return null;

View File

@@ -88,15 +88,22 @@ async function pollUntilDone(
throw new Error(`DDMA claim polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
await storage.createPdfFile(group.id!, filename, resp.data);
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : `Patient ${patientId}`;
await storage.savePdfToCloudStorage(
userId,
patientId,
patientName,
"Claims",
filename,
Buffer.from(resp.data)
);
log("ddma-claim-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("ddma-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
@@ -160,7 +167,7 @@ export async function runDDMAClaimProcessor(
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId);
}
emitToSocket(socketId, "selenium:ddma_claim_completed", {

View File

@@ -66,7 +66,7 @@ export interface DdmaEligibilityProcessorInput {
export interface DdmaEligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
}
@@ -81,7 +81,7 @@ async function processDdmaResult(
seleniumResult: any
): Promise<DdmaEligibilityProcessorResult> {
const output: DdmaEligibilityProcessorResult = {};
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
try {
// 1) Resolve patient name (prefer selenium extraction → form data)
@@ -185,18 +185,17 @@ async function processDdmaResult(
// 6) Save PDF to patient document group
if (pdfBuffer && pdfFilename) {
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
pdfFilename,
pdfBuffer
);
createdPdfFileId = `cloud:${cloudFile.id}`;
output.pdfUploadStatus = "PDF saved to Eligibility folder";
output.pdfFilename = pdfFilename;
}

View File

@@ -56,7 +56,7 @@ export interface DeltaInsEligibilityProcessorInput {
export interface DeltaInsEligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
}
@@ -71,7 +71,7 @@ async function processDeltaInsResult(
seleniumResult: any
): Promise<DeltaInsEligibilityProcessorResult> {
const output: DeltaInsEligibilityProcessorResult = {};
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
try {
// 1) Resolve patient name
@@ -132,18 +132,17 @@ async function processDeltaInsResult(
// 6) Save PDF to patient document group
if (pdfBuffer && pdfFilename) {
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
pdfFilename,
pdfBuffer
);
createdPdfFileId = `cloud:${cloudFile.id}`;
output.pdfUploadStatus = "PDF saved to Eligibility folder";
output.pdfFilename = pdfFilename;
}

View File

@@ -44,7 +44,7 @@ export interface DentaQuestEligibilityProcessorInput {
export interface DentaQuestEligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
}
@@ -57,7 +57,7 @@ async function processDentaQuestResult(
seleniumResult: any
): Promise<DentaQuestEligibilityProcessorResult> {
const output: DentaQuestEligibilityProcessorResult = {};
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
try {
const rawName =
@@ -130,18 +130,17 @@ async function processDentaQuestResult(
}
if (pdfBuffer && pdfFilename) {
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
pdfFilename,
pdfBuffer
);
createdPdfFileId = `cloud:${cloudFile.id}`;
output.pdfUploadStatus = "PDF saved to Eligibility folder";
output.pdfFilename = pdfFilename;
}

View File

@@ -26,11 +26,11 @@ export interface EligibilityHistoryProcessorInput {
export interface EligibilityHistoryProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
memberDetailsPdfFileId?: number | null;
memberDetailsPdfFileId?: number | string | null;
memberDetailsPdfFilename?: string | null;
historyPdfFileId?: number | null;
historyPdfFileId?: number | string | null;
historyPdfFilename?: string | null;
}
@@ -106,20 +106,20 @@ export async function runEligibilityHistoryProcessor(
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// Helper: save a PDF buffer to the patient's ELIGIBILITY_STATUS group
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
// Helper: save a PDF buffer to the patient's Cloud Storage Eligibility folder
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
const saveToGroup = async (pdfPath: string): Promise<number | string | null> => {
try {
const buffer = await fs.readFile(pdfPath);
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
if (created && typeof created === "object" && "id" in created) {
return Number(created.id);
}
return null;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient!.id,
patientName,
"Eligibility",
path.basename(pdfPath),
buffer
);
return `cloud:${cloudFile.id}`;
} catch (e: any) {
console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message);
return null;
@@ -127,7 +127,7 @@ export async function runEligibilityHistoryProcessor(
};
// Save eligibility PDF
let eligibilityPdfFileId: number | null = null;
let eligibilityPdfFileId: number | string | null = null;
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path);
outputResult.pdfUploadStatus = eligibilityPdfFileId
@@ -146,7 +146,7 @@ export async function runEligibilityHistoryProcessor(
}
// Save history PDF
let historyPdfFileId: number | null = null;
let historyPdfFileId: number | string | null = null;
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);

View File

@@ -29,7 +29,7 @@ export interface EligibilityProcessorInput {
export interface EligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
}
export async function runEligibilityProcessor(
@@ -122,27 +122,23 @@ export async function runEligibilityProcessor(
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// 6) Save PDF
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
try {
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(
group.id,
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
path.basename(seleniumResult.pdf_path),
pdfBuffer
);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`;
createdPdfFileId = `cloud:${cloudFile.id}`;
outputResult.pdfUploadStatus = "PDF saved to Eligibility folder";
} catch (e: any) {
outputResult.pdfUploadStatus = `PDF upload failed: ${e.message}`;
}

View File

@@ -87,15 +87,22 @@ async function pollUntilDone(
throw new Error(`Tufts SCO claim polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
await storage.createPdfFile(group.id!, filename, resp.data);
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
"Claims",
filename,
Buffer.from(resp.data)
);
log("tuftssco-claim-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("tuftssco-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
@@ -151,7 +158,7 @@ export async function runTuftsSCOClaimProcessor(
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId);
}
emitToSocket(socketId, "selenium:tuftssco_claim_completed", {

View File

@@ -88,16 +88,21 @@ async function pollUntilDone(
throw new Error(`Tufts SCO preauth polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const resp = await axios.get(pdf_url, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH");
if (!group) {
group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH");
}
await storage.createPdfFile(group.id!, filename, resp.data);
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
"PreAuth",
filename,
Buffer.from(resp.data)
);
log("tuftssco-preauth-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("tuftssco-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
@@ -158,7 +163,7 @@ export async function runTuftsSCOPreAuthProcessor(
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId);
}
emitToSocket(socketId, "selenium:tuftssco_preauth_completed", {

View File

@@ -99,18 +99,23 @@ async function pollUntilDone(
throw new Error(`UnitedDH claim polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) {
group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
}
await storage.createPdfFile(group.id!, filename, resp.data);
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
"Claims",
filename,
Buffer.from(resp.data)
);
log("uniteddh-claim-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("uniteddh-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
@@ -173,7 +178,7 @@ export async function runUnitedDHClaimProcessor(
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId);
}
emitToSocket(socketId, "selenium:uniteddh_claim_completed", {

View File

@@ -98,18 +98,23 @@ async function pollUntilDone(
throw new Error(`UnitedDH preauth polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
async function savePdfFromSelenium(pdf_url: string, patientId: number, userId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH");
if (!group) {
group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH");
}
await storage.createPdfFile(group.id!, filename, resp.data);
const patient = await storage.getPatient(patientId);
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
await storage.savePdfToCloudStorage(
userId,
patientId,
patientName || `Patient ${patientId}`,
"PreAuth",
filename,
Buffer.from(resp.data)
);
log("uniteddh-preauth-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("uniteddh-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
@@ -171,7 +176,7 @@ export async function runUnitedDHPreAuthProcessor(
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId), userId);
}
emitToSocket(socketId, "selenium:uniteddh_preauth_completed", {

View File

@@ -58,7 +58,7 @@ export interface UnitedSCOEligibilityProcessorInput {
export interface UnitedSCOEligibilityProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
pdfFileId?: number | string | null;
pdfFilename?: string | null;
}
@@ -73,7 +73,7 @@ async function processUnitedSCOResult(
seleniumResult: any
): Promise<UnitedSCOEligibilityProcessorResult> {
const output: UnitedSCOEligibilityProcessorResult = {};
let createdPdfFileId: number | null = null;
let createdPdfFileId: number | string | null = null;
try {
// 1) Resolve patient name
@@ -155,18 +155,17 @@ async function processUnitedSCOResult(
// 6) Save PDF to patient document group
if (pdfBuffer && pdfFilename) {
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
if (created && typeof created === "object" && "id" in created) {
createdPdfFileId = Number(created.id);
}
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
const cloudFile = await storage.savePdfToCloudStorage(
userId,
patient.id,
patientName,
"Eligibility",
pdfFilename,
pdfBuffer
);
createdPdfFileId = `cloud:${cloudFile.id}`;
output.pdfUploadStatus = "PDF saved to Eligibility folder";
output.pdfFilename = pdfFilename;
}

View File

@@ -50,6 +50,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
result = await runClaimStatusProcessor({
enrichedPayload,
insuranceId: job.data.insuranceId!,
userId,
});
} else if (jobType === "claim-submit") {
result = await runClaimSubmitProcessor({
@@ -58,6 +59,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
claimId: job.data.claimId,
variant: "claimsubmit",
socketId,
userId,
});
} else if (jobType === "claim-pre-auth") {
result = await runClaimSubmitProcessor({
@@ -66,6 +68,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
claimId: job.data.claimId,
variant: "claim-pre-auth",
socketId,
userId,
});
} else {
throw new Error(`Unknown selenium jobType: ${jobType}`);

View File

@@ -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) {

View File

@@ -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 });

View File

@@ -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) {

View File

@@ -143,6 +143,24 @@ export interface IStorage {
patientId: number,
patientName: string
): Promise<CloudFolder>;
getOrCreateSubfolder(
userId: number,
parentId: number,
name: string
): Promise<CloudFolder>;
savePdfToCloudStorage(
userId: number,
patientId: number,
patientName: string,
category: "Eligibility" | "Claims" | "PreAuth" | "Claim Status" | "Attachments",
filename: string,
buffer: Buffer,
mimeType?: string
): Promise<CloudFile>;
listPatientCategoryCloudFiles(
patientId: number,
category: string
): Promise<CloudFile[]>;
}
/* ------------------------------- Implementation ------------------------------- */
@@ -546,6 +564,82 @@ export const cloudStorageStorage: IStorage = {
return created as unknown as CloudFolder;
},
async getOrCreateSubfolder(userId: number, parentId: number, name: string) {
const existing = await db.cloudFolder.findFirst({
where: { parentId, name },
});
if (existing) return existing as unknown as CloudFolder;
try {
return await cloudStorageStorage.createFolder(userId, name, parentId);
} catch (err: any) {
// unique constraint on [userId, parentId, name] - another request created it first
if (err?.code === "P2002") {
const race = await db.cloudFolder.findFirst({ where: { parentId, name } });
if (race) return race as unknown as CloudFolder;
}
throw err;
}
},
async savePdfToCloudStorage(
userId: number,
patientId: number,
patientName: string,
category: "Eligibility" | "Claims" | "PreAuth" | "Claim Status" | "Attachments",
filename: string,
buffer: Buffer,
mimeType: string = "application/pdf"
) {
const patientFolder = await cloudStorageStorage.getOrCreatePatientFolder(
userId,
patientId,
patientName
);
const subfolder = await cloudStorageStorage.getOrCreateSubfolder(
userId,
patientFolder.id!,
category
);
const cloudFile = await cloudStorageStorage.initializeFileUpload(
userId,
filename,
mimeType,
BigInt(buffer.length),
1,
subfolder.id!
);
await cloudStorageStorage.appendFileChunk(cloudFile.id!, 0, buffer);
await cloudStorageStorage.finalizeFileUpload(cloudFile.id!);
const finalFile = await cloudStorageStorage.getFile(cloudFile.id!);
if (!finalFile) throw new Error("Failed to finalize PDF upload to cloud storage");
return finalFile;
},
async listPatientCategoryCloudFiles(patientId: number, category: string) {
const patientFolder = await db.cloudFolder.findFirst({ where: { patientId } });
if (!patientFolder) return [];
const subfolder = await db.cloudFolder.findFirst({
where: { parentId: patientFolder.id, name: category },
});
if (!subfolder) return [];
const files = await db.cloudFile.findMany({
where: { folderId: subfolder.id, isComplete: true },
orderBy: { createdAt: "desc" },
select: {
id: true,
name: true,
mimeType: true,
fileSize: true,
folderId: true,
isComplete: true,
createdAt: true,
updatedAt: true,
},
});
return files.map(serializeFile) as unknown as CloudFile[];
},
// --- STREAM ---
async streamFileTo(resStream: NodeJS.WritableStream, fileId: number) {
const file = await db.cloudFile.findUnique({

View File

@@ -5,7 +5,7 @@ import { Maximize2, Minimize2, Download, X } from "lucide-react";
import { viewDocument } from "@/lib/api/documents";
type Props = {
fileId: number | null;
fileId: number | string | null;
isOpen: boolean;
onClose: () => void;
initialFileName?: string | null;
@@ -60,8 +60,8 @@ export default function DocumentsFilePreviewModal({
setLoading(false);
return;
} else if (isPatientDocument && fileId) {
// For patient documents, use the viewDocument API to get the URL
const documentUrl = viewDocument(fileId);
// For patient documents, use the viewDocument API to get the URL (always a numeric id)
const documentUrl = viewDocument(Number(fileId));
res = await fetch(documentUrl);
} else {
// For PDF files, use the existing endpoint

View File

@@ -92,7 +92,7 @@ interface BcbsMaEligibilityButtonProps {
isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
}
export function BcbsMaEligibilityButton({
@@ -204,7 +204,7 @@ export function BcbsMaEligibilityButton({
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
const pdfId = data.result?.pdfFileId;
if (pdfId) {
onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`);
onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`);
}
} else if (data.status === "failed") {
const msg = data.error ?? "BCBS MA eligibility job failed.";

View File

@@ -19,7 +19,7 @@ interface CCAEligibilityButtonProps {
isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
}
export function CCAEligibilityButton({
@@ -143,7 +143,7 @@ export function CCAEligibilityButton({
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
const pdfId = data.result?.pdfFileId;
if (pdfId) {
onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_cca_${memberId}.pdf`);
onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_cca_${memberId}.pdf`);
}
} else if (data.status === "failed") {
const msg = data.error ?? "CCA eligibility job failed.";

View File

@@ -90,7 +90,7 @@ interface DdmaEligibilityButtonProps {
isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
}
export function DdmaEligibilityButton({
@@ -248,7 +248,7 @@ export function DdmaEligibilityButton({
if (pdfId) {
const filename =
data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`;
onPdfReady(Number(pdfId), filename);
onPdfReady(pdfId, filename);
}
} else if (data.status === "failed") {
const msg = data.error ?? "DDMA eligibility job failed.";
@@ -326,7 +326,7 @@ export function DdmaEligibilityButton({
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
const pdfId = data.result?.pdfFileId;
if (pdfId) {
onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`);
onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`);
}
} else if (data.status === "failed") {
const msg = data.error ?? "DDMA eligibility job failed.";

View File

@@ -89,7 +89,7 @@ interface DeltaInsEligibilityButtonProps {
isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
}
export function DeltaInsEligibilityButton({
@@ -246,7 +246,7 @@ export function DeltaInsEligibilityButton({
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
const pdfId = data.result?.pdfFileId;
if (pdfId) {
onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_deltains_${memberId}.pdf`);
onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_deltains_${memberId}.pdf`);
}
} else if (data.status === "failed") {
const msg = data.error ?? "Delta Ins eligibility job failed.";

View File

@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { apiRequest } from "@/lib/queryClient";
export interface PdfPanelConfig {
pdfId?: number | null;
pdfId?: number | string | null;
fallbackFilename?: string | null;
label: string;
autoDownload?: boolean;
@@ -34,7 +34,7 @@ function parseFilename(header: string | null): string | null {
return null;
}
function usePdfBlob(open: boolean, pdfId?: number | null, fallbackFilename?: string | null) {
function usePdfBlob(open: boolean, pdfId?: number | string | null, fallbackFilename?: string | null) {
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [filename, setFilename] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

View File

@@ -7,7 +7,7 @@ import { Maximize2, Minimize2 } from "lucide-react";
interface Props {
open: boolean;
onClose: () => void;
pdfId?: number | null;
pdfId?: number | string | null;
fallbackFilename?: string | null;
autoDownload?: boolean;
}

View File

@@ -89,7 +89,7 @@ interface TuftsSCOEligibilityButtonProps {
isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
}
export function TuftsSCOEligibilityButton({
@@ -243,7 +243,7 @@ export function TuftsSCOEligibilityButton({
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
const pdfId = data.result?.pdfFileId;
if (pdfId) {
onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
}
} else if (data.status === "failed") {
const msg = data.error ?? "Tufts SCO eligibility job failed.";

View File

@@ -89,7 +89,7 @@ interface UnitedSCOEligibilityButtonProps {
isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
}
export function UnitedSCOEligibilityButton({
@@ -243,7 +243,7 @@ export function UnitedSCOEligibilityButton({
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
const pdfId = data.result?.pdfFileId;
if (pdfId) {
onPdfReady(Number(pdfId), data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
onPdfReady(pdfId, data.result?.pdfFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
}
} else if (data.status === "failed") {
const msg = data.error ?? "United SCO eligibility job failed.";

View File

@@ -46,7 +46,7 @@ export default function ClaimsPage() {
>(null);
// PDF preview modal state
const [previewOpen, setPreviewOpen] = useState(false);
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
const [previewPdfId, setPreviewPdfId] = useState<number | string | null>(null);
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<string | null>(null);
const dispatch = useAppDispatch();
const { status, message, show } = useAppSelector(

View File

@@ -15,7 +15,13 @@ import { Eye, Trash, Download, FolderOpen, FileText, HardDrive } from "lucide-re
import { useAuth } from "@/hooks/use-auth";
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
import { PatientTable } from "@/components/patients/patient-table";
import { Patient, PdfFile } from "@repo/db/types";
import { Patient, PdfFile as PrismaPdfFile } from "@repo/db/types";
// A PDF list item as returned by /api/documents/pdf-files/group/:groupId and
// /api/documents/recent-pdf-files/group/:groupId — id is a plain number for
// legacy (Postgres-blob) records, or a "cloud:<id>" string for records saved
// to the new Cloud Storage backend.
type PdfFile = Omit<PrismaPdfFile, "id"> & { id: number | string };
import {
Pagination,
PaginationContent,
@@ -50,7 +56,7 @@ export default function DocumentsPage() {
const [showPatientDocuments, setShowPatientDocuments] = useState(false);
// Document preview state
const [previewDocumentId, setPreviewDocumentId] = useState<number | null>(null);
const [previewDocumentId, setPreviewDocumentId] = useState<number | string | null>(null);
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
// Delete document state
@@ -245,7 +251,7 @@ export default function DocumentsPage() {
// DELETE mutation
const deletePdfMutation = useMutation({
mutationFn: async (id: number) => {
mutationFn: async (id: number | string) => {
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
},
onSuccess: () => {
@@ -267,7 +273,7 @@ export default function DocumentsPage() {
const handleConfirmDeletePdf = () => {
if (currentPdf) {
deletePdfMutation.mutate(Number(currentPdf.id));
deletePdfMutation.mutate(currentPdf.id);
} else {
toast({
title: "Error",
@@ -277,12 +283,12 @@ export default function DocumentsPage() {
}
};
const handleViewPdf = (pdfId: number, filename?: string) => {
const handleViewPdf = (pdfId: number | string, filename?: string) => {
setPreviewDocumentId(pdfId);
setIsPreviewModalOpen(true);
};
const handleDownloadPdf = async (pdfId: number, filename: string) => {
const handleDownloadPdf = async (pdfId: number | string, filename: string) => {
const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
const arrayBuffer = await res.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
@@ -447,7 +453,7 @@ export default function DocumentsPage() {
size="sm"
onClick={() =>
handleViewPdf(
Number(pdf.id),
pdf.id,
pdf.filename
)
}
@@ -459,7 +465,7 @@ export default function DocumentsPage() {
size="sm"
onClick={() =>
handleDownloadPdf(
Number(pdf.id),
pdf.id,
pdf.filename
)
}

View File

@@ -142,14 +142,14 @@ export default function InsuranceStatusPage() {
// PDF preview modal state
const [previewOpen, setPreviewOpen] = useState(false);
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
const [previewPdfId, setPreviewPdfId] = useState<number | string | null>(null);
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<
string | null
>(null);
// Dual PDF modal state (used by MH Eligibility & History)
const [dualPreviewOpen, setDualPreviewOpen] = useState(false);
const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState<number | null>(null);
const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState<number | string | null>(null);
const [dualEligibilityFilename, setDualEligibilityFilename] = useState<string | null>(null);
const [dualMemberDetailsPdfId, setDualMemberDetailsPdfId] = useState<number | null>(null);
const [dualMemberDetailsFilename, setDualMemberDetailsFilename] = useState<string | null>(null);
@@ -158,7 +158,7 @@ export default function InsuranceStatusPage() {
// CMSP PDF modal state (used by CMSP Eligibility & History & Remaining)
const [cmspPreviewOpen, setCmspPreviewOpen] = useState(false);
const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState<number | null>(null);
const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState<number | string | null>(null);
const [cmspEligibilityFilename, setCmspEligibilityFilename] = useState<string | null>(null);
const [cmspMemberDetailsPdfId, setCmspMemberDetailsPdfId] = useState<number | null>(null);
const [cmspMemberDetailsFilename, setCmspMemberDetailsFilename] = useState<string | null>(null);
@@ -381,7 +381,7 @@ export default function InsuranceStatusPage() {
void tryAppointmentFromChatbot();
if (jobResult.pdfFileId) {
setPreviewPdfId(Number(jobResult.pdfFileId));
setPreviewPdfId(jobResult.pdfFileId);
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
setPreviewOpen(true);
}
@@ -466,7 +466,7 @@ export default function InsuranceStatusPage() {
setSelectedPatient(null);
if (jobResult.pdfFileId) {
setPreviewPdfId(Number(jobResult.pdfFileId));
setPreviewPdfId(jobResult.pdfFileId);
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
setPreviewOpen(true);
}
@@ -579,7 +579,7 @@ export default function InsuranceStatusPage() {
// Open all PDFs side by side in the modal
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
setDualEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
setDualEligibilityPdfId(jobResult.pdfFileId ?? null);
setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
setDualMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
setDualMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `eligibility_member_details_${memberId}.pdf`);
@@ -649,7 +649,7 @@ export default function InsuranceStatusPage() {
// Open 4-panel modal
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
setCmspEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
setCmspEligibilityPdfId(jobResult.pdfFileId ?? null);
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);