feat: MH eligibility & history, CMSP eligibility & history & remaining
- Add MH Eligibility & History button: runs full MH eligibility flow then clicks member ID → service history, prints both PDFs via CDP, opens dual side-by-side PDF modal (eligibility auto-downloads, history does not) - Add CMSP Eligibility & History & Remaining button: same flow plus navigates back to member details, clicks View Accumulator, prints accumulator PDF via CDP; opens 3-panel side-by-side PDF modal - Generalize DualPdfPreviewModal to accept panels[] array (works for 2 or 3 PDFs) - Auto-download eligibility PDF via direct API URL to avoid Chrome Safe Browsing pause on blob: URL downloads - New backend processors, job types, and routes for both flows - New Python Selenium workers with stable CSS selectors (ng-bind, href*) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ import { runDeltaInsEligibilityProcessor } from "./processors/deltaInsEligibilit
|
|||||||
import { runUnitedSCOEligibilityProcessor } from "./processors/unitedSCOEligibilityProcessor";
|
import { runUnitedSCOEligibilityProcessor } from "./processors/unitedSCOEligibilityProcessor";
|
||||||
import { runDentaQuestEligibilityProcessor } from "./processors/dentaQuestEligibilityProcessor";
|
import { runDentaQuestEligibilityProcessor } from "./processors/dentaQuestEligibilityProcessor";
|
||||||
import { runCCAEligibilityProcessor } from "./processors/ccaEligibilityProcessor";
|
import { runCCAEligibilityProcessor } from "./processors/ccaEligibilityProcessor";
|
||||||
|
import { runEligibilityHistoryProcessor } from "./processors/eligibilityHistoryProcessor";
|
||||||
|
import { runCmspEligibilityHistoryRemainingProcessor } from "./processors/cmspEligibilityHistoryRemainingProcessor";
|
||||||
import type { SeleniumJobData, OcrJobData } from "./queues";
|
import type { SeleniumJobData, OcrJobData } from "./queues";
|
||||||
|
|
||||||
// ── Queue instances ──────────────────────────────────────────────────────────
|
// ── Queue instances ──────────────────────────────────────────────────────────
|
||||||
@@ -144,6 +146,26 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
|||||||
job.id
|
job.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (jobType === "cmsp-eligibility-history-remaining-check") {
|
||||||
|
return runCmspEligibilityHistoryRemainingProcessor({
|
||||||
|
enrichedPayload: data.enrichedPayload,
|
||||||
|
userId: data.userId,
|
||||||
|
insuranceId: data.insuranceId!,
|
||||||
|
formFirstName: data.formFirstName,
|
||||||
|
formLastName: data.formLastName,
|
||||||
|
formDob: data.formDob,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (jobType === "mh-eligibility-history-check") {
|
||||||
|
return runEligibilityHistoryProcessor({
|
||||||
|
enrichedPayload: data.enrichedPayload,
|
||||||
|
userId: data.userId,
|
||||||
|
insuranceId: data.insuranceId!,
|
||||||
|
formFirstName: data.formFirstName,
|
||||||
|
formLastName: data.formLastName,
|
||||||
|
formDob: data.formDob,
|
||||||
|
});
|
||||||
|
}
|
||||||
throw new Error(`Unknown selenium jobType: ${jobType}`);
|
throw new Error(`Unknown selenium jobType: ${jobType}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Processor for "cmsp-eligibility-history-remaining-check" jobs.
|
||||||
|
* Saves eligibility PDF, service history PDF, and accumulator PDF
|
||||||
|
* to the patient's ELIGIBILITY_STATUS document group.
|
||||||
|
*/
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { storage } from "../../storage";
|
||||||
|
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
|
||||||
|
import forwardToPatientDataExtractorService from "../../services/patientDataExtractorService";
|
||||||
|
import {
|
||||||
|
callPythonSync,
|
||||||
|
splitName,
|
||||||
|
createOrUpdatePatientByInsuranceId,
|
||||||
|
} from "./_shared";
|
||||||
|
|
||||||
|
export interface CmspEligibilityHistoryRemainingProcessorInput {
|
||||||
|
enrichedPayload: any;
|
||||||
|
userId: number;
|
||||||
|
insuranceId: string;
|
||||||
|
formFirstName?: string;
|
||||||
|
formLastName?: string;
|
||||||
|
formDob?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CmspEligibilityHistoryRemainingProcessorResult {
|
||||||
|
patientUpdateStatus?: string;
|
||||||
|
pdfUploadStatus?: string;
|
||||||
|
pdfFileId?: number | null;
|
||||||
|
pdfFilename?: string | null;
|
||||||
|
historyPdfFileId?: number | null;
|
||||||
|
historyPdfFilename?: string | null;
|
||||||
|
accumulatorPdfFileId?: number | null;
|
||||||
|
accumulatorPdfFilename?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCmspEligibilityHistoryRemainingProcessor(
|
||||||
|
input: CmspEligibilityHistoryRemainingProcessorInput
|
||||||
|
): Promise<CmspEligibilityHistoryRemainingProcessorResult> {
|
||||||
|
const { enrichedPayload, userId, insuranceId, formFirstName, formLastName, formDob } = input;
|
||||||
|
|
||||||
|
const seleniumResult = await callPythonSync("/cmsp-eligibility-history-remaining-check", {
|
||||||
|
data: enrichedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputResult: CmspEligibilityHistoryRemainingProcessorResult = {};
|
||||||
|
|
||||||
|
// Extract patient name
|
||||||
|
const seleniumFirst = seleniumResult.firstName?.trim() || null;
|
||||||
|
const seleniumLast = seleniumResult.lastName?.trim() || null;
|
||||||
|
const seleniumName = seleniumResult.name?.trim() || null;
|
||||||
|
const seleniumInsurance = seleniumResult.insurance?.trim() || null;
|
||||||
|
|
||||||
|
const extracted: { firstName?: string | null; lastName?: string | null } = {};
|
||||||
|
|
||||||
|
if (seleniumFirst || seleniumLast) {
|
||||||
|
extracted.firstName = seleniumFirst;
|
||||||
|
extracted.lastName = seleniumLast;
|
||||||
|
} else if (seleniumName) {
|
||||||
|
const parts = splitName(seleniumName);
|
||||||
|
extracted.firstName = parts.firstName || null;
|
||||||
|
extracted.lastName = parts.lastName || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extracted.firstName && !extracted.lastName && seleniumResult?.pdf_path?.endsWith(".pdf")) {
|
||||||
|
try {
|
||||||
|
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
||||||
|
const extraction = await forwardToPatientDataExtractorService({
|
||||||
|
buffer: pdfBuffer,
|
||||||
|
originalname: path.basename(seleniumResult.pdf_path),
|
||||||
|
mimetype: "application/pdf",
|
||||||
|
} as any);
|
||||||
|
if (extraction.name) {
|
||||||
|
const parts = splitName(extraction.name);
|
||||||
|
extracted.firstName = parts.firstName || null;
|
||||||
|
extracted.lastName = parts.lastName || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[cmspProcessor] PDF name extraction failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferFirst = extracted.firstName || formFirstName || null;
|
||||||
|
const preferLast = extracted.lastName || formLastName || null;
|
||||||
|
|
||||||
|
let patient;
|
||||||
|
try {
|
||||||
|
patient = await createOrUpdatePatientByInsuranceId({
|
||||||
|
insuranceId,
|
||||||
|
firstName: preferFirst,
|
||||||
|
lastName: preferLast,
|
||||||
|
dob: formDob,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Failed to create/update patient: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patient && patient.id !== undefined) {
|
||||||
|
let newStatus = "UNKNOWN";
|
||||||
|
if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE";
|
||||||
|
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE";
|
||||||
|
|
||||||
|
const updates: any = { status: newStatus };
|
||||||
|
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
|
||||||
|
await storage.updatePatient(patient.id, updates);
|
||||||
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
|
// Helper: save a single PDF to the patient's ELIGIBILITY_STATUS group
|
||||||
|
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
|
||||||
|
try {
|
||||||
|
const buffer = await fs.readFile(pdfPath);
|
||||||
|
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||||
|
const groupTitle = "Eligibility Status";
|
||||||
|
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
|
||||||
|
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
|
||||||
|
if (!group?.id) throw new Error("PDF group creation failed");
|
||||||
|
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
return Number(created.id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("[cmspProcessor] saveToGroup failed:", e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save eligibility PDF
|
||||||
|
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
||||||
|
outputResult.pdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
||||||
|
outputResult.pdfFilename = path.basename(seleniumResult.pdf_path);
|
||||||
|
outputResult.pdfUploadStatus = outputResult.pdfFileId ? "Eligibility PDF saved" : "Eligibility PDF upload failed";
|
||||||
|
} else {
|
||||||
|
outputResult.pdfUploadStatus = "No eligibility PDF returned by Selenium";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save history PDF
|
||||||
|
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
||||||
|
outputResult.historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
||||||
|
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save accumulator PDF
|
||||||
|
if (seleniumResult.accumulator_pdf_path?.endsWith(".pdf")) {
|
||||||
|
outputResult.accumulatorPdfFileId = await saveToGroup(seleniumResult.accumulator_pdf_path);
|
||||||
|
outputResult.accumulatorPdfFilename = path.basename(seleniumResult.accumulator_pdf_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputResult.patientUpdateStatus = "Patient not found or missing ID; no update performed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup temp files
|
||||||
|
try {
|
||||||
|
if (seleniumResult.pdf_path) await emptyFolderContainingFile(seleniumResult.pdf_path);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[cmspProcessor] cleanup failed:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputResult;
|
||||||
|
}
|
||||||
159
apps/Backend/src/queue/processors/eligibilityHistoryProcessor.ts
Normal file
159
apps/Backend/src/queue/processors/eligibilityHistoryProcessor.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* Processor for "mh-eligibility-history-check" jobs.
|
||||||
|
* Runs MH eligibility + service history selenium flow and saves both PDFs
|
||||||
|
* to the patient's ELIGIBILITY_STATUS document group.
|
||||||
|
*/
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { storage } from "../../storage";
|
||||||
|
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
|
||||||
|
import forwardToPatientDataExtractorService from "../../services/patientDataExtractorService";
|
||||||
|
import {
|
||||||
|
callPythonSync,
|
||||||
|
splitName,
|
||||||
|
createOrUpdatePatientByInsuranceId,
|
||||||
|
} from "./_shared";
|
||||||
|
|
||||||
|
export interface EligibilityHistoryProcessorInput {
|
||||||
|
enrichedPayload: any;
|
||||||
|
userId: number;
|
||||||
|
insuranceId: string;
|
||||||
|
formFirstName?: string;
|
||||||
|
formLastName?: string;
|
||||||
|
formDob?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EligibilityHistoryProcessorResult {
|
||||||
|
patientUpdateStatus?: string;
|
||||||
|
pdfUploadStatus?: string;
|
||||||
|
pdfFileId?: number | null;
|
||||||
|
historyPdfFileId?: number | null;
|
||||||
|
pdfFilename?: string | null;
|
||||||
|
historyPdfFilename?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runEligibilityHistoryProcessor(
|
||||||
|
input: EligibilityHistoryProcessorInput
|
||||||
|
): Promise<EligibilityHistoryProcessorResult> {
|
||||||
|
const { enrichedPayload, userId, insuranceId, formFirstName, formLastName, formDob } = input;
|
||||||
|
|
||||||
|
const seleniumResult = await callPythonSync("/mh-eligibility-history-check", {
|
||||||
|
data: enrichedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputResult: EligibilityHistoryProcessorResult = {};
|
||||||
|
|
||||||
|
// Extract patient name
|
||||||
|
const seleniumFirst = seleniumResult.firstName?.trim() || null;
|
||||||
|
const seleniumLast = seleniumResult.lastName?.trim() || null;
|
||||||
|
const seleniumName = seleniumResult.name?.trim() || null;
|
||||||
|
const seleniumInsurance = seleniumResult.insurance?.trim() || null;
|
||||||
|
|
||||||
|
const extracted: { firstName?: string | null; lastName?: string | null } = {};
|
||||||
|
|
||||||
|
if (seleniumFirst || seleniumLast) {
|
||||||
|
extracted.firstName = seleniumFirst;
|
||||||
|
extracted.lastName = seleniumLast;
|
||||||
|
} else if (seleniumName) {
|
||||||
|
const parts = splitName(seleniumName);
|
||||||
|
extracted.firstName = parts.firstName || null;
|
||||||
|
extracted.lastName = parts.lastName || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extracted.firstName && !extracted.lastName && seleniumResult?.pdf_path?.endsWith(".pdf")) {
|
||||||
|
try {
|
||||||
|
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
||||||
|
const extraction = await forwardToPatientDataExtractorService({
|
||||||
|
buffer: pdfBuffer,
|
||||||
|
originalname: path.basename(seleniumResult.pdf_path),
|
||||||
|
mimetype: "application/pdf",
|
||||||
|
} as any);
|
||||||
|
if (extraction.name) {
|
||||||
|
const parts = splitName(extraction.name);
|
||||||
|
extracted.firstName = parts.firstName || null;
|
||||||
|
extracted.lastName = parts.lastName || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[eligibilityHistoryProcessor] PDF name extraction failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferFirst = extracted.firstName || formFirstName || null;
|
||||||
|
const preferLast = extracted.lastName || formLastName || null;
|
||||||
|
|
||||||
|
let patient;
|
||||||
|
try {
|
||||||
|
patient = await createOrUpdatePatientByInsuranceId({
|
||||||
|
insuranceId,
|
||||||
|
firstName: preferFirst,
|
||||||
|
lastName: preferLast,
|
||||||
|
dob: formDob,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Failed to create/update patient: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patient && patient.id !== undefined) {
|
||||||
|
let newStatus = "UNKNOWN";
|
||||||
|
if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE";
|
||||||
|
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE";
|
||||||
|
|
||||||
|
const updates: any = { status: newStatus };
|
||||||
|
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
|
||||||
|
await storage.updatePatient(patient.id, updates);
|
||||||
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
|
// Helper: save a PDF buffer to the patient's ELIGIBILITY_STATUS group
|
||||||
|
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
|
||||||
|
try {
|
||||||
|
const buffer = await fs.readFile(pdfPath);
|
||||||
|
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||||
|
const groupTitle = "Eligibility Status";
|
||||||
|
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
|
||||||
|
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
|
||||||
|
if (!group?.id) throw new Error("PDF group creation failed");
|
||||||
|
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
return Number(created.id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save eligibility PDF
|
||||||
|
let eligibilityPdfFileId: number | null = null;
|
||||||
|
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
||||||
|
eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
||||||
|
outputResult.pdfUploadStatus = eligibilityPdfFileId
|
||||||
|
? "Eligibility PDF saved"
|
||||||
|
: "Eligibility PDF upload failed";
|
||||||
|
outputResult.pdfFilename = path.basename(seleniumResult.pdf_path);
|
||||||
|
} else {
|
||||||
|
outputResult.pdfUploadStatus = "No eligibility PDF returned by Selenium";
|
||||||
|
}
|
||||||
|
outputResult.pdfFileId = eligibilityPdfFileId;
|
||||||
|
|
||||||
|
// Save history PDF
|
||||||
|
let historyPdfFileId: number | null = null;
|
||||||
|
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
||||||
|
historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
||||||
|
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
||||||
|
}
|
||||||
|
outputResult.historyPdfFileId = historyPdfFileId;
|
||||||
|
} else {
|
||||||
|
outputResult.patientUpdateStatus = "Patient not found or missing ID; no update performed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup temp files
|
||||||
|
try {
|
||||||
|
if (seleniumResult.pdf_path) await emptyFolderContainingFile(seleniumResult.pdf_path);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[eligibilityHistoryProcessor] cleanup failed:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputResult;
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@ export type SeleniumJobType =
|
|||||||
| "ddma-eligibility-check"
|
| "ddma-eligibility-check"
|
||||||
| "deltains-eligibility-check"
|
| "deltains-eligibility-check"
|
||||||
| "unitedsco-eligibility-check"
|
| "unitedsco-eligibility-check"
|
||||||
| "cca-eligibility-check";
|
| "cca-eligibility-check"
|
||||||
|
| "mh-eligibility-history-check"
|
||||||
|
| "cmsp-eligibility-history-remaining-check";
|
||||||
|
|
||||||
export interface SeleniumJobData {
|
export interface SeleniumJobData {
|
||||||
jobType: SeleniumJobType;
|
jobType: SeleniumJobType;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "../../../../packages/db/types/patient-types";
|
} from "../../../../packages/db/types/patient-types";
|
||||||
import { formatDobForAgent } from "../utils/dateUtils";
|
import { formatDobForAgent } from "../utils/dateUtils";
|
||||||
import { seleniumQueue } from "../queue/queues";
|
import { seleniumQueue } from "../queue/queues";
|
||||||
|
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -166,6 +167,98 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/eligibility-history-check",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
if (!req.body.data) {
|
||||||
|
return res.status(400).json({ error: "Missing Insurance Eligibility data for selenium" });
|
||||||
|
}
|
||||||
|
if (!req.user || !req.user.id) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data =
|
||||||
|
typeof req.body.data === "string" ? JSON.parse(req.body.data) : req.body.data;
|
||||||
|
|
||||||
|
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, "MH");
|
||||||
|
if (!credentials) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "No MassHealth credentials found. Please update them in Settings.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const insuranceId = String(data.memberId ?? "").trim();
|
||||||
|
if (!insuranceId) {
|
||||||
|
return res.status(400).json({ error: "Missing memberId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedData = {
|
||||||
|
...data,
|
||||||
|
massdhpUsername: credentials.username,
|
||||||
|
massdhpPassword: credentials.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobId = enqueueSeleniumJob({
|
||||||
|
jobType: "mh-eligibility-history-check",
|
||||||
|
userId: req.user.id,
|
||||||
|
socketId: req.body.socketId,
|
||||||
|
enrichedPayload: enrichedData,
|
||||||
|
insuranceId,
|
||||||
|
formFirstName: data.firstName,
|
||||||
|
formLastName: data.lastName,
|
||||||
|
formDob: data.dateOfBirth,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ jobId, status: "queued" });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/cmsp-eligibility-history-remaining-check",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
if (!req.body.data) {
|
||||||
|
return res.status(400).json({ error: "Missing data for selenium" });
|
||||||
|
}
|
||||||
|
if (!req.user || !req.user.id) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data =
|
||||||
|
typeof req.body.data === "string" ? JSON.parse(req.body.data) : req.body.data;
|
||||||
|
|
||||||
|
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, "MH");
|
||||||
|
if (!credentials) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "No MassHealth credentials found. Please update them in Settings.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const insuranceId = String(data.memberId ?? "").trim();
|
||||||
|
if (!insuranceId) {
|
||||||
|
return res.status(400).json({ error: "Missing memberId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedData = {
|
||||||
|
...data,
|
||||||
|
massdhpUsername: credentials.username,
|
||||||
|
massdhpPassword: credentials.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobId = enqueueSeleniumJob({
|
||||||
|
jobType: "cmsp-eligibility-history-remaining-check",
|
||||||
|
userId: req.user.id,
|
||||||
|
socketId: req.body.socketId,
|
||||||
|
enrichedPayload: enrichedData,
|
||||||
|
insuranceId,
|
||||||
|
formFirstName: data.firstName,
|
||||||
|
formLastName: data.lastName,
|
||||||
|
formDob: data.dateOfBirth,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ jobId, status: "queued" });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/claim-status-check",
|
"/claim-status-check",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
|
||||||
|
export interface PdfPanelConfig {
|
||||||
|
pdfId?: number | null;
|
||||||
|
fallbackFilename?: string | null;
|
||||||
|
label: string;
|
||||||
|
autoDownload?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
panels: PdfPanelConfig[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFilename(header: string | null): string | null {
|
||||||
|
if (!header) return null;
|
||||||
|
const starMatch = header.match(/filename\*\s*=\s*([^;]+)/i);
|
||||||
|
if (starMatch?.[1]) {
|
||||||
|
const raw = starMatch[1].trim().replace(/^"(.*)"$/, "$1");
|
||||||
|
const parts = raw.split("''");
|
||||||
|
if (parts.length === 2 && parts[1]) {
|
||||||
|
try { return decodeURIComponent(parts[1]); } catch { return parts[1]; }
|
||||||
|
}
|
||||||
|
try { return decodeURIComponent(raw); } catch { return raw; }
|
||||||
|
}
|
||||||
|
const quoted = header.match(/filename\s*=\s*"([^"]+)"/i);
|
||||||
|
if (quoted?.[1]) return quoted[1].trim();
|
||||||
|
const plain = header.match(/filename\s*=\s*([^;]+)/i);
|
||||||
|
if (plain?.[1]) return plain[1].trim().replace(/^"(.*)"$/, "$1");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePdfBlob(open: boolean, pdfId?: number | null, fallbackFilename?: string | null) {
|
||||||
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
||||||
|
const [filename, setFilename] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !pdfId) return;
|
||||||
|
|
||||||
|
let objectUrl: string | null = null;
|
||||||
|
let aborted = false;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
|
||||||
|
if (!res?.ok) {
|
||||||
|
const txt = await res?.text().catch(() => "");
|
||||||
|
throw new Error(txt || `Failed to fetch PDF: ${res?.status}`);
|
||||||
|
}
|
||||||
|
const header = res.headers?.get?.("content-disposition") ?? null;
|
||||||
|
const finalName = parseFilename(header) ?? fallbackFilename ?? `file_${pdfId}.pdf`;
|
||||||
|
setFilename(finalName);
|
||||||
|
|
||||||
|
const buf = await res.arrayBuffer();
|
||||||
|
if (aborted) return;
|
||||||
|
|
||||||
|
const blob = new Blob([buf], { type: "application/pdf" });
|
||||||
|
objectUrl = URL.createObjectURL(blob);
|
||||||
|
setBlobUrl(objectUrl);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name === "AbortError") return;
|
||||||
|
setError(e?.message ?? "Failed to fetch PDF");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
aborted = true;
|
||||||
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||||
|
setBlobUrl(null);
|
||||||
|
setError(null);
|
||||||
|
setLoading(false);
|
||||||
|
setFilename(null);
|
||||||
|
};
|
||||||
|
}, [open, pdfId, fallbackFilename]);
|
||||||
|
|
||||||
|
return { blobUrl, filename, loading, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
function PdfPanel({ config, open }: { config: PdfPanelConfig; open: boolean }) {
|
||||||
|
const { blobUrl, filename, loading, error } = usePdfBlob(
|
||||||
|
open,
|
||||||
|
config.pdfId,
|
||||||
|
config.fallbackFilename
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-download via direct API URL to avoid Chrome Safe Browsing pause
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config.autoDownload || !config.pdfId || !filename) return;
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = `/api/documents/pdf-files/${config.pdfId}`;
|
||||||
|
a.download = filename;
|
||||||
|
a.rel = "noopener";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}, [config.autoDownload, config.pdfId, filename]);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!config.pdfId || !filename) return;
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = `/api/documents/pdf-files/${config.pdfId}`;
|
||||||
|
a.download = filename;
|
||||||
|
a.rel = "noopener";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 min-w-0 border-r last:border-r-0">
|
||||||
|
<div className="flex items-center justify-between px-3 py-2 border-b bg-gray-50 shrink-0">
|
||||||
|
<div className="flex flex-col min-w-0">
|
||||||
|
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium truncate" title={filename ?? undefined}>
|
||||||
|
{filename ?? "Loading…"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={!config.pdfId || !filename}
|
||||||
|
className="shrink-0 ml-2"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden p-2">
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
|
||||||
|
Loading PDF…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-destructive">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!config.pdfId && !loading && (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
|
||||||
|
No PDF available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{blobUrl && (
|
||||||
|
<iframe
|
||||||
|
title={config.label}
|
||||||
|
src={blobUrl}
|
||||||
|
className="w-full h-full border rounded"
|
||||||
|
style={{ minHeight: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DualPdfPreviewModal({ open, onClose, panels, title }: Props) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-lg shadow-lg flex flex-col"
|
||||||
|
style={{ width: "92vw", height: "88vh", maxWidth: 1600 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b shrink-0">
|
||||||
|
<h3 className="text-base font-semibold">
|
||||||
|
{title ?? "PDF Preview"}
|
||||||
|
</h3>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 min-h-0 divide-x">
|
||||||
|
{panels.map((panel, i) => (
|
||||||
|
<PdfPanel key={i} config={panel} open={open} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ interface Props {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
pdfId?: number | null;
|
pdfId?: number | null;
|
||||||
fallbackFilename?: string | null;
|
fallbackFilename?: string | null;
|
||||||
|
autoDownload?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFilenameFromContentDisposition(header: string | null): string | null {
|
function parseFilenameFromContentDisposition(header: string | null): string | null {
|
||||||
@@ -50,6 +51,7 @@ export function PdfPreviewModal({
|
|||||||
onClose,
|
onClose,
|
||||||
pdfId,
|
pdfId,
|
||||||
fallbackFilename = null,
|
fallbackFilename = null,
|
||||||
|
autoDownload = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -99,6 +101,19 @@ export function PdfPreviewModal({
|
|||||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
objectUrl = URL.createObjectURL(blob);
|
objectUrl = URL.createObjectURL(blob);
|
||||||
setFileBlobUrl(objectUrl);
|
setFileBlobUrl(objectUrl);
|
||||||
|
|
||||||
|
if (autoDownload) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
// Use the direct API URL so Chrome sees a proper HTTP response with
|
||||||
|
// Content-Disposition: attachment headers, which bypasses the Safe
|
||||||
|
// Browsing pause that blob: URL downloads trigger on Linux/Chrome.
|
||||||
|
a.href = `/api/documents/pdf-files/${pdfId}`;
|
||||||
|
a.download = finalName;
|
||||||
|
a.rel = "noopener";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err && (err.name === "AbortError" || err.message === "The user aborted a request.")) {
|
if (err && (err.name === "AbortError" || err.message === "The user aborted a request.")) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { InsertPatient, Patient } from "@repo/db/types";
|
|||||||
import { DateInput } from "@/components/ui/dateInput";
|
import { DateInput } from "@/components/ui/dateInput";
|
||||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||||
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||||
|
import { DualPdfPreviewModal } from "@/components/insurance-status/dual-pdf-preview-modal";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
||||||
import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltains-button-modal";
|
import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltains-button-modal";
|
||||||
@@ -63,6 +64,9 @@ export default function InsuranceStatusPage() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isCheckingEligibilityClaimsPreAuth, setIsCheckingEligibilityClaimsPreAuth] =
|
const [isCheckingEligibilityClaimsPreAuth, setIsCheckingEligibilityClaimsPreAuth] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isCheckingEligibilityHistory, setIsCheckingEligibilityHistory] =
|
||||||
|
useState(false);
|
||||||
|
const [isCheckingCMSP, setIsCheckingCMSP] = useState(false);
|
||||||
|
|
||||||
// AI Call Insurance section
|
// AI Call Insurance section
|
||||||
const [aiCallOpen, setAiCallOpen] = useState(false);
|
const [aiCallOpen, setAiCallOpen] = useState(false);
|
||||||
@@ -90,6 +94,22 @@ export default function InsuranceStatusPage() {
|
|||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
// Dual PDF modal state (used by MH Eligibility & History)
|
||||||
|
const [dualPreviewOpen, setDualPreviewOpen] = useState(false);
|
||||||
|
const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState<number | null>(null);
|
||||||
|
const [dualEligibilityFilename, setDualEligibilityFilename] = useState<string | null>(null);
|
||||||
|
const [dualHistoryPdfId, setDualHistoryPdfId] = useState<number | null>(null);
|
||||||
|
const [dualHistoryFilename, setDualHistoryFilename] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Triple PDF modal state (used by CMSP Eligibility & History & Remaining)
|
||||||
|
const [cmspPreviewOpen, setCmspPreviewOpen] = useState(false);
|
||||||
|
const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState<number | null>(null);
|
||||||
|
const [cmspEligibilityFilename, setCmspEligibilityFilename] = useState<string | null>(null);
|
||||||
|
const [cmspHistoryPdfId, setCmspHistoryPdfId] = useState<number | null>(null);
|
||||||
|
const [cmspHistoryFilename, setCmspHistoryFilename] = useState<string | null>(null);
|
||||||
|
const [cmspAccumulatorPdfId, setCmspAccumulatorPdfId] = useState<number | null>(null);
|
||||||
|
const [cmspAccumulatorFilename, setCmspAccumulatorFilename] = useState<string | null>(null);
|
||||||
|
|
||||||
// Populate fields from selected patient
|
// Populate fields from selected patient
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPatient) {
|
if (selectedPatient) {
|
||||||
@@ -386,6 +406,166 @@ export default function InsuranceStatusPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MH Eligibility & History — eligibility check + service history, saves both PDFs, auto-downloads eligibility PDF only
|
||||||
|
const handleMHEligibilityHistoryButton = async () => {
|
||||||
|
if (!memberId || !dateOfBirth) {
|
||||||
|
toast({
|
||||||
|
title: "Missing Fields",
|
||||||
|
description: "Please fill in all the required fields: Member ID, Date of Birth.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckingEligibilityHistory(true);
|
||||||
|
try {
|
||||||
|
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||||
|
const data = {
|
||||||
|
memberId,
|
||||||
|
dateOfBirth: formattedDob,
|
||||||
|
insuranceSiteKey: "MH",
|
||||||
|
firstName: firstName || undefined,
|
||||||
|
lastName: lastName || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Sending Data to Selenium..." }));
|
||||||
|
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/insurance-status/eligibility-history-check",
|
||||||
|
{ data, socketId: socket.id },
|
||||||
|
);
|
||||||
|
const enqueueResult = await response.json();
|
||||||
|
if (enqueueResult.error) throw new Error(enqueueResult.error);
|
||||||
|
|
||||||
|
const jobId = enqueueResult.jobId;
|
||||||
|
if (!jobId) throw new Error("No jobId returned from server");
|
||||||
|
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Selenium browser starting..." }));
|
||||||
|
|
||||||
|
const jobResult = await new Promise<any>((resolve, reject) => {
|
||||||
|
const handler = (payload: any) => {
|
||||||
|
if (String(payload.jobId) !== String(jobId)) return;
|
||||||
|
if (payload.status === "active") {
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: payload.message ?? "Selenium running..." }));
|
||||||
|
} else if (payload.status === "completed") {
|
||||||
|
socket.off("job:update", handler);
|
||||||
|
resolve(payload.result ?? {});
|
||||||
|
} else if (payload.status === "failed") {
|
||||||
|
socket.off("job:update", handler);
|
||||||
|
reject(new Error(payload.error ?? "Selenium job failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.on("job:update", handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "Eligibility and service history PDFs saved to Documents." }));
|
||||||
|
toast({
|
||||||
|
title: "Done",
|
||||||
|
description: "Eligibility and service history PDFs saved. Eligibility PDF downloading now.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedPatient(null);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
|
|
||||||
|
// Open both PDFs side by side in the dual modal
|
||||||
|
if (jobResult.pdfFileId || jobResult.historyPdfFileId) {
|
||||||
|
setDualEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
|
||||||
|
setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
|
||||||
|
setDualHistoryPdfId(jobResult.historyPdfFileId ? Number(jobResult.historyPdfFileId) : null);
|
||||||
|
setDualHistoryFilename(jobResult.historyPdfFilename ?? `eligibility_history_${memberId}.pdf`);
|
||||||
|
setDualPreviewOpen(true);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||||
|
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||||
|
} finally {
|
||||||
|
setIsCheckingEligibilityHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// CMSP Eligibility & History & Remaining — eligibility + service history + accumulator PDFs
|
||||||
|
const handleCMSPButton = async () => {
|
||||||
|
if (!memberId || !dateOfBirth) {
|
||||||
|
toast({
|
||||||
|
title: "Missing Fields",
|
||||||
|
description: "Please fill in Member ID and Date of Birth.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckingCMSP(true);
|
||||||
|
try {
|
||||||
|
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||||
|
const data = {
|
||||||
|
memberId,
|
||||||
|
dateOfBirth: formattedDob,
|
||||||
|
insuranceSiteKey: "MH",
|
||||||
|
firstName: firstName || undefined,
|
||||||
|
lastName: lastName || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Sending Data to Selenium..." }));
|
||||||
|
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/insurance-status/cmsp-eligibility-history-remaining-check",
|
||||||
|
{ data, socketId: socket.id },
|
||||||
|
);
|
||||||
|
const enqueueResult = await response.json();
|
||||||
|
if (enqueueResult.error) throw new Error(enqueueResult.error);
|
||||||
|
|
||||||
|
const jobId = enqueueResult.jobId;
|
||||||
|
if (!jobId) throw new Error("No jobId returned from server");
|
||||||
|
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Selenium browser starting..." }));
|
||||||
|
|
||||||
|
const jobResult = await new Promise<any>((resolve, reject) => {
|
||||||
|
const handler = (payload: any) => {
|
||||||
|
if (String(payload.jobId) !== String(jobId)) return;
|
||||||
|
if (payload.status === "active") {
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: payload.message ?? "Selenium running..." }));
|
||||||
|
} else if (payload.status === "completed") {
|
||||||
|
socket.off("job:update", handler);
|
||||||
|
resolve(payload.result ?? {});
|
||||||
|
} else if (payload.status === "failed") {
|
||||||
|
socket.off("job:update", handler);
|
||||||
|
reject(new Error(payload.error ?? "Selenium job failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.on("job:update", handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "CMSP PDFs saved to Documents." }));
|
||||||
|
toast({
|
||||||
|
title: "Done",
|
||||||
|
description: "Eligibility, history, and accumulator PDFs saved.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedPatient(null);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
|
|
||||||
|
// Open 3-panel modal
|
||||||
|
if (jobResult.pdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
|
||||||
|
setCmspEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
|
||||||
|
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
|
||||||
|
setCmspHistoryPdfId(jobResult.historyPdfFileId ? Number(jobResult.historyPdfFileId) : null);
|
||||||
|
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `cmsp_history_${memberId}.pdf`);
|
||||||
|
setCmspAccumulatorPdfId(jobResult.accumulatorPdfFileId ? Number(jobResult.accumulatorPdfFileId) : null);
|
||||||
|
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `cmsp_accumulator_${memberId}.pdf`);
|
||||||
|
setCmspPreviewOpen(true);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
|
||||||
|
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
|
||||||
|
} finally {
|
||||||
|
setIsCheckingCMSP(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// small helper: remove given query params from the current URL (silent, no reload)
|
// small helper: remove given query params from the current URL (silent, no reload)
|
||||||
const clearUrlParams = (params: string[]) => {
|
const clearUrlParams = (params: string[]) => {
|
||||||
try {
|
try {
|
||||||
@@ -620,6 +800,44 @@ export default function InsuranceStatusPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col-2 gap-4 mt-4">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={isCheckingEligibilityHistory}
|
||||||
|
onClick={() => handleMHEligibilityHistoryButton()}
|
||||||
|
>
|
||||||
|
{isCheckingEligibilityHistory ? (
|
||||||
|
<>
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
|
MH Eligibility & History
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={isCheckingCMSP}
|
||||||
|
onClick={() => handleCMSPButton()}
|
||||||
|
>
|
||||||
|
{isCheckingCMSP ? (
|
||||||
|
<>
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
|
CMSP Eligibility & History & Remaining
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* TEMP PROVIDER BUTTONS */}
|
{/* TEMP PROVIDER BUTTONS */}
|
||||||
<div className="space-y-4 mt-6">
|
<div className="space-y-4 mt-6">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">
|
<h3 className="text-sm font-medium text-muted-foreground">
|
||||||
@@ -937,7 +1155,67 @@ export default function InsuranceStatusPage() {
|
|||||||
setPreviewFallbackFilename(null);
|
setPreviewFallbackFilename(null);
|
||||||
}}
|
}}
|
||||||
pdfId={previewPdfId ?? undefined}
|
pdfId={previewPdfId ?? undefined}
|
||||||
fallbackFilename={previewFallbackFilename ?? undefined} // optional
|
fallbackFilename={previewFallbackFilename ?? undefined}
|
||||||
|
autoDownload
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Triple PDF modal for CMSP — eligibility, history, accumulator side by side */}
|
||||||
|
<DualPdfPreviewModal
|
||||||
|
open={cmspPreviewOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setCmspPreviewOpen(false);
|
||||||
|
setCmspEligibilityPdfId(null);
|
||||||
|
setCmspEligibilityFilename(null);
|
||||||
|
setCmspHistoryPdfId(null);
|
||||||
|
setCmspHistoryFilename(null);
|
||||||
|
setCmspAccumulatorPdfId(null);
|
||||||
|
setCmspAccumulatorFilename(null);
|
||||||
|
}}
|
||||||
|
title="CMSP Eligibility, History & Remaining"
|
||||||
|
panels={[
|
||||||
|
{
|
||||||
|
pdfId: cmspEligibilityPdfId,
|
||||||
|
fallbackFilename: cmspEligibilityFilename,
|
||||||
|
label: "Eligibility",
|
||||||
|
autoDownload: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pdfId: cmspHistoryPdfId,
|
||||||
|
fallbackFilename: cmspHistoryFilename,
|
||||||
|
label: "Service History",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pdfId: cmspAccumulatorPdfId,
|
||||||
|
fallbackFilename: cmspAccumulatorFilename,
|
||||||
|
label: "Accumulator (Remaining)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Dual PDF modal for MH Eligibility & History — both PDFs side by side */}
|
||||||
|
<DualPdfPreviewModal
|
||||||
|
open={dualPreviewOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setDualPreviewOpen(false);
|
||||||
|
setDualEligibilityPdfId(null);
|
||||||
|
setDualEligibilityFilename(null);
|
||||||
|
setDualHistoryPdfId(null);
|
||||||
|
setDualHistoryFilename(null);
|
||||||
|
}}
|
||||||
|
title="MH Eligibility & Service History"
|
||||||
|
panels={[
|
||||||
|
{
|
||||||
|
pdfId: dualEligibilityPdfId,
|
||||||
|
fallbackFilename: dualEligibilityFilename,
|
||||||
|
label: "Eligibility",
|
||||||
|
autoDownload: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pdfId: dualHistoryPdfId,
|
||||||
|
fallbackFilename: dualHistoryFilename,
|
||||||
|
label: "Service History",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import uvicorn
|
|||||||
import asyncio
|
import asyncio
|
||||||
from selenium_claimSubmitWorker import AutomationMassHealthClaimsLogin
|
from selenium_claimSubmitWorker import AutomationMassHealthClaimsLogin
|
||||||
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
||||||
|
from selenium_MH_eligibilityHistoryCheckWorker import AutomationMassHealthEligibilityHistoryCheck
|
||||||
|
from selenium_CMSP_eligibilityHistoryRemainingCheckWorker import AutomationCMSPEligibilityHistoryRemainingCheck
|
||||||
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
||||||
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
||||||
from selenium_MHPaymentCheckWorker import AutomationMassHealthPaymentCheck
|
from selenium_MHPaymentCheckWorker import AutomationMassHealthPaymentCheck
|
||||||
@@ -135,6 +137,83 @@ async def start_workflow(request: Request):
|
|||||||
async with lock:
|
async with lock:
|
||||||
active_jobs -= 1
|
active_jobs -= 1
|
||||||
|
|
||||||
|
# Endpoint: 2a — MH Eligibility + Service History
|
||||||
|
@app.post("/mh-eligibility-history-check")
|
||||||
|
async def mh_eligibility_history_check(request: Request):
|
||||||
|
global active_jobs, waiting_jobs
|
||||||
|
data = await request.json()
|
||||||
|
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs += 1
|
||||||
|
|
||||||
|
async with semaphore:
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs -= 1
|
||||||
|
active_jobs += 1
|
||||||
|
try:
|
||||||
|
bot = AutomationMassHealthEligibilityHistoryCheck(data)
|
||||||
|
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
|
||||||
|
|
||||||
|
if result.get("status") == "error":
|
||||||
|
return {"status": "error", "message": result.get("message")}
|
||||||
|
|
||||||
|
port = os.getenv("PORT", "5002")
|
||||||
|
url_host = os.getenv("HOST", "localhost")
|
||||||
|
|
||||||
|
if result.get("pdf_path"):
|
||||||
|
filename = os.path.basename(result["pdf_path"])
|
||||||
|
result["pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}"
|
||||||
|
|
||||||
|
if result.get("history_pdf_path"):
|
||||||
|
filename = os.path.basename(result["history_pdf_path"])
|
||||||
|
result["history_pdf_url"] = f"http://{url_host}:{port}/downloads/{filename}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
finally:
|
||||||
|
async with lock:
|
||||||
|
active_jobs -= 1
|
||||||
|
|
||||||
|
# Endpoint: 2b — CMSP Eligibility + Service History + Accumulator (Remaining)
|
||||||
|
@app.post("/cmsp-eligibility-history-remaining-check")
|
||||||
|
async def cmsp_eligibility_history_remaining_check(request: Request):
|
||||||
|
global active_jobs, waiting_jobs
|
||||||
|
data = await request.json()
|
||||||
|
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs += 1
|
||||||
|
|
||||||
|
async with semaphore:
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs -= 1
|
||||||
|
active_jobs += 1
|
||||||
|
try:
|
||||||
|
bot = AutomationCMSPEligibilityHistoryRemainingCheck(data)
|
||||||
|
result = bot.main_workflow("https://provider.masshealth-dental.org/mh_provider_login")
|
||||||
|
|
||||||
|
if result.get("status") == "error":
|
||||||
|
return {"status": "error", "message": result.get("message")}
|
||||||
|
|
||||||
|
port = os.getenv("PORT", "5002")
|
||||||
|
url_host = os.getenv("HOST", "localhost")
|
||||||
|
|
||||||
|
for key, url_key in [
|
||||||
|
("pdf_path", "pdf_url"),
|
||||||
|
("history_pdf_path", "history_pdf_url"),
|
||||||
|
("accumulator_pdf_path", "accumulator_pdf_url"),
|
||||||
|
]:
|
||||||
|
if result.get(key):
|
||||||
|
filename = os.path.basename(result[key])
|
||||||
|
result[url_key] = f"http://{url_host}:{port}/downloads/{filename}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
finally:
|
||||||
|
async with lock:
|
||||||
|
active_jobs -= 1
|
||||||
|
|
||||||
# Endpoint: 2.1 — Start the automation for Claims login (open browser and log in)
|
# Endpoint: 2.1 — Start the automation for Claims login (open browser and log in)
|
||||||
@app.post("/claims-login")
|
@app.post("/claims-login")
|
||||||
async def start_claims_login(request: Request):
|
async def start_claims_login(request: Request):
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common import TimeoutException
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait, Select
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationCMSPEligibilityHistoryRemainingCheck:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.headless = False
|
||||||
|
self.driver = None
|
||||||
|
self.extracted_data = {}
|
||||||
|
self.eligibility_tab = None
|
||||||
|
|
||||||
|
self.data = data.get("data")
|
||||||
|
|
||||||
|
self.massdhp_username = self.data.get("massdhpUsername", "")
|
||||||
|
self.massdhp_password = self.data.get("massdhpPassword", "")
|
||||||
|
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||||
|
self.memberId = self.data.get("memberId", "")
|
||||||
|
self.firstName = self.data.get("firstName", "")
|
||||||
|
self.lastName = self.data.get("lastName", "")
|
||||||
|
|
||||||
|
# Convert dateOfBirth from YYYY-MM-DD to MMDDYYYY
|
||||||
|
if self.dateOfBirth and "-" in self.dateOfBirth:
|
||||||
|
parts = self.dateOfBirth.split("-")
|
||||||
|
if len(parts) == 3:
|
||||||
|
year, month, day = parts
|
||||||
|
self.dateOfBirth = f"{month.zfill(2)}{day.zfill(2)}{year}"
|
||||||
|
|
||||||
|
self.download_dir = os.path.abspath("downloads")
|
||||||
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# ── driver ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def config_driver(self):
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
if self.headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
|
||||||
|
prefs = {
|
||||||
|
"download.default_directory": self.download_dir,
|
||||||
|
"plugins.always_open_pdf_externally": False,
|
||||||
|
"download.prompt_for_download": False,
|
||||||
|
"download.directory_upgrade": True,
|
||||||
|
}
|
||||||
|
options.add_experimental_option("prefs", prefs)
|
||||||
|
|
||||||
|
s = Service(ChromeDriverManager().install())
|
||||||
|
self.driver = webdriver.Chrome(service=s, options=options)
|
||||||
|
|
||||||
|
# ── login (same MassHealth credentials) ─────────────────────────────────────
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
try:
|
||||||
|
signin_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a.btn.btn-block.btn-primary[href='https://connectsso.masshealth-dental.org/mhprovider/index.html']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
signin_button.click()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
email_field = wait.until(EC.presence_of_element_located((By.ID, "User")))
|
||||||
|
email_field.clear()
|
||||||
|
email_field.send_keys(self.massdhp_username)
|
||||||
|
|
||||||
|
password_field = wait.until(EC.presence_of_element_located((By.ID, "Password")))
|
||||||
|
password_field.clear()
|
||||||
|
password_field.send_keys(self.massdhp_password)
|
||||||
|
|
||||||
|
login_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "input[type='submit'][name='submit'][value='Login']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
login_button.click()
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[login] Error: {e}")
|
||||||
|
return "ERROR:LOGIN FAILED"
|
||||||
|
|
||||||
|
# ── step 1 — search member, extract eligibility data ────────────────────────
|
||||||
|
|
||||||
|
def step1(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "init"
|
||||||
|
try:
|
||||||
|
substep = "patient_management"
|
||||||
|
patient_mgmt = wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//strong[@translate='Patient Management']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", patient_mgmt)
|
||||||
|
self.driver.execute_script("arguments[0].click();", patient_mgmt)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
substep = "member_eligibility_link"
|
||||||
|
eligibility_link = wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//a[@translate='Member Eligibility']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].click();", eligibility_link)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
substep = "provider_dropdown"
|
||||||
|
provider_dropdown = wait.until(
|
||||||
|
EC.presence_of_element_located((By.NAME, "provider"))
|
||||||
|
)
|
||||||
|
select_provider = Select(provider_dropdown)
|
||||||
|
first_option = next(
|
||||||
|
(o for o in select_provider.options if o.get_attribute("value").strip()),
|
||||||
|
select_provider.options[0]
|
||||||
|
)
|
||||||
|
select_provider.select_by_value(first_option.get_attribute("value"))
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
substep = "member_dob"
|
||||||
|
member_dob = wait.until(
|
||||||
|
EC.presence_of_all_elements_located((By.NAME, "dateInput"))
|
||||||
|
)[1]
|
||||||
|
member_dob.clear()
|
||||||
|
member_dob.send_keys(self.dateOfBirth)
|
||||||
|
|
||||||
|
substep = "member_number"
|
||||||
|
member_number = wait.until(
|
||||||
|
EC.presence_of_element_located((By.NAME, "memberNumber"))
|
||||||
|
)
|
||||||
|
member_number.clear()
|
||||||
|
member_number.send_keys(self.memberId)
|
||||||
|
|
||||||
|
substep = "search_button"
|
||||||
|
search_button = wait.until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary')]"))
|
||||||
|
)
|
||||||
|
search_button.click()
|
||||||
|
|
||||||
|
substep = "wait_results"
|
||||||
|
wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.extracted_data = self._extract_data_from_page()
|
||||||
|
print(f"[step1] data extracted: {self.extracted_data}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step1] FAILED at substep='{substep}': {e}")
|
||||||
|
return f"ERROR:STEP1:{substep}"
|
||||||
|
|
||||||
|
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _cell_text(self, cell):
|
||||||
|
text = cell.text.strip()
|
||||||
|
if not text:
|
||||||
|
try:
|
||||||
|
text = (self.driver.execute_script("return arguments[0].innerText;", cell) or "").strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _normalize_id(self, s):
|
||||||
|
return "".join(c for c in str(s) if c.isalnum()).lower()
|
||||||
|
|
||||||
|
def _extract_data_from_page(self):
|
||||||
|
wait = WebDriverWait(self.driver, 15)
|
||||||
|
try:
|
||||||
|
wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]:
|
||||||
|
rows = self.driver.find_elements(
|
||||||
|
By.XPATH,
|
||||||
|
f"//h4[text()='{status_label}']/following::table[1]/tbody/tr"
|
||||||
|
)
|
||||||
|
for row in rows:
|
||||||
|
cells = row.find_elements(By.TAG_NAME, "td")
|
||||||
|
if len(cells) < 3:
|
||||||
|
continue
|
||||||
|
norm_cell = self._normalize_id(self._cell_text(cells[2]))
|
||||||
|
norm_self = self._normalize_id(self.memberId)
|
||||||
|
if norm_self and norm_cell and (norm_self in norm_cell or norm_cell in norm_self):
|
||||||
|
full_name = self._cell_text(cells[4]) if len(cells) > 4 else ""
|
||||||
|
plan_name = (
|
||||||
|
self._cell_text(cells[6]) if len(cells) > 6
|
||||||
|
else (self._cell_text(cells[-1]) if len(cells) > 4 else "")
|
||||||
|
)
|
||||||
|
name_parts = full_name.split()
|
||||||
|
return {
|
||||||
|
"eligibility": elig_flag,
|
||||||
|
"firstName": name_parts[0] if name_parts else "",
|
||||||
|
"lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "",
|
||||||
|
"insurance": plan_name,
|
||||||
|
}
|
||||||
|
return {"eligibility": None}
|
||||||
|
except Exception as e:
|
||||||
|
print("Extraction error:", e)
|
||||||
|
return {"eligibility": None}
|
||||||
|
|
||||||
|
# ── step 2 — print eligibility PDF, return to results tab ───────────────────
|
||||||
|
|
||||||
|
def step2_eligibility_pdf(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
self.eligibility_tab = self.driver.current_window_handle
|
||||||
|
try:
|
||||||
|
download_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.XPATH, "//button[contains(.,'Printer Friendly Format')]")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
download_button.click()
|
||||||
|
|
||||||
|
try:
|
||||||
|
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||||
|
new_tabs = [t for t in self.driver.window_handles if t != self.eligibility_tab]
|
||||||
|
self.driver.switch_to.window(new_tabs[0])
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||||
|
time.sleep(2)
|
||||||
|
except TimeoutException:
|
||||||
|
print("No new tab for eligibility; printing current page directly")
|
||||||
|
|
||||||
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||||
|
pdf_filename = f"cmsp_eligibility_{safe_member}.pdf"
|
||||||
|
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||||
|
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||||
|
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||||
|
with open(pdf_path, "wb") as f:
|
||||||
|
f.write(pdf_bytes)
|
||||||
|
print("Eligibility PDF saved at:", pdf_path)
|
||||||
|
|
||||||
|
if self.driver.current_window_handle != self.eligibility_tab:
|
||||||
|
self.driver.close()
|
||||||
|
self.driver.switch_to.window(self.eligibility_tab)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return pdf_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step2_eligibility_pdf] failed: {e}")
|
||||||
|
if self.eligibility_tab and self.driver.current_window_handle != self.eligibility_tab:
|
||||||
|
try:
|
||||||
|
self.driver.close()
|
||||||
|
self.driver.switch_to.window(self.eligibility_tab)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
# ── step 3 — click member ID link → member details ──────────────────────────
|
||||||
|
|
||||||
|
def step3_click_member_id(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "ng_bind_link"
|
||||||
|
try:
|
||||||
|
member_link = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a[ng-bind='member.memberNumber']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].click();", member_link)
|
||||||
|
time.sleep(2)
|
||||||
|
print(f"[step3] clicked member ID link, URL: {self.driver.current_url}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step3] FAILED at substep='{substep}': {e}")
|
||||||
|
return f"ERROR:STEP3:{substep}"
|
||||||
|
|
||||||
|
# ── step 4 — click "VIEW SERVICE HISTORY" → service history page ─────────────
|
||||||
|
|
||||||
|
def step4_view_service_history(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "service_history_link"
|
||||||
|
try:
|
||||||
|
history_link = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a.btn.btn-primary[href*='service-history']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", history_link)
|
||||||
|
self.driver.execute_script("arguments[0].click();", history_link)
|
||||||
|
time.sleep(2)
|
||||||
|
print(f"[step4] navigated to service history, URL: {self.driver.current_url}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step4] FAILED at substep='{substep}': {e}")
|
||||||
|
return f"ERROR:STEP4:{substep}"
|
||||||
|
|
||||||
|
# ── step 5 — print history PDF via CDP, navigate back to member details ──────
|
||||||
|
# We do NOT click the "Printer Friendly Format" button here because on the
|
||||||
|
# service history page it calls window.print() (native dialog) which freezes
|
||||||
|
# Chrome. Instead we capture the page directly via CDP and then go back.
|
||||||
|
|
||||||
|
def step5_history_pdf(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
try:
|
||||||
|
# Wait for Angular to finish rendering the service history data
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||||
|
pdf_filename = f"cmsp_history_{safe_member}.pdf"
|
||||||
|
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||||
|
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||||
|
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||||
|
with open(pdf_path, "wb") as f:
|
||||||
|
f.write(pdf_bytes)
|
||||||
|
print("History PDF saved at:", pdf_path)
|
||||||
|
|
||||||
|
# Go back to member details page where "View Accumulator" lives
|
||||||
|
self.driver.back()
|
||||||
|
time.sleep(2)
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
print(f"[step5] returned to member details, URL: {self.driver.current_url}")
|
||||||
|
|
||||||
|
return pdf_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step5_history_pdf] failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── step 6 — click "View Accumulator" ───────────────────────────────────────
|
||||||
|
|
||||||
|
def step6_click_view_accumulator(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "view_accumulator_link"
|
||||||
|
try:
|
||||||
|
# ng-if="vm.showAccumulator" is the stable Angular condition on this link
|
||||||
|
accumulator_link = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a.btn.btn-primary[ng-if='vm.showAccumulator']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", accumulator_link)
|
||||||
|
self.driver.execute_script("arguments[0].click();", accumulator_link)
|
||||||
|
time.sleep(2)
|
||||||
|
print(f"[step6] navigated to accumulator, URL: {self.driver.current_url}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step6] FAILED at substep='{substep}': {e}")
|
||||||
|
return f"ERROR:STEP6:{substep}"
|
||||||
|
|
||||||
|
# ── step 7 — print accumulator PDF ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def step7_accumulator_pdf(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
current_tab = self.driver.current_window_handle
|
||||||
|
try:
|
||||||
|
# ng-click="vm.printResults()" and ng-if="vm.hasResults" identify this button
|
||||||
|
print_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "button.btn.btn-primary[ng-click='vm.printResults()']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print_button.click()
|
||||||
|
|
||||||
|
try:
|
||||||
|
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||||
|
new_tabs = [t for t in self.driver.window_handles if t != current_tab]
|
||||||
|
self.driver.switch_to.window(new_tabs[0])
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||||
|
time.sleep(2)
|
||||||
|
except TimeoutException:
|
||||||
|
print("No new tab for accumulator; printing current page directly")
|
||||||
|
|
||||||
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||||
|
pdf_filename = f"cmsp_accumulator_{safe_member}.pdf"
|
||||||
|
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||||
|
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||||
|
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||||
|
with open(pdf_path, "wb") as f:
|
||||||
|
f.write(pdf_bytes)
|
||||||
|
print("Accumulator PDF saved at:", pdf_path)
|
||||||
|
return pdf_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step7_accumulator_pdf] failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── main workflow ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main_workflow(self, url):
|
||||||
|
try:
|
||||||
|
self.config_driver()
|
||||||
|
self.driver.maximize_window()
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
login_result = self.login()
|
||||||
|
if login_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": login_result}
|
||||||
|
|
||||||
|
step1_result = self.step1()
|
||||||
|
if step1_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": step1_result}
|
||||||
|
|
||||||
|
eligibility_pdf_path = self.step2_eligibility_pdf()
|
||||||
|
|
||||||
|
step3_result = self.step3_click_member_id()
|
||||||
|
if step3_result.startswith("ERROR"):
|
||||||
|
return {"status": "partial", "message": step3_result,
|
||||||
|
"pdf_path": eligibility_pdf_path, "file_type": "pdf"}
|
||||||
|
|
||||||
|
step4_result = self.step4_view_service_history()
|
||||||
|
if step4_result.startswith("ERROR"):
|
||||||
|
return {"status": "partial", "message": step4_result,
|
||||||
|
"pdf_path": eligibility_pdf_path, "file_type": "pdf"}
|
||||||
|
|
||||||
|
history_pdf_path = self.step5_history_pdf()
|
||||||
|
|
||||||
|
step6_result = self.step6_click_view_accumulator()
|
||||||
|
if step6_result.startswith("ERROR"):
|
||||||
|
return {"status": "partial", "message": step6_result,
|
||||||
|
"pdf_path": eligibility_pdf_path,
|
||||||
|
"history_pdf_path": history_pdf_path, "file_type": "pdf"}
|
||||||
|
|
||||||
|
accumulator_pdf_path = self.step7_accumulator_pdf()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"pdf_path": eligibility_pdf_path,
|
||||||
|
"history_pdf_path": history_pdf_path,
|
||||||
|
"accumulator_pdf_path": accumulator_pdf_path,
|
||||||
|
"file_type": "pdf",
|
||||||
|
"message": "Eligibility, service history, and accumulator PDFs captured",
|
||||||
|
}
|
||||||
|
if self.extracted_data:
|
||||||
|
result.update(self.extracted_data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
finally:
|
||||||
|
if self.driver:
|
||||||
|
self.driver.quit()
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common import TimeoutException
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait, Select
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationMassHealthEligibilityHistoryCheck:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.headless = False
|
||||||
|
self.driver = None
|
||||||
|
self.extracted_data = {}
|
||||||
|
self.eligibility_tab = None # handle to the eligibility results tab
|
||||||
|
|
||||||
|
self.data = data.get("data")
|
||||||
|
|
||||||
|
self.massdhp_username = self.data.get("massdhpUsername", "")
|
||||||
|
self.massdhp_password = self.data.get("massdhpPassword", "")
|
||||||
|
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||||
|
self.memberId = self.data.get("memberId", "")
|
||||||
|
self.firstName = self.data.get("firstName", "")
|
||||||
|
self.lastName = self.data.get("lastName", "")
|
||||||
|
|
||||||
|
# Convert dateOfBirth from YYYY-MM-DD to MMDDYYYY
|
||||||
|
if self.dateOfBirth and "-" in self.dateOfBirth:
|
||||||
|
parts = self.dateOfBirth.split("-")
|
||||||
|
if len(parts) == 3:
|
||||||
|
year, month, day = parts
|
||||||
|
self.dateOfBirth = f"{month.zfill(2)}{day.zfill(2)}{year}"
|
||||||
|
|
||||||
|
self.download_dir = os.path.abspath("downloads")
|
||||||
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# ── driver ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def config_driver(self):
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
if self.headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
|
||||||
|
prefs = {
|
||||||
|
"download.default_directory": self.download_dir,
|
||||||
|
"plugins.always_open_pdf_externally": False,
|
||||||
|
"download.prompt_for_download": False,
|
||||||
|
"download.directory_upgrade": True,
|
||||||
|
}
|
||||||
|
options.add_experimental_option("prefs", prefs)
|
||||||
|
|
||||||
|
s = Service(ChromeDriverManager().install())
|
||||||
|
self.driver = webdriver.Chrome(service=s, options=options)
|
||||||
|
|
||||||
|
# ── login ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
try:
|
||||||
|
signin_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a.btn.btn-block.btn-primary[href='https://connectsso.masshealth-dental.org/mhprovider/index.html']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
signin_button.click()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
email_field = wait.until(EC.presence_of_element_located((By.ID, "User")))
|
||||||
|
email_field.clear()
|
||||||
|
email_field.send_keys(self.massdhp_username)
|
||||||
|
|
||||||
|
password_field = wait.until(EC.presence_of_element_located((By.ID, "Password")))
|
||||||
|
password_field.clear()
|
||||||
|
password_field.send_keys(self.massdhp_password)
|
||||||
|
|
||||||
|
login_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "input[type='submit'][name='submit'][value='Login']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
login_button.click()
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[login] Error: {e}")
|
||||||
|
return "ERROR:LOGIN FAILED"
|
||||||
|
|
||||||
|
# ── step 1 — search member, extract eligibility data ────────────────────────
|
||||||
|
|
||||||
|
def step1(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "init"
|
||||||
|
try:
|
||||||
|
print(f"[step1] URL: {self.driver.current_url}")
|
||||||
|
|
||||||
|
substep = "patient_management"
|
||||||
|
patient_mgmt = wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//strong[@translate='Patient Management']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", patient_mgmt)
|
||||||
|
self.driver.execute_script("arguments[0].click();", patient_mgmt)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
substep = "member_eligibility_link"
|
||||||
|
eligibility_link = wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//a[@translate='Member Eligibility']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].click();", eligibility_link)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
substep = "provider_dropdown"
|
||||||
|
provider_dropdown = wait.until(
|
||||||
|
EC.presence_of_element_located((By.NAME, "provider"))
|
||||||
|
)
|
||||||
|
select_provider = Select(provider_dropdown)
|
||||||
|
options = [o.text for o in select_provider.options]
|
||||||
|
print(f"[step1] provider options: {options}")
|
||||||
|
|
||||||
|
substep = "select_provider"
|
||||||
|
first_option = next(
|
||||||
|
(o for o in select_provider.options if o.get_attribute("value").strip()),
|
||||||
|
select_provider.options[0]
|
||||||
|
)
|
||||||
|
print(f"[step1] selecting provider: '{first_option.text}'")
|
||||||
|
select_provider.select_by_value(first_option.get_attribute("value"))
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
substep = "member_dob"
|
||||||
|
member_dob = wait.until(
|
||||||
|
EC.presence_of_all_elements_located((By.NAME, "dateInput"))
|
||||||
|
)[1]
|
||||||
|
member_dob.clear()
|
||||||
|
member_dob.send_keys(self.dateOfBirth)
|
||||||
|
|
||||||
|
substep = "member_number"
|
||||||
|
member_number = wait.until(
|
||||||
|
EC.presence_of_element_located((By.NAME, "memberNumber"))
|
||||||
|
)
|
||||||
|
member_number.clear()
|
||||||
|
member_number.send_keys(self.memberId)
|
||||||
|
|
||||||
|
substep = "search_button"
|
||||||
|
search_button = wait.until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary')]"))
|
||||||
|
)
|
||||||
|
search_button.click()
|
||||||
|
|
||||||
|
substep = "wait_results"
|
||||||
|
wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.extracted_data = self._extract_data_from_page()
|
||||||
|
print(f"[step1] data extracted: {self.extracted_data}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step1] FAILED at substep='{substep}': {e}")
|
||||||
|
print(f"[step1] URL at failure: {self.driver.current_url}")
|
||||||
|
return f"ERROR:STEP1:{substep}"
|
||||||
|
|
||||||
|
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _cell_text(self, cell):
|
||||||
|
text = cell.text.strip()
|
||||||
|
if not text:
|
||||||
|
try:
|
||||||
|
text = (self.driver.execute_script("return arguments[0].innerText;", cell) or "").strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _normalize_id(self, s):
|
||||||
|
return "".join(c for c in str(s) if c.isalnum()).lower()
|
||||||
|
|
||||||
|
def _extract_data_from_page(self):
|
||||||
|
wait = WebDriverWait(self.driver, 15)
|
||||||
|
extracted = {}
|
||||||
|
try:
|
||||||
|
wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]:
|
||||||
|
rows = self.driver.find_elements(
|
||||||
|
By.XPATH,
|
||||||
|
f"//h4[text()='{status_label}']/following::table[1]/tbody/tr"
|
||||||
|
)
|
||||||
|
for row in rows:
|
||||||
|
cells = row.find_elements(By.TAG_NAME, "td")
|
||||||
|
if len(cells) < 3:
|
||||||
|
continue
|
||||||
|
member_number = self._cell_text(cells[2])
|
||||||
|
norm_cell = self._normalize_id(member_number)
|
||||||
|
norm_self = self._normalize_id(self.memberId)
|
||||||
|
if norm_self and norm_cell and (norm_self in norm_cell or norm_cell in norm_self):
|
||||||
|
full_name = self._cell_text(cells[4]) if len(cells) > 4 else ""
|
||||||
|
plan_name = (
|
||||||
|
self._cell_text(cells[6]) if len(cells) > 6
|
||||||
|
else (self._cell_text(cells[-1]) if len(cells) > 4 else "")
|
||||||
|
)
|
||||||
|
name_parts = full_name.split()
|
||||||
|
extracted = {
|
||||||
|
"eligibility": elig_flag,
|
||||||
|
"firstName": name_parts[0] if name_parts else "",
|
||||||
|
"lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "",
|
||||||
|
"insurance": plan_name,
|
||||||
|
}
|
||||||
|
print(f"[extraction] MATCHED {status_label} → name='{full_name}' plan='{plan_name}'")
|
||||||
|
return extracted
|
||||||
|
|
||||||
|
print(f"[extraction] No matching row for memberId='{self.memberId}'")
|
||||||
|
return {"eligibility": None}
|
||||||
|
except Exception as e:
|
||||||
|
print("Extraction error:", e)
|
||||||
|
return {"eligibility": None}
|
||||||
|
|
||||||
|
# ── step 2 — print eligibility PDF, stay on results tab ─────────────────────
|
||||||
|
|
||||||
|
def step2_eligibility_pdf(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
self.eligibility_tab = self.driver.current_window_handle
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.XPATH, "//button[contains(.,'Printer Friendly Format')]")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
download_button.click()
|
||||||
|
|
||||||
|
try:
|
||||||
|
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||||
|
new_tabs = [t for t in self.driver.window_handles if t != self.eligibility_tab]
|
||||||
|
self.driver.switch_to.window(new_tabs[0])
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||||
|
time.sleep(2)
|
||||||
|
print("Printer-friendly tab opened:", self.driver.current_url)
|
||||||
|
except TimeoutException:
|
||||||
|
print("No new tab for eligibility; printing current page directly")
|
||||||
|
|
||||||
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||||
|
pdf_filename = f"eligibility_{safe_member}.pdf"
|
||||||
|
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||||
|
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||||
|
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||||
|
with open(pdf_path, "wb") as f:
|
||||||
|
f.write(pdf_bytes)
|
||||||
|
print("Eligibility PDF saved at:", pdf_path)
|
||||||
|
|
||||||
|
# Close printer-friendly tab and return to eligibility results
|
||||||
|
if self.driver.current_window_handle != self.eligibility_tab:
|
||||||
|
self.driver.close()
|
||||||
|
self.driver.switch_to.window(self.eligibility_tab)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return pdf_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step2_eligibility_pdf] failed: {e}")
|
||||||
|
# Ensure we are back on the eligibility results tab even on error
|
||||||
|
if self.eligibility_tab and self.driver.current_window_handle != self.eligibility_tab:
|
||||||
|
try:
|
||||||
|
self.driver.close()
|
||||||
|
self.driver.switch_to.window(self.eligibility_tab)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
# ── step 3 — click member ID link → member details page ─────────────────────
|
||||||
|
|
||||||
|
def step3_click_member_id(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "init"
|
||||||
|
try:
|
||||||
|
# Primary: ng-bind="member.memberNumber" is the stable Angular binding on the link
|
||||||
|
substep = "ng_bind_link"
|
||||||
|
member_link = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a[ng-bind='member.memberNumber']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].click();", member_link)
|
||||||
|
time.sleep(2)
|
||||||
|
print(f"[step3] clicked member ID link, URL: {self.driver.current_url}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step3] FAILED at substep='{substep}': {e}")
|
||||||
|
return f"ERROR:STEP3:{substep}"
|
||||||
|
|
||||||
|
# ── step 4 — click "View Service History" on member details page ─────────────
|
||||||
|
|
||||||
|
def step4_view_service_history(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
substep = "init"
|
||||||
|
try:
|
||||||
|
# Primary: href contains /service-history and has btn-primary class
|
||||||
|
substep = "service_history_link"
|
||||||
|
history_link = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "a.btn.btn-primary[href*='service-history']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView(true);", history_link)
|
||||||
|
self.driver.execute_script("arguments[0].click();", history_link)
|
||||||
|
time.sleep(2)
|
||||||
|
print(f"[step4] navigated to service history, URL: {self.driver.current_url}")
|
||||||
|
return "Success"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step4] FAILED at substep='{substep}': {e}")
|
||||||
|
return f"ERROR:STEP4:{substep}"
|
||||||
|
|
||||||
|
# ── step 5 — print service history PDF ──────────────────────────────────────
|
||||||
|
|
||||||
|
def step5_history_pdf(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
current_tab = self.driver.current_window_handle
|
||||||
|
try:
|
||||||
|
print_button = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, "button.btn.btn-primary[ng-click='vm.printPage()']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print_button.click()
|
||||||
|
|
||||||
|
try:
|
||||||
|
WebDriverWait(self.driver, 10).until(lambda d: len(d.window_handles) > 1)
|
||||||
|
new_tabs = [t for t in self.driver.window_handles if t != current_tab]
|
||||||
|
self.driver.switch_to.window(new_tabs[0])
|
||||||
|
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
|
||||||
|
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||||
|
time.sleep(2)
|
||||||
|
print("History printer-friendly tab opened:", self.driver.current_url)
|
||||||
|
except TimeoutException:
|
||||||
|
print("No new tab for history; printing current page directly")
|
||||||
|
|
||||||
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||||
|
pdf_filename = f"eligibility_history_{safe_member}.pdf"
|
||||||
|
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {"printBackground": True})
|
||||||
|
pdf_bytes = base64.b64decode(pdf_data["data"])
|
||||||
|
pdf_path = os.path.join(self.download_dir, pdf_filename)
|
||||||
|
with open(pdf_path, "wb") as f:
|
||||||
|
f.write(pdf_bytes)
|
||||||
|
print("History PDF saved at:", pdf_path)
|
||||||
|
return pdf_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step5_history_pdf] failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── main workflow ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main_workflow(self, url):
|
||||||
|
try:
|
||||||
|
self.config_driver()
|
||||||
|
self.driver.maximize_window()
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
login_result = self.login()
|
||||||
|
if login_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": login_result}
|
||||||
|
|
||||||
|
step1_result = self.step1()
|
||||||
|
if step1_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": step1_result}
|
||||||
|
|
||||||
|
# Print eligibility PDF; stays on results tab afterwards
|
||||||
|
eligibility_pdf_path = self.step2_eligibility_pdf()
|
||||||
|
|
||||||
|
# Click the member ID link → member details
|
||||||
|
step3_result = self.step3_click_member_id()
|
||||||
|
if step3_result.startswith("ERROR"):
|
||||||
|
return {
|
||||||
|
"status": "partial",
|
||||||
|
"message": step3_result,
|
||||||
|
"pdf_path": eligibility_pdf_path,
|
||||||
|
"history_pdf_path": None,
|
||||||
|
"file_type": "pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Click "View Service History"
|
||||||
|
step4_result = self.step4_view_service_history()
|
||||||
|
if step4_result.startswith("ERROR"):
|
||||||
|
return {
|
||||||
|
"status": "partial",
|
||||||
|
"message": step4_result,
|
||||||
|
"pdf_path": eligibility_pdf_path,
|
||||||
|
"history_pdf_path": None,
|
||||||
|
"file_type": "pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print history PDF
|
||||||
|
history_pdf_path = self.step5_history_pdf()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"pdf_path": eligibility_pdf_path,
|
||||||
|
"history_pdf_path": history_pdf_path,
|
||||||
|
"file_type": "pdf",
|
||||||
|
"message": "Eligibility and service history PDFs captured successfully",
|
||||||
|
}
|
||||||
|
if self.extracted_data:
|
||||||
|
result.update(self.extracted_data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
finally:
|
||||||
|
if self.driver:
|
||||||
|
self.driver.quit()
|
||||||
Reference in New Issue
Block a user