feat: add Remaining/Third Party columns, toggle PDF viewer, retire CMSP as separate insurance type

- Add remainingBenefit and thirdPartyInsurance columns to Patient; extract both
  from the MassHealth accumulator/member-detail pages during the eligibility
  check and surface them in the eligibility table
- Add an updated-date display and a toggle to suppress the post-check PDF
  viewer popup (default off) on the insurance eligibility page
- Rename CMSP-prefixed PDF filenames and UI copy to MH, since CMSP and MH now
  share the same worker
- Fold CMSP into MassHealth/MH across patient/claim insurance-type dropdowns
  and the chatbot's eligibility routing, since they're the same insurance
- Remove auto-download of saved eligibility PDFs; downloading is now manual

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 22:32:43 -04:00
co-authored by Claude Sonnet 5
parent f8235787db
commit 52ddfd9f57
96 changed files with 561 additions and 160 deletions
@@ -34,6 +34,8 @@ export interface CmspEligibilityHistoryRemainingProcessorResult {
historyPdfFilename?: string | null;
accumulatorPdfFileId?: number | string | null;
accumulatorPdfFilename?: string | null;
remainingBenefit?: string | null;
thirdPartyInsurance?: string | null;
}
export async function runCmspEligibilityHistoryRemainingProcessor(
@@ -105,8 +107,21 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
const updates: any = { status: newStatus };
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
const remainingBenefit = seleniumResult.remaining_amount?.trim() || null;
if (remainingBenefit) updates.remainingBenefit = remainingBenefit;
// third_party_insurance is present (even as null) whenever the Third Party
// Liability table was found — null means the member has no 2nd insurance,
// so we still write it to clear any stale value from a prior check.
if (Object.prototype.hasOwnProperty.call(seleniumResult, "third_party_insurance")) {
updates.thirdPartyInsurance = seleniumResult.third_party_insurance?.trim() || null;
}
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
outputResult.remainingBenefit = remainingBenefit;
outputResult.thirdPartyInsurance = updates.thirdPartyInsurance ?? null;
// Helper: save a single PDF to the patient's Cloud Storage Eligibility folder
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
@@ -791,13 +791,12 @@ export function ClaimForm({
const deriveInsuranceSiteKey = (provider: string | null | undefined): string => {
const p = (provider || "").toLowerCase().trim();
if (!p) return "";
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
if (p.includes("masshealth") || p === "mh" || p === "mass health" || p.includes("cmsp")) return "MH";
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
if (p.includes("delta ins") || p === "deltains") return "DeltaIns";
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
if ((p.includes("united") && p.includes("sco")) || p === "unitedsco") return "UnitedSCO";
if (p.includes("cmsp")) return "CMSP";
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
if (p.includes("aetna")) return "Aetna";
@@ -2008,7 +2007,6 @@ export function ClaimForm({
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
<SelectItem value="UnitedSCO">United SCO</SelectItem>
<SelectItem value="CMSP">CMSP</SelectItem>
<SelectItem value="BCBS">BCBS</SelectItem>
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
@@ -2596,7 +2594,6 @@ export function ClaimForm({
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
<SelectItem value="UnitedSCO">United SCO</SelectItem>
<SelectItem value="CMSP">CMSP</SelectItem>
<SelectItem value="BCBS">BCBS</SelectItem>
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
@@ -135,7 +135,6 @@ function mdyToISO(mdy: string): string {
const INSURANCE_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: "mh", label: "MassHealth" },
{ value: "cmsp", label: "CMSP" },
{ value: "ddma", label: "DDMA" },
{ value: "delta-ins", label: "Delta Dental" },
{ value: "united-sco", label: "United SCO" },
@@ -143,13 +142,10 @@ const INSURANCE_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: "cca", label: "CCA" },
];
function getAutoCheck(dobISO: string): "mh" | "cmsp" {
const [y, m, d] = dobISO.split("-").map(Number);
const today = new Date();
let age = today.getFullYear() - (y ?? 0);
const monthDiff = today.getMonth() + 1 - (m ?? 0);
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < (d ?? 0))) age--;
return age >= 21 ? "mh" : "cmsp";
function getAutoCheck(_dobISO: string): "mh" {
// CMSP and MassHealth eligibility checks now run through the same MH worker,
// so there's no longer an age-based split — everyone is "mh".
return "mh";
}
function parseEligibilityInput(
@@ -64,13 +64,12 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
const normalizeInsuranceProvider = (val: string | null | undefined): string => {
const p = (val || "").toLowerCase().trim();
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MassHealth";
if (p.includes("masshealth") || p === "mh" || p === "mass health" || p.includes("cmsp")) return "MassHealth";
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
if (p.includes("ddma")) return "DDMA";
if (p.includes("delta dental") || p.includes("delta ins") || p === "deltains") return "DeltaIns";
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
if (p.includes("united sco") || p === "unitedsco") return "UnitedSCO";
if (p.includes("cmsp")) return "CMSP";
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
if (p.includes("aetna")) return "Aetna";
@@ -451,7 +450,6 @@ export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
<SelectItem value="UnitedSCO">United SCO</SelectItem>
<SelectItem value="CMSP">CMSP</SelectItem>
<SelectItem value="BCBS">BCBS</SelectItem>
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
@@ -63,6 +63,7 @@ interface PatientTableProps {
allowCheckbox?: boolean;
allowNewClaim?: boolean;
allowFinancial?: boolean;
showEligibilityColumns?: boolean;
onNewClaim?: (patientId: number) => void;
onSelectPatient?: (patient: Patient | null) => void;
onPageChange?: (page: number) => void;
@@ -82,6 +83,7 @@ export function PatientTable({
allowCheckbox,
allowNewClaim,
allowFinancial,
showEligibilityColumns,
onNewClaim,
onSelectPatient,
onPageChange,
@@ -976,6 +978,12 @@ export function PatientTable({
{/*<TableHead>Contact</TableHead>*/}
<TableHead>Insurance</TableHead>
<TableHead>Status</TableHead>
{showEligibilityColumns && (
<>
<TableHead>Remaining</TableHead>
<TableHead>Third Party/2nd Insurance</TableHead>
</>
)}
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -983,7 +991,7 @@ export function PatientTable({
{isLoading ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={showEligibilityColumns ? 8 : 6}
className="text-center py-8 text-muted-foreground"
>
<LoadingScreen />
@@ -992,7 +1000,7 @@ export function PatientTable({
) : isError ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={showEligibilityColumns ? 8 : 6}
className="text-center py-8 text-red-500"
>
Error loading patients.
@@ -1001,7 +1009,7 @@ export function PatientTable({
) : patientsData?.patients.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={showEligibilityColumns ? 8 : 6}
className="text-center py-8 text-muted-foreground"
>
No patients found.
@@ -1088,8 +1096,28 @@ export function PatientTable({
</span>
)}
</div>
{patient.updatedAt && (
<div className="text-xs text-gray-500 mt-1">
Updated: {formatDateToHumanReadable(patient.updatedAt)}
</div>
)}
</TableCell>
{showEligibilityColumns && (
<>
<TableCell>
<div className="text-sm text-gray-900">
{patient.remainingBenefit || "—"}
</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-900">
{patient.thirdPartyInsurance || "—"}
</div>
</TableCell>
</>
)}
<TableCell className="text-right">
<div className="flex justify-end">
{allowDelete && isAdmin && (
@@ -114,13 +114,12 @@ interface ScheduledAppointment {
function deriveInsuranceSiteKey(provider: string | null | undefined): string {
const p = (provider || "").toLowerCase().trim();
if (!p) return "";
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
if (p.includes("masshealth") || p === "mh" || p === "mass health" || p.includes("cmsp")) return "MH";
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
if (p.includes("delta ins") || p === "deltains") return "DeltaIns";
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
if ((p.includes("united") && p.includes("sco")) || p === "unitedsco") return "UnitedSCO";
if (p.includes("cmsp")) return "CMSP";
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
if (p.includes("aetna")) return "Aetna";
@@ -10,6 +10,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
@@ -122,6 +123,7 @@ export default function InsuranceStatusPage() {
const [isCheckingEligibilityHistory, setIsCheckingEligibilityHistory] =
useState(false);
const [isCheckingCMSP, setIsCheckingCMSP] = useState(false);
const [showPdfViewerOnComplete, setShowPdfViewerOnComplete] = useState(false);
// AI Call Insurance section
const [aiCallOpen, setAiCallOpen] = useState(false);
@@ -710,7 +712,7 @@ export default function InsuranceStatusPage() {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: msg }))
);
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "CMSP PDFs saved to Documents." }));
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "MH PDFs saved to Documents." }));
toast({
title: "Done",
description: "Eligibility, history, and accumulator PDFs saved.",
@@ -727,14 +729,14 @@ export default function InsuranceStatusPage() {
// Open 4-panel modal
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
setCmspEligibilityPdfId(jobResult.pdfFileId ?? null);
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
setCmspEligibilityFilename(jobResult.pdfFilename ?? `mh_eligibility_${memberId}.pdf`);
setCmspMemberDetailsPdfId(jobResult.memberDetailsPdfFileId ?? null);
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `cmsp_member_details_${memberId}.pdf`);
setCmspMemberDetailsFilename(jobResult.memberDetailsPdfFilename ?? `mh_member_details_${memberId}.pdf`);
setCmspHistoryPdfId(jobResult.historyPdfFileId ?? null);
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `cmsp_history_${memberId}.pdf`);
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `mh_history_${memberId}.pdf`);
setCmspAccumulatorPdfId(jobResult.accumulatorPdfFileId ?? null);
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `cmsp_accumulator_${memberId}.pdf`);
setCmspPreviewOpen(true);
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `mh_accumulator_${memberId}.pdf`);
if (showPdfViewerOnComplete) setCmspPreviewOpen(true);
}
} catch (error: any) {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
@@ -750,11 +752,9 @@ export default function InsuranceStatusPage() {
const check = pendingAutoCheck.current;
pendingAutoCheck.current = null;
if (check === "mh" || check === "mh-history") {
if (isUnder14(dateOfBirth)) {
handleCMSPButton();
} else {
handleMHEligibilityHistoryButton();
}
// Temporarily disabled: selenium_MH_eligibilityHistoryCheckWorker.py is no
// longer used — all MH eligibility checks route through the CMSP/MH worker.
handleCMSPButton();
} else if (check === "cmsp") {
handleCMSPButton();
} else {
@@ -1021,82 +1021,7 @@ export default function InsuranceStatusPage() {
</div>
</div>
<div className="flex flex-col-2 gap-4">
<Button
onClick={() => handleMHEligibilityButton()}
className="w-full"
disabled={isCheckingEligibilityStatus}
>
{isCheckingEligibilityStatus ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility
</>
)}
</Button>
<Button
onClick={() => handleMHEligibilityAppointmentButton()}
className="w-full"
disabled={isCheckingEligibilityAppointment}
>
{isCheckingEligibilityAppointment ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility&Appointment
</>
)}
</Button>
<Button
onClick={() => handleMHEligibilityClaimsPreAuthButton()}
className="w-full"
disabled={isCheckingEligibilityClaimsPreAuth}
>
{isCheckingEligibilityClaimsPreAuth ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility&Claims/PreAuth
</>
)}
</Button>
</div>
<div className="flex flex-col-2 gap-4 mt-4">
<Button
id="eligibility-mh-history"
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 &amp; History
</>
)}
</Button>
<Button
id="eligibility-cmsp"
className="w-full"
@@ -1111,12 +1036,23 @@ export default function InsuranceStatusPage() {
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
CMSP Eligibility &amp; History &amp; Remaining
MH Eligibility &amp; Remaining &amp; History
</>
)}
</Button>
</div>
<div className="flex items-center gap-2 mt-2">
<Switch
id="mh-show-pdf-viewer"
checked={showPdfViewerOnComplete}
onCheckedChange={setShowPdfViewerOnComplete}
/>
<Label htmlFor="mh-show-pdf-viewer" className="text-sm text-muted-foreground font-normal">
Show PDF viewer after check completes
</Label>
</div>
{/* TEMP PROVIDER BUTTONS */}
<div id="eligibility-other-providers" className="space-y-4 mt-6">
<h3 className="text-sm font-medium text-muted-foreground">
@@ -1464,6 +1400,7 @@ export default function InsuranceStatusPage() {
allowDelete={true}
allowCheckbox={true}
allowEdit={true}
showEligibilityColumns={true}
onSelectPatient={setSelectedPatient}
/>
</CardContent>
@@ -1480,7 +1417,6 @@ export default function InsuranceStatusPage() {
}}
pdfId={previewPdfId ?? undefined}
fallbackFilename={previewFallbackFilename ?? undefined}
autoDownload
/>
{/* 4-panel modal for CMSP — eligibility, member details, history, accumulator */}
@@ -1497,13 +1433,12 @@ export default function InsuranceStatusPage() {
setCmspAccumulatorPdfId(null);
setCmspAccumulatorFilename(null);
}}
title="CMSP Eligibility, History &amp; Remaining"
title="MH Eligibility, Remaining &amp; History"
panels={[
{
pdfId: cmspEligibilityPdfId,
fallbackFilename: cmspEligibilityFilename,
label: "Eligibility",
autoDownload: true,
},
{
pdfId: cmspMemberDetailsPdfId,
@@ -1541,7 +1476,6 @@ export default function InsuranceStatusPage() {
pdfId: dualEligibilityPdfId,
fallbackFilename: dualEligibilityFilename,
label: "Eligibility",
autoDownload: true,
},
{
pdfId: dualMemberDetailsPdfId,
@@ -234,7 +234,7 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
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_filename = f"mh_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)
@@ -279,14 +279,57 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
# ── step 3b — print member details page as PDF via CDP ───────────────────────
def extract_third_party_insurance(self):
"""Reads the "Third Party Liability" table (Policy Number | Coverage Type |
Effective Date | Termination Date | Insurer Name | Insurer Payment Order)
and returns the Insurer Name(s), or None if the table has no rows
(i.e. the member has no third-party/2nd insurance).
"""
try:
tables = self.driver.find_elements(By.TAG_NAME, "table")
for table in tables:
rows = table.find_elements(By.TAG_NAME, "tr")
if not rows:
continue
header_cells = [
c.text.strip().lower()
for c in (rows[0].find_elements(By.TAG_NAME, "th") or rows[0].find_elements(By.TAG_NAME, "td"))
]
if "insurer name" not in header_cells:
continue
insurer_idx = header_cells.index("insurer name")
insurers = []
for row in rows[1:]:
cells = row.find_elements(By.TAG_NAME, "td")
if len(cells) <= insurer_idx:
continue
name = cells[insurer_idx].text.strip()
if name:
insurers.append(name)
third_party = ", ".join(insurers) if insurers else None
self.extracted_data["third_party_insurance"] = third_party
print(f"[extract_third_party_insurance] {third_party!r}")
return third_party
print("[extract_third_party_insurance] No 'Third Party Liability' table found")
return None
except Exception as e:
print(f"[extract_third_party_insurance] failed: {e}")
return None
def step3b_member_details_pdf(self):
wait = WebDriverWait(self.driver, 30)
try:
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
time.sleep(2)
self.extract_third_party_insurance()
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
pdf_filename = f"cmsp_member_details_{safe_member}.pdf"
pdf_filename = f"mh_member_details_{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)
@@ -341,7 +384,7 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
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_filename = f"mh_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)
@@ -353,7 +396,68 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
print(f"[step5_service_history_pdf] failed: {e}")
return None
# ── step 6 — Tab B: accumulator → wait for data → CDP print ─────────────────
# ── step 6 — Tab B: accumulator → extract remaining → CDP print ─────────────
def extract_remaining_amount(self):
"""Reads the accumulator benefits table and returns the Remaining amount.
Layout (see sample PDF "Accumulator Details"):
Type | Level | Network | Benefit Period | Period | Limit | Applied | Remaining
Prefers the Type="Maximum", Level="Individual" row; falls back to the
first data row with a non-empty Remaining cell if that combination
isn't found (payer may only return one row, or reorder columns).
"""
try:
wait = WebDriverWait(self.driver, 15)
wait.until(EC.presence_of_element_located((By.TAG_NAME, "table")))
tables = self.driver.find_elements(By.TAG_NAME, "table")
for table in tables:
rows = table.find_elements(By.TAG_NAME, "tr")
if not rows:
continue
header_cells = [
c.text.strip().lower()
for c in (rows[0].find_elements(By.TAG_NAME, "th") or rows[0].find_elements(By.TAG_NAME, "td"))
]
if "remaining" not in header_cells:
continue
remaining_idx = header_cells.index("remaining")
type_idx = header_cells.index("type") if "type" in header_cells else None
level_idx = header_cells.index("level") if "level" in header_cells else None
fallback_value = None
best_value = None
for row in rows[1:]:
cells = row.find_elements(By.TAG_NAME, "td")
if len(cells) <= remaining_idx:
continue
cell_texts = [c.text.strip() for c in cells]
value = cell_texts[remaining_idx]
if not value:
continue
if fallback_value is None:
fallback_value = value
type_ok = type_idx is None or "maximum" in cell_texts[type_idx].lower()
level_ok = level_idx is None or "individual" in cell_texts[level_idx].lower()
if type_ok and level_ok:
best_value = value
break
remaining_value = best_value or fallback_value
if remaining_value:
self.extracted_data["remaining_amount"] = remaining_value
print(f"[extract_remaining_amount] Remaining: {remaining_value}")
return remaining_value
print("[extract_remaining_amount] No accumulator table with a 'Remaining' column found")
return None
except Exception as e:
print(f"[extract_remaining_amount] failed: {e}")
return None
def step6_accumulator_pdf(self):
wait = WebDriverWait(self.driver, 30)
@@ -379,8 +483,10 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
time.sleep(4)
self.extract_remaining_amount()
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_filename = f"mh_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)