feat: MassHealth member detail demographics, wait for name/plan before PDF

- Extract gender, address (street/city/state/zip) and home phone from the
  MassHealth Member Detail page and save to the patient; phone is only
  filled when the patient has none
- Shared extract_eligibility_row polls up to 30s until the matching results
  row has both name and plan before the PDF is printed (AngularJS fills cell
  text after the rows appear); used by all three MH eligibility workers
- Fall back to the Member Detail heading for the name when the results table
  never filled it in

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 15:53:34 -04:00
co-authored by Claude Opus 5.5
parent 13be6e97d5
commit ebc5fba2c9
8 changed files with 219 additions and 232 deletions
@@ -60,6 +60,33 @@ export function splitName(fullName?: string | null) {
return { firstName, lastName };
}
/**
* Patient updates from the MassHealth Member Detail block (gender, address,
* home phone) — only fields the portal actually returned. Phone is only
* filled when the patient has none, so a number the office entered is kept.
*/
export function memberDetailDemographics(
seleniumResult: any,
existingPhone?: string | null
): Record<string, string> {
const updates: Record<string, string> = {};
for (const key of ["address", "city", "state", "zipCode"] as const) {
const val = seleniumResult?.[key];
if (typeof val === "string" && val.trim()) updates[key] = val.trim();
}
// "Female"/"Male" → patient form values
const rawGender = String(seleniumResult?.gender ?? "").trim().toUpperCase();
const gender = rawGender.startsWith("M") ? "male" : rawGender.startsWith("F") ? "female" : "";
if (gender) updates.gender = gender;
// Stored like the patient form: "1" + 10 digits
const digits = String(seleniumResult?.phone ?? "").replace(/\D/g, "");
if (digits.length === 10 && !(existingPhone ?? "").trim()) updates.phone = "1" + digits;
return updates;
}
export async function imageToPdfBuffer(imagePath: string): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
try {
@@ -12,6 +12,7 @@ import {
callPythonSync,
splitName,
createOrUpdatePatientByInsuranceId,
memberDetailDemographics,
} from "./_shared";
export interface CmspEligibilityHistoryRemainingProcessorInput {
@@ -112,6 +113,7 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
const updates: any = {};
if (newStatus) updates.status = newStatus;
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
Object.assign(updates, memberDetailDemographics(seleniumResult, patient.phone));
const remainingBenefit = seleniumResult.remaining_amount?.trim() || null;
if (remainingBenefit) updates.remainingBenefit = remainingBenefit;
@@ -12,6 +12,7 @@ import {
callPythonSync,
splitName,
createOrUpdatePatientByInsuranceId,
memberDetailDemographics,
} from "./_shared";
export interface EligibilityHistoryProcessorInput {
@@ -103,6 +104,7 @@ export async function runEligibilityHistoryProcessor(
const updates: any = { status: newStatus };
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
Object.assign(updates, memberDetailDemographics(seleniumResult, patient.phone));
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;