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:
@@ -73,6 +73,7 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
|||||||
return runClaimStatusProcessor({
|
return runClaimStatusProcessor({
|
||||||
enrichedPayload: data.enrichedPayload,
|
enrichedPayload: data.enrichedPayload,
|
||||||
insuranceId: data.insuranceId!,
|
insuranceId: data.insuranceId!,
|
||||||
|
userId: data.userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (jobType === "claim-submit" || jobType === "claim-pre-auth") {
|
if (jobType === "claim-submit" || jobType === "claim-pre-auth") {
|
||||||
@@ -82,6 +83,7 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
|||||||
claimId: data.claimId,
|
claimId: data.claimId,
|
||||||
variant: jobType === "claim-pre-auth" ? "claim-pre-auth" : "claimsubmit",
|
variant: jobType === "claim-pre-auth" ? "claim-pre-auth" : "claimsubmit",
|
||||||
socketId: data.socketId,
|
socketId: data.socketId,
|
||||||
|
userId: data.userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (jobType === "ddma-eligibility-check") {
|
if (jobType === "ddma-eligibility-check") {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export interface BcbsMaEligibilityProcessorInput {
|
|||||||
export interface BcbsMaEligibilityProcessorResult {
|
export interface BcbsMaEligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ async function processBcbsMaResult(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
): Promise<BcbsMaEligibilityProcessorResult> {
|
): Promise<BcbsMaEligibilityProcessorResult> {
|
||||||
const output: BcbsMaEligibilityProcessorResult = {};
|
const output: BcbsMaEligibilityProcessorResult = {};
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prefer names extracted from the BCBS MA results page (Demographic Information section)
|
// Prefer names extracted from the BCBS MA results page (Demographic Information section)
|
||||||
@@ -133,16 +133,17 @@ async function processBcbsMaResult(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pdfBuffer && pdfFilename) {
|
if (pdfBuffer && pdfFilename) {
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
userId,
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
patient.id,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
patientName,
|
||||||
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
|
"Eligibility",
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
pdfFilename,
|
||||||
createdPdfFileId = Number(created.id);
|
pdfBuffer
|
||||||
}
|
);
|
||||||
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
|
output.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
output.pdfFilename = pdfFilename;
|
output.pdfFilename = pdfFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export interface CCAClaimProcessorInput {
|
|||||||
export async function runCCAClaimProcessor(
|
export async function runCCAClaimProcessor(
|
||||||
input: CCAClaimProcessorInput,
|
input: CCAClaimProcessorInput,
|
||||||
jobId: string
|
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;
|
const { enrichedPayload, userId, claimId, socketId } = input;
|
||||||
|
|
||||||
log("cca-claim-processor", "starting Python agent session", { claimId });
|
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`;
|
seleniumResult?.pdfFilename || `cca_claim_${claimId ?? "unknown"}_${Date.now()}.pdf`;
|
||||||
|
|
||||||
// Save PDF to patient's Claims document group
|
// Save PDF to patient's Claims document group
|
||||||
let pdfFileId: number | null = null;
|
let pdfFileId: number | string | null = null;
|
||||||
if (pdfBase64 && enrichedPayload?.claim?.patientId) {
|
if (pdfBase64 && enrichedPayload?.claim?.patientId) {
|
||||||
try {
|
try {
|
||||||
const patientId = Number(enrichedPayload.claim.patientId);
|
const patientId = Number(enrichedPayload.claim.patientId);
|
||||||
const pdfBuffer = Buffer.from(pdfBase64, "base64");
|
const pdfBuffer = Buffer.from(pdfBase64, "base64");
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) {
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
}
|
userId,
|
||||||
const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer);
|
patientId,
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
patientName || `Patient ${patientId}`,
|
||||||
pdfFileId = Number((created as any).id);
|
"Claims",
|
||||||
}
|
pdfFilename,
|
||||||
|
pdfBuffer
|
||||||
|
);
|
||||||
|
pdfFileId = `cloud:${cloudFile.id}`;
|
||||||
log("cca-claim-processor", "PDF saved", { pdfFilename, pdfFileId, patientId });
|
log("cca-claim-processor", "PDF saved", { pdfFilename, pdfFileId, patientId });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log("cca-claim-processor", "failed to save PDF", { error: e?.message });
|
log("cca-claim-processor", "failed to save PDF", { error: e?.message });
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export interface CCAEligibilityProcessorInput {
|
|||||||
export interface CCAEligibilityProcessorResult {
|
export interface CCAEligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ async function processCCAResult(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
): Promise<CCAEligibilityProcessorResult> {
|
): Promise<CCAEligibilityProcessorResult> {
|
||||||
const output: CCAEligibilityProcessorResult = {};
|
const output: CCAEligibilityProcessorResult = {};
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Resolve patient name
|
// 1) Resolve patient name
|
||||||
@@ -143,18 +143,17 @@ async function processCCAResult(
|
|||||||
|
|
||||||
// 6) Save PDF to patient document group
|
// 6) Save PDF to patient document group
|
||||||
if (pdfBuffer && pdfFilename) {
|
if (pdfBuffer && pdfFilename) {
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
patient.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
|
pdfFilename,
|
||||||
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
|
pdfBuffer
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
);
|
||||||
createdPdfFileId = Number(created.id);
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
}
|
output.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
output.pdfFilename = pdfFilename;
|
output.pdfFilename = pdfFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export interface CCAPreAuthProcessorInput {
|
|||||||
export async function runCCAPreAuthProcessor(
|
export async function runCCAPreAuthProcessor(
|
||||||
input: CCAPreAuthProcessorInput,
|
input: CCAPreAuthProcessorInput,
|
||||||
jobId: string
|
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;
|
const { enrichedPayload, userId, claimId, socketId } = input;
|
||||||
|
|
||||||
log("cca-preauth-processor", "starting Python agent session", { claimId });
|
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
|
// Save PDF to patient's PreAuth document group
|
||||||
let pdfFileId: number | null = null;
|
let pdfFileId: number | string | null = null;
|
||||||
if (pdfBase64 && enrichedPayload?.claim?.patientId) {
|
if (pdfBase64 && enrichedPayload?.claim?.patientId) {
|
||||||
try {
|
try {
|
||||||
const patientId = Number(enrichedPayload.claim.patientId);
|
const patientId = Number(enrichedPayload.claim.patientId);
|
||||||
const pdfBuffer = Buffer.from(pdfBase64, "base64");
|
const pdfBuffer = Buffer.from(pdfBase64, "base64");
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) {
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH");
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
}
|
userId,
|
||||||
const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer);
|
patientId,
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
patientName || `Patient ${patientId}`,
|
||||||
pdfFileId = Number((created as any).id);
|
"PreAuth",
|
||||||
}
|
pdfFilename,
|
||||||
|
pdfBuffer
|
||||||
|
);
|
||||||
|
pdfFileId = `cloud:${cloudFile.id}`;
|
||||||
log("cca-preauth-processor", "PDF saved", { pdfFilename, pdfFileId, patientId });
|
log("cca-preauth-processor", "PDF saved", { pdfFilename, pdfFileId, patientId });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log("cca-preauth-processor", "failed to save PDF", { error: e?.message });
|
log("cca-preauth-processor", "failed to save PDF", { error: e?.message });
|
||||||
|
|||||||
@@ -15,17 +15,18 @@ import {
|
|||||||
export interface ClaimStatusProcessorInput {
|
export interface ClaimStatusProcessorInput {
|
||||||
enrichedPayload: any;
|
enrichedPayload: any;
|
||||||
insuranceId: string; // memberId used to look up the patient
|
insuranceId: string; // memberId used to look up the patient
|
||||||
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimStatusProcessorResult {
|
export interface ClaimStatusProcessorResult {
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runClaimStatusProcessor(
|
export async function runClaimStatusProcessor(
|
||||||
input: ClaimStatusProcessorInput
|
input: ClaimStatusProcessorInput
|
||||||
): Promise<ClaimStatusProcessorResult> {
|
): Promise<ClaimStatusProcessorResult> {
|
||||||
const { enrichedPayload, insuranceId } = input;
|
const { enrichedPayload, insuranceId, userId } = input;
|
||||||
|
|
||||||
// 1) Call the Python service synchronously (BullMQ worker handles async)
|
// 1) Call the Python service synchronously (BullMQ worker handles async)
|
||||||
const result = await callPythonSync("/claim-status-check", {
|
const result = await callPythonSync("/claim-status-check", {
|
||||||
@@ -61,22 +62,19 @@ export async function runClaimStatusProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pdfBuffer && generatedPdfPath) {
|
if (pdfBuffer && generatedPdfPath) {
|
||||||
const groupTitleKey = "CLAIM_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
|
||||||
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 basename = path.basename(generatedPdfPath);
|
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;
|
outputResult.pdfUploadStatus = `PDF saved to Claim Status`;
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
|
||||||
createdPdfFileId = Number(created.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
outputResult.pdfFileId = createdPdfFileId;
|
outputResult.pdfFileId = createdPdfFileId;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface ClaimSubmitProcessorInput {
|
|||||||
variant?: "claimsubmit" | "claim-pre-auth";
|
variant?: "claimsubmit" | "claim-pre-auth";
|
||||||
/** When set, the frontend socket listener will handle the PDF download — skip auto-save */
|
/** When set, the frontend socket listener will handle the PDF download — skip auto-save */
|
||||||
socketId?: string;
|
socketId?: string;
|
||||||
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimSubmitProcessorResult {
|
export interface ClaimSubmitProcessorResult {
|
||||||
@@ -28,7 +29,8 @@ export interface ClaimSubmitProcessorResult {
|
|||||||
async function savePdfFromSelenium(
|
async function savePdfFromSelenium(
|
||||||
pdf_url: string,
|
pdf_url: string,
|
||||||
patientId: number,
|
patientId: number,
|
||||||
variant: "claimsubmit" | "claim-pre-auth"
|
variant: "claimsubmit" | "claim-pre-auth",
|
||||||
|
userId: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
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 resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
||||||
|
|
||||||
const groupTitleKey = variant === "claim-pre-auth" ? "INSURANCE_CLAIM_PREAUTH" : "INSURANCE_CLAIM";
|
const category = variant === "claim-pre-auth" ? "PreAuth" : "Claims";
|
||||||
const groupTitle = variant === "claim-pre-auth" ? "Preauth" : "Claims";
|
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, groupTitleKey);
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) {
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
group = await storage.createPdfGroup(patientId, groupTitle, groupTitleKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
console.log(`[claimSubmitProcessor] PDF saved for patient ${patientId}: ${filename}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Non-fatal — claim was submitted; just log the PDF failure
|
// Non-fatal — claim was submitted; just log the PDF failure
|
||||||
@@ -56,7 +62,7 @@ async function savePdfFromSelenium(
|
|||||||
export async function runClaimSubmitProcessor(
|
export async function runClaimSubmitProcessor(
|
||||||
input: ClaimSubmitProcessorInput
|
input: ClaimSubmitProcessorInput
|
||||||
): Promise<ClaimSubmitProcessorResult> {
|
): Promise<ClaimSubmitProcessorResult> {
|
||||||
const { enrichedPayload, files, claimId } = input;
|
const { enrichedPayload, files, claimId, userId } = input;
|
||||||
const variant = input.variant ?? "claimsubmit";
|
const variant = input.variant ?? "claimsubmit";
|
||||||
|
|
||||||
// Build the same payload shape the Python /claimsubmit endpoint expects
|
// Build the same payload shape the Python /claimsubmit endpoint expects
|
||||||
@@ -97,7 +103,8 @@ export async function runClaimSubmitProcessor(
|
|||||||
await savePdfFromSelenium(
|
await savePdfFromSelenium(
|
||||||
result.pdf_url,
|
result.pdf_url,
|
||||||
Number(enrichedPayload.patientId),
|
Number(enrichedPayload.patientId),
|
||||||
variant
|
variant,
|
||||||
|
userId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ export interface CmspEligibilityHistoryRemainingProcessorInput {
|
|||||||
export interface CmspEligibilityHistoryRemainingProcessorResult {
|
export interface CmspEligibilityHistoryRemainingProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
memberDetailsPdfFileId?: number | null;
|
memberDetailsPdfFileId?: number | string | null;
|
||||||
memberDetailsPdfFilename?: string | null;
|
memberDetailsPdfFilename?: string | null;
|
||||||
historyPdfFileId?: number | null;
|
historyPdfFileId?: number | string | null;
|
||||||
historyPdfFilename?: string | null;
|
historyPdfFilename?: string | null;
|
||||||
accumulatorPdfFileId?: number | null;
|
accumulatorPdfFileId?: number | string | null;
|
||||||
accumulatorPdfFilename?: string | null;
|
accumulatorPdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,20 +108,20 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
|
|||||||
await storage.updatePatient(patient.id, updates);
|
await storage.updatePatient(patient.id, updates);
|
||||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
// Helper: save a single PDF to the patient's ELIGIBILITY_STATUS group
|
// Helper: save a single PDF to the patient's Cloud Storage Eligibility folder
|
||||||
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
|
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
|
||||||
|
const saveToGroup = async (pdfPath: string): Promise<number | string | null> => {
|
||||||
try {
|
try {
|
||||||
const buffer = await fs.readFile(pdfPath);
|
const buffer = await fs.readFile(pdfPath);
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
const groupTitle = "Eligibility Status";
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
|
patient!.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
|
path.basename(pdfPath),
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
buffer
|
||||||
return Number(created.id);
|
);
|
||||||
}
|
return `cloud:${cloudFile.id}`;
|
||||||
return null;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("[cmspProcessor] saveToGroup failed:", e.message);
|
console.error("[cmspProcessor] saveToGroup failed:", e.message);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -88,15 +88,22 @@ async function pollUntilDone(
|
|||||||
throw new Error(`DDMA claim polling exhausted all attempts for session ${sessionId}`);
|
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 {
|
try {
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
const filename = path.basename(new URL(pdf_url).pathname);
|
||||||
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
||||||
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
||||||
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
|
const patientName = patient ? `${patient.firstName} ${patient.lastName}` : `Patient ${patientId}`;
|
||||||
await storage.createPdfFile(group.id!, filename, resp.data);
|
await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
|
patientId,
|
||||||
|
patientName,
|
||||||
|
"Claims",
|
||||||
|
filename,
|
||||||
|
Buffer.from(resp.data)
|
||||||
|
);
|
||||||
log("ddma-claim-processor", "PDF saved", { patientId, filename });
|
log("ddma-claim-processor", "PDF saved", { patientId, filename });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log("ddma-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
|
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) {
|
if (pdf_url && !socketId) {
|
||||||
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
||||||
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
|
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", {
|
emitToSocket(socketId, "selenium:ddma_claim_completed", {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface DdmaEligibilityProcessorInput {
|
|||||||
export interface DdmaEligibilityProcessorResult {
|
export interface DdmaEligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ async function processDdmaResult(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
): Promise<DdmaEligibilityProcessorResult> {
|
): Promise<DdmaEligibilityProcessorResult> {
|
||||||
const output: DdmaEligibilityProcessorResult = {};
|
const output: DdmaEligibilityProcessorResult = {};
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Resolve patient name (prefer selenium extraction → form data)
|
// 1) Resolve patient name (prefer selenium extraction → form data)
|
||||||
@@ -185,18 +185,17 @@ async function processDdmaResult(
|
|||||||
|
|
||||||
// 6) Save PDF to patient document group
|
// 6) Save PDF to patient document group
|
||||||
if (pdfBuffer && pdfFilename) {
|
if (pdfBuffer && pdfFilename) {
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
patient.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
|
pdfFilename,
|
||||||
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
|
pdfBuffer
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
);
|
||||||
createdPdfFileId = Number(created.id);
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
}
|
output.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
output.pdfFilename = pdfFilename;
|
output.pdfFilename = pdfFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export interface DeltaInsEligibilityProcessorInput {
|
|||||||
export interface DeltaInsEligibilityProcessorResult {
|
export interface DeltaInsEligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ async function processDeltaInsResult(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
): Promise<DeltaInsEligibilityProcessorResult> {
|
): Promise<DeltaInsEligibilityProcessorResult> {
|
||||||
const output: DeltaInsEligibilityProcessorResult = {};
|
const output: DeltaInsEligibilityProcessorResult = {};
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Resolve patient name
|
// 1) Resolve patient name
|
||||||
@@ -132,18 +132,17 @@ async function processDeltaInsResult(
|
|||||||
|
|
||||||
// 6) Save PDF to patient document group
|
// 6) Save PDF to patient document group
|
||||||
if (pdfBuffer && pdfFilename) {
|
if (pdfBuffer && pdfFilename) {
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
patient.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
|
pdfFilename,
|
||||||
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
|
pdfBuffer
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
);
|
||||||
createdPdfFileId = Number(created.id);
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
}
|
output.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
output.pdfFilename = pdfFilename;
|
output.pdfFilename = pdfFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export interface DentaQuestEligibilityProcessorInput {
|
|||||||
export interface DentaQuestEligibilityProcessorResult {
|
export interface DentaQuestEligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ async function processDentaQuestResult(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
): Promise<DentaQuestEligibilityProcessorResult> {
|
): Promise<DentaQuestEligibilityProcessorResult> {
|
||||||
const output: DentaQuestEligibilityProcessorResult = {};
|
const output: DentaQuestEligibilityProcessorResult = {};
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawName =
|
const rawName =
|
||||||
@@ -130,18 +130,17 @@ async function processDentaQuestResult(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pdfBuffer && pdfFilename) {
|
if (pdfBuffer && pdfFilename) {
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
patient.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
|
pdfFilename,
|
||||||
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
|
pdfBuffer
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
);
|
||||||
createdPdfFileId = Number(created.id);
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
}
|
output.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
output.pdfFilename = pdfFilename;
|
output.pdfFilename = pdfFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ export interface EligibilityHistoryProcessorInput {
|
|||||||
export interface EligibilityHistoryProcessorResult {
|
export interface EligibilityHistoryProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
memberDetailsPdfFileId?: number | null;
|
memberDetailsPdfFileId?: number | string | null;
|
||||||
memberDetailsPdfFilename?: string | null;
|
memberDetailsPdfFilename?: string | null;
|
||||||
historyPdfFileId?: number | null;
|
historyPdfFileId?: number | string | null;
|
||||||
historyPdfFilename?: string | null;
|
historyPdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,20 +106,20 @@ export async function runEligibilityHistoryProcessor(
|
|||||||
await storage.updatePatient(patient.id, updates);
|
await storage.updatePatient(patient.id, updates);
|
||||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
// Helper: save a PDF buffer to the patient's ELIGIBILITY_STATUS group
|
// Helper: save a PDF buffer to the patient's Cloud Storage Eligibility folder
|
||||||
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
|
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
|
||||||
|
const saveToGroup = async (pdfPath: string): Promise<number | string | null> => {
|
||||||
try {
|
try {
|
||||||
const buffer = await fs.readFile(pdfPath);
|
const buffer = await fs.readFile(pdfPath);
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
const groupTitle = "Eligibility Status";
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
|
patient!.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
|
path.basename(pdfPath),
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
buffer
|
||||||
return Number(created.id);
|
);
|
||||||
}
|
return `cloud:${cloudFile.id}`;
|
||||||
return null;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message);
|
console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message);
|
||||||
return null;
|
return null;
|
||||||
@@ -127,7 +127,7 @@ export async function runEligibilityHistoryProcessor(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Save eligibility PDF
|
// Save eligibility PDF
|
||||||
let eligibilityPdfFileId: number | null = null;
|
let eligibilityPdfFileId: number | string | null = null;
|
||||||
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
||||||
eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
||||||
outputResult.pdfUploadStatus = eligibilityPdfFileId
|
outputResult.pdfUploadStatus = eligibilityPdfFileId
|
||||||
@@ -146,7 +146,7 @@ export async function runEligibilityHistoryProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save history PDF
|
// Save history PDF
|
||||||
let historyPdfFileId: number | null = null;
|
let historyPdfFileId: number | string | null = null;
|
||||||
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
||||||
historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
||||||
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export interface EligibilityProcessorInput {
|
|||||||
export interface EligibilityProcessorResult {
|
export interface EligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runEligibilityProcessor(
|
export async function runEligibilityProcessor(
|
||||||
@@ -122,27 +122,23 @@ export async function runEligibilityProcessor(
|
|||||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
// 6) Save PDF
|
// 6) Save PDF
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
||||||
try {
|
try {
|
||||||
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
userId,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
patient.id,
|
||||||
|
patientName,
|
||||||
const created = await storage.createPdfFile(
|
"Eligibility",
|
||||||
group.id,
|
|
||||||
path.basename(seleniumResult.pdf_path),
|
path.basename(seleniumResult.pdf_path),
|
||||||
pdfBuffer
|
pdfBuffer
|
||||||
);
|
);
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
createdPdfFileId = Number(created.id);
|
outputResult.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
}
|
|
||||||
outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
outputResult.pdfUploadStatus = `PDF upload failed: ${e.message}`;
|
outputResult.pdfUploadStatus = `PDF upload failed: ${e.message}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,15 +87,22 @@ async function pollUntilDone(
|
|||||||
throw new Error(`Tufts SCO claim polling exhausted all attempts for session ${sessionId}`);
|
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 {
|
try {
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
const filename = path.basename(new URL(pdf_url).pathname);
|
||||||
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
||||||
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
||||||
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
await storage.createPdfFile(group.id!, filename, resp.data);
|
await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
|
patientId,
|
||||||
|
patientName || `Patient ${patientId}`,
|
||||||
|
"Claims",
|
||||||
|
filename,
|
||||||
|
Buffer.from(resp.data)
|
||||||
|
);
|
||||||
log("tuftssco-claim-processor", "PDF saved", { patientId, filename });
|
log("tuftssco-claim-processor", "PDF saved", { patientId, filename });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log("tuftssco-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
|
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) {
|
if (pdf_url && !socketId) {
|
||||||
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
||||||
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
|
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", {
|
emitToSocket(socketId, "selenium:tuftssco_claim_completed", {
|
||||||
|
|||||||
@@ -88,16 +88,21 @@ async function pollUntilDone(
|
|||||||
throw new Error(`Tufts SCO preauth polling exhausted all attempts for session ${sessionId}`);
|
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 {
|
try {
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
const filename = path.basename(new URL(pdf_url).pathname);
|
||||||
const resp = await axios.get(pdf_url, { responseType: "arraybuffer", timeout: 30000 });
|
const resp = await axios.get(pdf_url, { responseType: "arraybuffer", timeout: 30000 });
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) {
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH");
|
await storage.savePdfToCloudStorage(
|
||||||
}
|
userId,
|
||||||
await storage.createPdfFile(group.id!, filename, resp.data);
|
patientId,
|
||||||
|
patientName || `Patient ${patientId}`,
|
||||||
|
"PreAuth",
|
||||||
|
filename,
|
||||||
|
Buffer.from(resp.data)
|
||||||
|
);
|
||||||
log("tuftssco-preauth-processor", "PDF saved", { patientId, filename });
|
log("tuftssco-preauth-processor", "PDF saved", { patientId, filename });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log("tuftssco-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
|
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) {
|
if (pdf_url && !socketId) {
|
||||||
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
||||||
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
|
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", {
|
emitToSocket(socketId, "selenium:tuftssco_preauth_completed", {
|
||||||
|
|||||||
@@ -99,18 +99,23 @@ async function pollUntilDone(
|
|||||||
throw new Error(`UnitedDH claim polling exhausted all attempts for session ${sessionId}`);
|
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 {
|
try {
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
const filename = path.basename(new URL(pdf_url).pathname);
|
||||||
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
||||||
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
||||||
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) {
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
|
await storage.savePdfToCloudStorage(
|
||||||
}
|
userId,
|
||||||
await storage.createPdfFile(group.id!, filename, resp.data);
|
patientId,
|
||||||
|
patientName || `Patient ${patientId}`,
|
||||||
|
"Claims",
|
||||||
|
filename,
|
||||||
|
Buffer.from(resp.data)
|
||||||
|
);
|
||||||
log("uniteddh-claim-processor", "PDF saved", { patientId, filename });
|
log("uniteddh-claim-processor", "PDF saved", { patientId, filename });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log("uniteddh-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
|
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) {
|
if (pdf_url && !socketId) {
|
||||||
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
||||||
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
|
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", {
|
emitToSocket(socketId, "selenium:uniteddh_claim_completed", {
|
||||||
|
|||||||
@@ -98,18 +98,23 @@ async function pollUntilDone(
|
|||||||
throw new Error(`UnitedDH preauth polling exhausted all attempts for session ${sessionId}`);
|
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 {
|
try {
|
||||||
const filename = path.basename(new URL(pdf_url).pathname);
|
const filename = path.basename(new URL(pdf_url).pathname);
|
||||||
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
const seleniumPort = process.env.SELENIUM_PORT || "5002";
|
||||||
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
|
||||||
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM_PREAUTH");
|
const patient = await storage.getPatient(patientId);
|
||||||
if (!group) {
|
const patientName = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : "";
|
||||||
group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH");
|
await storage.savePdfToCloudStorage(
|
||||||
}
|
userId,
|
||||||
await storage.createPdfFile(group.id!, filename, resp.data);
|
patientId,
|
||||||
|
patientName || `Patient ${patientId}`,
|
||||||
|
"PreAuth",
|
||||||
|
filename,
|
||||||
|
Buffer.from(resp.data)
|
||||||
|
);
|
||||||
log("uniteddh-preauth-processor", "PDF saved", { patientId, filename });
|
log("uniteddh-preauth-processor", "PDF saved", { patientId, filename });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
log("uniteddh-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
|
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) {
|
if (pdf_url && !socketId) {
|
||||||
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
|
||||||
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
|
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", {
|
emitToSocket(socketId, "selenium:uniteddh_preauth_completed", {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export interface UnitedSCOEligibilityProcessorInput {
|
|||||||
export interface UnitedSCOEligibilityProcessorResult {
|
export interface UnitedSCOEligibilityProcessorResult {
|
||||||
patientUpdateStatus?: string;
|
patientUpdateStatus?: string;
|
||||||
pdfUploadStatus?: string;
|
pdfUploadStatus?: string;
|
||||||
pdfFileId?: number | null;
|
pdfFileId?: number | string | null;
|
||||||
pdfFilename?: string | null;
|
pdfFilename?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ async function processUnitedSCOResult(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
): Promise<UnitedSCOEligibilityProcessorResult> {
|
): Promise<UnitedSCOEligibilityProcessorResult> {
|
||||||
const output: UnitedSCOEligibilityProcessorResult = {};
|
const output: UnitedSCOEligibilityProcessorResult = {};
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Resolve patient name
|
// 1) Resolve patient name
|
||||||
@@ -155,18 +155,17 @@ async function processUnitedSCOResult(
|
|||||||
|
|
||||||
// 6) Save PDF to patient document group
|
// 6) Save PDF to patient document group
|
||||||
if (pdfBuffer && pdfFilename) {
|
if (pdfBuffer && pdfFilename) {
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
const patientName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `Patient ${patient.id}`;
|
||||||
const groupTitle = "Eligibility Status";
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
|
userId,
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(patient.id, groupTitleKey);
|
patient.id,
|
||||||
if (!group) group = await storage.createPdfGroup(patient.id, groupTitle, groupTitleKey);
|
patientName,
|
||||||
if (!group?.id) throw new Error("PDF group creation failed");
|
"Eligibility",
|
||||||
|
pdfFilename,
|
||||||
const created = await storage.createPdfFile(group.id, pdfFilename, pdfBuffer);
|
pdfBuffer
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
);
|
||||||
createdPdfFileId = Number(created.id);
|
createdPdfFileId = `cloud:${cloudFile.id}`;
|
||||||
}
|
output.pdfUploadStatus = "PDF saved to Eligibility folder";
|
||||||
output.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
|
||||||
output.pdfFilename = pdfFilename;
|
output.pdfFilename = pdfFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
|||||||
result = await runClaimStatusProcessor({
|
result = await runClaimStatusProcessor({
|
||||||
enrichedPayload,
|
enrichedPayload,
|
||||||
insuranceId: job.data.insuranceId!,
|
insuranceId: job.data.insuranceId!,
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
} else if (jobType === "claim-submit") {
|
} else if (jobType === "claim-submit") {
|
||||||
result = await runClaimSubmitProcessor({
|
result = await runClaimSubmitProcessor({
|
||||||
@@ -58,6 +59,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
|||||||
claimId: job.data.claimId,
|
claimId: job.data.claimId,
|
||||||
variant: "claimsubmit",
|
variant: "claimsubmit",
|
||||||
socketId,
|
socketId,
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
} else if (jobType === "claim-pre-auth") {
|
} else if (jobType === "claim-pre-auth") {
|
||||||
result = await runClaimSubmitProcessor({
|
result = await runClaimSubmitProcessor({
|
||||||
@@ -66,6 +68,7 @@ async function processSeleniumJob(job: Job<SeleniumJobData>) {
|
|||||||
claimId: job.data.claimId,
|
claimId: job.data.claimId,
|
||||||
variant: "claim-pre-auth",
|
variant: "claim-pre-auth",
|
||||||
socketId,
|
socketId,
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown selenium jobType: ${jobType}`);
|
throw new Error(`Unknown selenium jobType: ${jobType}`);
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ router.post(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const folder = await storage.getOrCreatePatientFolder(req.user.id, patientId, patientName);
|
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 }[] = [];
|
const result: { filename: string; mimeType: string; filePath: string }[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -126,14 +131,14 @@ router.post(
|
|||||||
file.buffer
|
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(
|
const cloudFile = await storage.initializeFileUpload(
|
||||||
req.user.id,
|
req.user.id,
|
||||||
file.originalname,
|
file.originalname,
|
||||||
file.mimetype,
|
file.mimetype,
|
||||||
BigInt(file.size),
|
BigInt(file.size),
|
||||||
1,
|
1,
|
||||||
(folder as any).id
|
(attachmentsFolder as any).id
|
||||||
);
|
);
|
||||||
await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer);
|
await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer);
|
||||||
const finalized = await storage.finalizeFileUpload((cloudFile as any).id);
|
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: "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
|
const patient = await storage.getPatient(parsedPatientId);
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(
|
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,
|
parsedPatientId,
|
||||||
groupTitleKey
|
patientName || `Patient ${parsedPatientId}`,
|
||||||
|
cloudCategory,
|
||||||
|
filename,
|
||||||
|
Buffer.from(pdfResponse.data)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!group) {
|
const pdfFileId: string = `cloud:${cloudFile.id}`;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -942,6 +939,20 @@ router.get(
|
|||||||
const safeName = (f.filename || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf";
|
const safeName = (f.filename || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf";
|
||||||
pdfEntries.push({ filename: safeName, data: buf });
|
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) {
|
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(
|
router.get(
|
||||||
"/pdf-files/group/:groupId",
|
"/pdf-files/group/:groupId",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
@@ -160,7 +188,8 @@ router.get(
|
|||||||
}
|
}
|
||||||
const groupId = parseInt(idParam);
|
const groupId = parseInt(idParam);
|
||||||
const files = await storage.getPdfFilesByGroupId(groupId);
|
const files = await storage.getPdfFilesByGroupId(groupId);
|
||||||
res.json(files);
|
const cloudFiles = await listCloudPdfsForGroup(groupId);
|
||||||
|
res.json([...cloudFiles, ...(files as any[])]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: "Internal server error" });
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
@@ -203,6 +232,8 @@ router.get(
|
|||||||
// Decide whether client asked for paginated response
|
// Decide whether client asked for paginated response
|
||||||
const wantsPagination = typeof limit === "number";
|
const wantsPagination = typeof limit === "number";
|
||||||
|
|
||||||
|
const cloudFiles = await listCloudPdfsForGroup(groupId);
|
||||||
|
|
||||||
if (wantsPagination) {
|
if (wantsPagination) {
|
||||||
// storage.getPdfFilesByGroupId with pagination should return { total, data }
|
// storage.getPdfFilesByGroupId with pagination should return { total, data }
|
||||||
const result = await storage.getPdfFilesByGroupId(groupId, {
|
const result = await storage.getPdfFilesByGroupId(groupId, {
|
||||||
@@ -214,14 +245,17 @@ router.get(
|
|||||||
// result should be { total, data }, but handle unexpected shapes defensively
|
// result should be { total, data }, but handle unexpected shapes defensively
|
||||||
if (Array.isArray(result)) {
|
if (Array.isArray(result)) {
|
||||||
// fallback: storage returned full array; compute total
|
// 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 {
|
} else {
|
||||||
// no limit requested -> return all files for the group
|
// no limit requested -> return all files for the group
|
||||||
const all = (await storage.getPdfFilesByGroupId(groupId)) as PdfFile[];
|
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) {
|
} catch (err) {
|
||||||
console.error("GET /pdf-files/group/:groupId error:", err);
|
console.error("GET /pdf-files/group/:groupId error:", err);
|
||||||
@@ -238,6 +272,31 @@ router.get(
|
|||||||
if (!idParam) {
|
if (!idParam) {
|
||||||
return res.status(400).json({ error: "Missing ID" });
|
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);
|
const id = parseInt(idParam, 10);
|
||||||
if (Number.isNaN(id)) {
|
if (Number.isNaN(id)) {
|
||||||
return res.status(400).json({ error: "Invalid ID" });
|
return res.status(400).json({ error: "Invalid ID" });
|
||||||
@@ -409,7 +468,18 @@ router.delete(
|
|||||||
if (!idParam) {
|
if (!idParam) {
|
||||||
return res.status(400).json({ error: "Missing ID" });
|
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);
|
const success = await storage.deletePdfFile(id);
|
||||||
res.json({ success });
|
res.json({ success });
|
||||||
|
|||||||
@@ -521,36 +521,18 @@ router.post(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const pdfBuf = await fs.readFile(seleniumResult.pdf_path);
|
const pdfBuf = await fs.readFile(seleniumResult.pdf_path);
|
||||||
const groupTitle = "Eligibility Status";
|
const patientName = `${updatedPatient.firstName ?? ""} ${updatedPatient.lastName ?? ""}`.trim();
|
||||||
const groupTitleKey = "ELIGIBILITY_STATUS";
|
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(
|
const cloudFile = await storage.savePdfToCloudStorage(
|
||||||
|
req.user.id,
|
||||||
updatedPatient.id,
|
updatedPatient.id,
|
||||||
groupTitleKey
|
patientName || `Patient ${updatedPatient.id}`,
|
||||||
);
|
"Eligibility",
|
||||||
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,
|
|
||||||
path.basename(seleniumResult.pdf_path),
|
path.basename(seleniumResult.pdf_path),
|
||||||
pdfBuf
|
pdfBuf
|
||||||
);
|
);
|
||||||
|
|
||||||
if (created && typeof created === "object" && "id" in created) {
|
resultItem.pdfFileId = `cloud:${cloudFile.id}`;
|
||||||
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.processed = true;
|
resultItem.processed = true;
|
||||||
} catch (pdfErr: any) {
|
} catch (pdfErr: any) {
|
||||||
|
|||||||
@@ -143,6 +143,24 @@ export interface IStorage {
|
|||||||
patientId: number,
|
patientId: number,
|
||||||
patientName: string
|
patientName: string
|
||||||
): Promise<CloudFolder>;
|
): 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 ------------------------------- */
|
/* ------------------------------- Implementation ------------------------------- */
|
||||||
@@ -546,6 +564,82 @@ export const cloudStorageStorage: IStorage = {
|
|||||||
return created as unknown as CloudFolder;
|
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 ---
|
// --- STREAM ---
|
||||||
async streamFileTo(resStream: NodeJS.WritableStream, fileId: number) {
|
async streamFileTo(resStream: NodeJS.WritableStream, fileId: number) {
|
||||||
const file = await db.cloudFile.findUnique({
|
const file = await db.cloudFile.findUnique({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Maximize2, Minimize2, Download, X } from "lucide-react";
|
|||||||
import { viewDocument } from "@/lib/api/documents";
|
import { viewDocument } from "@/lib/api/documents";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fileId: number | null;
|
fileId: number | string | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
initialFileName?: string | null;
|
initialFileName?: string | null;
|
||||||
@@ -60,8 +60,8 @@ export default function DocumentsFilePreviewModal({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
} else if (isPatientDocument && fileId) {
|
} else if (isPatientDocument && fileId) {
|
||||||
// For patient documents, use the viewDocument API to get the URL
|
// For patient documents, use the viewDocument API to get the URL (always a numeric id)
|
||||||
const documentUrl = viewDocument(fileId);
|
const documentUrl = viewDocument(Number(fileId));
|
||||||
res = await fetch(documentUrl);
|
res = await fetch(documentUrl);
|
||||||
} else {
|
} else {
|
||||||
// For PDF files, use the existing endpoint
|
// For PDF files, use the existing endpoint
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ interface BcbsMaEligibilityButtonProps {
|
|||||||
isFormIncomplete: boolean;
|
isFormIncomplete: boolean;
|
||||||
autoTrigger?: boolean;
|
autoTrigger?: boolean;
|
||||||
onAutoTriggered?: () => void;
|
onAutoTriggered?: () => void;
|
||||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BcbsMaEligibilityButton({
|
export function BcbsMaEligibilityButton({
|
||||||
@@ -204,7 +204,7 @@ export function BcbsMaEligibilityButton({
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
const pdfId = data.result?.pdfFileId;
|
const pdfId = data.result?.pdfFileId;
|
||||||
if (pdfId) {
|
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") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "BCBS MA eligibility job failed.";
|
const msg = data.error ?? "BCBS MA eligibility job failed.";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface CCAEligibilityButtonProps {
|
|||||||
isFormIncomplete: boolean;
|
isFormIncomplete: boolean;
|
||||||
autoTrigger?: boolean;
|
autoTrigger?: boolean;
|
||||||
onAutoTriggered?: () => void;
|
onAutoTriggered?: () => void;
|
||||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CCAEligibilityButton({
|
export function CCAEligibilityButton({
|
||||||
@@ -143,7 +143,7 @@ export function CCAEligibilityButton({
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
const pdfId = data.result?.pdfFileId;
|
const pdfId = data.result?.pdfFileId;
|
||||||
if (pdfId) {
|
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") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "CCA eligibility job failed.";
|
const msg = data.error ?? "CCA eligibility job failed.";
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ interface DdmaEligibilityButtonProps {
|
|||||||
isFormIncomplete: boolean;
|
isFormIncomplete: boolean;
|
||||||
autoTrigger?: boolean;
|
autoTrigger?: boolean;
|
||||||
onAutoTriggered?: () => void;
|
onAutoTriggered?: () => void;
|
||||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DdmaEligibilityButton({
|
export function DdmaEligibilityButton({
|
||||||
@@ -248,7 +248,7 @@ export function DdmaEligibilityButton({
|
|||||||
if (pdfId) {
|
if (pdfId) {
|
||||||
const filename =
|
const filename =
|
||||||
data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`;
|
data.result?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`;
|
||||||
onPdfReady(Number(pdfId), filename);
|
onPdfReady(pdfId, filename);
|
||||||
}
|
}
|
||||||
} else if (data.status === "failed") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "DDMA eligibility job failed.";
|
const msg = data.error ?? "DDMA eligibility job failed.";
|
||||||
@@ -326,7 +326,7 @@ export function DdmaEligibilityButton({
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
const pdfId = data.result?.pdfFileId;
|
const pdfId = data.result?.pdfFileId;
|
||||||
if (pdfId) {
|
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") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "DDMA eligibility job failed.";
|
const msg = data.error ?? "DDMA eligibility job failed.";
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ interface DeltaInsEligibilityButtonProps {
|
|||||||
isFormIncomplete: boolean;
|
isFormIncomplete: boolean;
|
||||||
autoTrigger?: boolean;
|
autoTrigger?: boolean;
|
||||||
onAutoTriggered?: () => void;
|
onAutoTriggered?: () => void;
|
||||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeltaInsEligibilityButton({
|
export function DeltaInsEligibilityButton({
|
||||||
@@ -246,7 +246,7 @@ export function DeltaInsEligibilityButton({
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
const pdfId = data.result?.pdfFileId;
|
const pdfId = data.result?.pdfFileId;
|
||||||
if (pdfId) {
|
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") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "Delta Ins eligibility job failed.";
|
const msg = data.error ?? "Delta Ins eligibility job failed.";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
|
||||||
export interface PdfPanelConfig {
|
export interface PdfPanelConfig {
|
||||||
pdfId?: number | null;
|
pdfId?: number | string | null;
|
||||||
fallbackFilename?: string | null;
|
fallbackFilename?: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
autoDownload?: boolean;
|
autoDownload?: boolean;
|
||||||
@@ -34,7 +34,7 @@ function parseFilename(header: string | null): string | null {
|
|||||||
return 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 [blobUrl, setBlobUrl] = useState<string | null>(null);
|
||||||
const [filename, setFilename] = useState<string | null>(null);
|
const [filename, setFilename] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Maximize2, Minimize2 } from "lucide-react";
|
|||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
pdfId?: number | null;
|
pdfId?: number | string | null;
|
||||||
fallbackFilename?: string | null;
|
fallbackFilename?: string | null;
|
||||||
autoDownload?: boolean;
|
autoDownload?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ interface TuftsSCOEligibilityButtonProps {
|
|||||||
isFormIncomplete: boolean;
|
isFormIncomplete: boolean;
|
||||||
autoTrigger?: boolean;
|
autoTrigger?: boolean;
|
||||||
onAutoTriggered?: () => void;
|
onAutoTriggered?: () => void;
|
||||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TuftsSCOEligibilityButton({
|
export function TuftsSCOEligibilityButton({
|
||||||
@@ -243,7 +243,7 @@ export function TuftsSCOEligibilityButton({
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
const pdfId = data.result?.pdfFileId;
|
const pdfId = data.result?.pdfFileId;
|
||||||
if (pdfId) {
|
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") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "Tufts SCO eligibility job failed.";
|
const msg = data.error ?? "Tufts SCO eligibility job failed.";
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ interface UnitedSCOEligibilityButtonProps {
|
|||||||
isFormIncomplete: boolean;
|
isFormIncomplete: boolean;
|
||||||
autoTrigger?: boolean;
|
autoTrigger?: boolean;
|
||||||
onAutoTriggered?: () => void;
|
onAutoTriggered?: () => void;
|
||||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
onPdfReady: (pdfId: number | string, fallbackFilename: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UnitedSCOEligibilityButton({
|
export function UnitedSCOEligibilityButton({
|
||||||
@@ -243,7 +243,7 @@ export function UnitedSCOEligibilityButton({
|
|||||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
const pdfId = data.result?.pdfFileId;
|
const pdfId = data.result?.pdfFileId;
|
||||||
if (pdfId) {
|
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") {
|
} else if (data.status === "failed") {
|
||||||
const msg = data.error ?? "United SCO eligibility job failed.";
|
const msg = data.error ?? "United SCO eligibility job failed.";
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function ClaimsPage() {
|
|||||||
>(null);
|
>(null);
|
||||||
// PDF preview modal state
|
// PDF preview modal state
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
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 [previewFallbackFilename, setPreviewFallbackFilename] = useState<string | null>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { status, message, show } = useAppSelector(
|
const { status, message, show } = useAppSelector(
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ import { Eye, Trash, Download, FolderOpen, FileText, HardDrive } from "lucide-re
|
|||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
||||||
import { PatientTable } from "@/components/patients/patient-table";
|
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 {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
@@ -50,7 +56,7 @@ export default function DocumentsPage() {
|
|||||||
const [showPatientDocuments, setShowPatientDocuments] = useState(false);
|
const [showPatientDocuments, setShowPatientDocuments] = useState(false);
|
||||||
|
|
||||||
// Document preview state
|
// Document preview state
|
||||||
const [previewDocumentId, setPreviewDocumentId] = useState<number | null>(null);
|
const [previewDocumentId, setPreviewDocumentId] = useState<number | string | null>(null);
|
||||||
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
||||||
|
|
||||||
// Delete document state
|
// Delete document state
|
||||||
@@ -245,7 +251,7 @@ export default function DocumentsPage() {
|
|||||||
|
|
||||||
// DELETE mutation
|
// DELETE mutation
|
||||||
const deletePdfMutation = useMutation({
|
const deletePdfMutation = useMutation({
|
||||||
mutationFn: async (id: number) => {
|
mutationFn: async (id: number | string) => {
|
||||||
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
|
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -267,7 +273,7 @@ export default function DocumentsPage() {
|
|||||||
|
|
||||||
const handleConfirmDeletePdf = () => {
|
const handleConfirmDeletePdf = () => {
|
||||||
if (currentPdf) {
|
if (currentPdf) {
|
||||||
deletePdfMutation.mutate(Number(currentPdf.id));
|
deletePdfMutation.mutate(currentPdf.id);
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
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);
|
setPreviewDocumentId(pdfId);
|
||||||
setIsPreviewModalOpen(true);
|
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 res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
|
||||||
const arrayBuffer = await res.arrayBuffer();
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
@@ -447,7 +453,7 @@ export default function DocumentsPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleViewPdf(
|
handleViewPdf(
|
||||||
Number(pdf.id),
|
pdf.id,
|
||||||
pdf.filename
|
pdf.filename
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -459,7 +465,7 @@ export default function DocumentsPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleDownloadPdf(
|
handleDownloadPdf(
|
||||||
Number(pdf.id),
|
pdf.id,
|
||||||
pdf.filename
|
pdf.filename
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,14 +142,14 @@ export default function InsuranceStatusPage() {
|
|||||||
|
|
||||||
// PDF preview modal state
|
// PDF preview modal state
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
|
const [previewPdfId, setPreviewPdfId] = useState<number | string | null>(null);
|
||||||
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<
|
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
// Dual PDF modal state (used by MH Eligibility & History)
|
// Dual PDF modal state (used by MH Eligibility & History)
|
||||||
const [dualPreviewOpen, setDualPreviewOpen] = useState(false);
|
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 [dualEligibilityFilename, setDualEligibilityFilename] = useState<string | null>(null);
|
||||||
const [dualMemberDetailsPdfId, setDualMemberDetailsPdfId] = useState<number | null>(null);
|
const [dualMemberDetailsPdfId, setDualMemberDetailsPdfId] = useState<number | null>(null);
|
||||||
const [dualMemberDetailsFilename, setDualMemberDetailsFilename] = useState<string | 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)
|
// CMSP PDF modal state (used by CMSP Eligibility & History & Remaining)
|
||||||
const [cmspPreviewOpen, setCmspPreviewOpen] = useState(false);
|
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 [cmspEligibilityFilename, setCmspEligibilityFilename] = useState<string | null>(null);
|
||||||
const [cmspMemberDetailsPdfId, setCmspMemberDetailsPdfId] = useState<number | null>(null);
|
const [cmspMemberDetailsPdfId, setCmspMemberDetailsPdfId] = useState<number | null>(null);
|
||||||
const [cmspMemberDetailsFilename, setCmspMemberDetailsFilename] = useState<string | null>(null);
|
const [cmspMemberDetailsFilename, setCmspMemberDetailsFilename] = useState<string | null>(null);
|
||||||
@@ -381,7 +381,7 @@ export default function InsuranceStatusPage() {
|
|||||||
void tryAppointmentFromChatbot();
|
void tryAppointmentFromChatbot();
|
||||||
|
|
||||||
if (jobResult.pdfFileId) {
|
if (jobResult.pdfFileId) {
|
||||||
setPreviewPdfId(Number(jobResult.pdfFileId));
|
setPreviewPdfId(jobResult.pdfFileId);
|
||||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ export default function InsuranceStatusPage() {
|
|||||||
setSelectedPatient(null);
|
setSelectedPatient(null);
|
||||||
|
|
||||||
if (jobResult.pdfFileId) {
|
if (jobResult.pdfFileId) {
|
||||||
setPreviewPdfId(Number(jobResult.pdfFileId));
|
setPreviewPdfId(jobResult.pdfFileId);
|
||||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
}
|
}
|
||||||
@@ -579,7 +579,7 @@ export default function InsuranceStatusPage() {
|
|||||||
|
|
||||||
// Open all PDFs side by side in the modal
|
// Open all PDFs side by side in the modal
|
||||||
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
|
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
|
||||||
setDualEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
|
setDualEligibilityPdfId(jobResult.pdfFileId ?? null);
|
||||||
setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||||
setDualMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
|
setDualMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
|
||||||
setDualMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `eligibility_member_details_${memberId}.pdf`);
|
setDualMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `eligibility_member_details_${memberId}.pdf`);
|
||||||
@@ -649,7 +649,7 @@ export default function InsuranceStatusPage() {
|
|||||||
|
|
||||||
// Open 4-panel modal
|
// Open 4-panel modal
|
||||||
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
|
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`);
|
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
|
||||||
setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
|
setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ? Number(jobResult.memberDetailsPdfFileId) : null);
|
||||||
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);
|
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);
|
||||||
|
|||||||
Reference in New Issue
Block a user