feat: route batch-column claims by insurance type, fix eligibility UX

- claims.ts: batch-column now routes each patient to the correct portal
  (MH/CCA/DDMA/TuftsSCO/UnitedSCO) based on patient.insuranceProvider
- appointments-page.tsx: eligibility badge falls back to patientStatus
  for all insurance types, not just MassHealth
- unitedDHClaimProcessor/ddmaClaimProcessor/tuftsSCOClaimProcessor:
  auto-save claim PDF when no socketId (batch-column path)
- unitedSCOEligibilityProcessor: unknown eligibility no longer stored as INACTIVE
- queues.ts: add tuftssco-claim-submit and uniteddh-claim-submit to SeleniumJobType
- selenium_UnitedSCO_eligibilityCheckWorker.py:
  - step1: add Select Insurance OK click with staleness wait; Provider &
    Location page just clicks Continue; skip first/last name input
  - step2: extract name from eligibility details tab (ALL CAPS → title case);
    skip "not available" guard; fix name regex to match only all-caps words
- .gitignore: ignore all chrome_profile_* directories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-05-27 23:07:42 -04:00
parent 1b017177e9
commit 3e61bdec36
261 changed files with 331 additions and 119284 deletions

View File

@@ -14,6 +14,8 @@ import {
} from "../../services/seleniumDDMAClaimClient";
import { io } from "../../socket";
import { storage } from "../../storage";
import axios from "axios";
import path from "path";
function log(tag: string, msg: string, ctx?: any) {
console.log(`${new Date().toISOString()} [${tag}] ${msg}`, ctx ?? "");
@@ -86,6 +88,21 @@ async function pollUntilDone(
throw new Error(`DDMA claim polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
await storage.createPdfFile(group.id!, filename, resp.data);
log("ddma-claim-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("ddma-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
}
}
export interface DDMAClaimProcessorInput {
enrichedPayload: any;
userId: number;
@@ -139,6 +156,13 @@ export async function runDDMAClaimProcessor(
}
}
// Auto-save PDF for batch-column calls (no socketId = no frontend listener)
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
}
emitToSocket(socketId, "selenium:ddma_claim_completed", {
jobId,
claimId,

View File

@@ -14,6 +14,8 @@ import {
} from "../../services/seleniumTuftsSCOClaimClient";
import { io } from "../../socket";
import { storage } from "../../storage";
import axios from "axios";
import path from "path";
function log(tag: string, msg: string, ctx?: any) {
console.log(`${new Date().toISOString()} [${tag}] ${msg}`, ctx ?? "");
@@ -85,6 +87,21 @@ async function pollUntilDone(
throw new Error(`Tufts SCO claim polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
await storage.createPdfFile(group.id!, filename, resp.data);
log("tuftssco-claim-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("tuftssco-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
}
}
export interface TuftsSCOClaimProcessorInput {
enrichedPayload: any;
userId: number;
@@ -130,6 +147,13 @@ export async function runTuftsSCOClaimProcessor(
}
}
// Auto-save PDF for batch-column calls (no socketId = no frontend listener)
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
}
emitToSocket(socketId, "selenium:tuftssco_claim_completed", {
jobId,
claimId,

View File

@@ -14,6 +14,8 @@ import {
} from "../../services/seleniumUnitedDHClaimClient";
import { io } from "../../socket";
import { storage } from "../../storage";
import axios from "axios";
import path from "path";
function log(tag: string, msg: string, ctx?: any) {
console.log(`${new Date().toISOString()} [${tag}] ${msg}`, ctx ?? "");
@@ -97,6 +99,24 @@ async function pollUntilDone(
throw new Error(`UnitedDH claim polling exhausted all attempts for session ${sessionId}`);
}
async function savePdfFromSelenium(pdf_url: string, patientId: number) {
try {
const filename = path.basename(new URL(pdf_url).pathname);
const seleniumPort = process.env.SELENIUM_PORT || "5002";
const localUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
const resp = await axios.get(localUrl, { responseType: "arraybuffer", timeout: 30000 });
let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
if (!group) {
group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM");
}
await storage.createPdfFile(group.id!, filename, resp.data);
log("uniteddh-claim-processor", "PDF saved", { patientId, filename });
} catch (err: any) {
log("uniteddh-claim-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err });
}
}
export interface UnitedDHClaimProcessorInput {
enrichedPayload: any;
userId: number;
@@ -149,6 +169,13 @@ export async function runUnitedDHClaimProcessor(
}
}
// Auto-save PDF for batch-column calls (no socketId = no frontend listener)
if (pdf_url && !socketId) {
const claim = claimId ? await storage.getClaim(claimId).catch(() => null) : null;
const patientId = claim?.patientId ?? enrichedPayload?.claim?.patientId ?? enrichedPayload?.patientId;
if (patientId) await savePdfFromSelenium(pdf_url, Number(patientId));
}
emitToSocket(socketId, "selenium:uniteddh_claim_completed", {
jobId,
claimId,

View File

@@ -104,13 +104,21 @@ async function processUnitedSCOResult(
// 4) Determine eligibility status
const eligStatus = (seleniumResult?.eligibility ?? "").toLowerCase();
const newStatus = eligStatus === "active" || eligStatus === "y" ? "ACTIVE" : "INACTIVE";
const newStatus =
eligStatus === "active" || eligStatus === "y" ? "ACTIVE" :
eligStatus === "inactive" ? "INACTIVE" : null;
await storage.updatePatient(patient.id, {
status: newStatus,
insuranceProvider: "United Healthcare SCO",
});
output.patientUpdateStatus = `Patient status updated to ${newStatus}`;
if (newStatus) {
await storage.updatePatient(patient.id, {
status: newStatus,
insuranceProvider: "United Healthcare SCO",
});
output.patientUpdateStatus = `Patient status updated to ${newStatus}`;
} else {
// Unknown eligibility — still update insuranceProvider but leave status unchanged
await storage.updatePatient(patient.id, { insuranceProvider: "United Healthcare SCO" });
output.patientUpdateStatus = `Eligibility unknown — status not changed`;
}
// 5) Resolve PDF buffer from file path (same as DDMA)
let pdfBuffer: Buffer | null = null;

View File

@@ -14,6 +14,8 @@ export type SeleniumJobType =
| "cca-claim-submit"
| "cca-preauth-submit"
| "ddma-claim-submit"
| "tuftssco-claim-submit"
| "uniteddh-claim-submit"
| "tuftssco-eligibility-check"
| "mh-eligibility-history-check"
| "cmsp-eligibility-history-remaining-check";