fix: MH eligibility selenium integration and patient data extraction

This commit is contained in:
ff
2026-04-14 22:10:19 -04:00
parent f415100cdb
commit 714029df24
12 changed files with 6677 additions and 98 deletions

View File

@@ -18,50 +18,30 @@ const SELENIUM_BASE_URL =
// Python service helpers
// ---------------------------------------------------------------------------
/** Start an async job on the Python service and return the session ID. */
export async function startPythonJob(
/**
* Call a synchronous Python service endpoint and return its result.
* The Python service runs the selenium job inline and returns the result directly.
* Timeout is long (8 min) to accommodate slow selenium flows.
*/
export async function callPythonSync(
endpoint: string,
payload: any
): Promise<string> {
payload: any,
timeoutMs = 8 * 60 * 1000
): Promise<any> {
const resp = await axios.post(
`${SELENIUM_BASE_URL}${endpoint}`,
payload,
{ timeout: 10_000 }
{ timeout: timeoutMs }
);
const sid: string = resp.data?.session_id;
if (!sid) throw new Error(`Python service did not return a session_id from ${endpoint}`);
return sid;
}
/** Poll /job/<sid>/status until completed/failed or timeout. */
export async function pollPythonJob(
sid: string,
timeoutMs = 5 * 60 * 1000,
intervalMs = 2_000
): Promise<any> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const resp = await axios.get(
`${SELENIUM_BASE_URL}/job/${sid}/status`,
{ timeout: 5_000 }
);
const s = resp.data;
if (s.status === "completed") {
if (s.result?.status === "error") {
const msg =
typeof s.result.message === "string"
? s.result.message
: s.result.message?.msg ?? "Selenium returned error status";
throw new Error(msg);
}
return s.result;
}
if (s.status === "failed") {
throw new Error(s.error || "Python job failed");
}
await sleep(intervalMs);
const data = resp.data;
if (data?.status === "error") {
const msg =
typeof data.message === "string"
? data.message
: data.message?.msg ?? "Selenium returned error status";
throw new Error(msg);
}
throw new Error("Selenium job timed out after polling");
return data;
}
// ---------------------------------------------------------------------------

View File

@@ -8,8 +8,7 @@ import path from "path";
import { storage } from "../../storage";
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
import {
startPythonJob,
pollPythonJob,
callPythonSync,
imageToPdfBuffer,
} from "./_shared";
@@ -28,14 +27,11 @@ export async function runClaimStatusProcessor(
): Promise<ClaimStatusProcessorResult> {
const { enrichedPayload, insuranceId } = input;
// 1) Start async Python job
const sid = await startPythonJob("/claim-status-check/async", {
// 1) Call the Python service synchronously (BullMQ worker handles async)
const result = await callPythonSync("/claim-status-check", {
data: enrichedPayload,
});
// 2) Poll for completion
const result = await pollPythonJob(sid);
const outputResult: ClaimStatusProcessorResult = {};
// 3) Look up patient

View File

@@ -3,7 +3,7 @@
* Mirrors routes/claims.ts /selenium-claim and /selenium-claim-pre-auth
*/
import { storage } from "../../storage";
import { startPythonJob, pollPythonJob } from "./_shared";
import { callPythonSync } from "./_shared";
export interface ClaimSubmitProcessorInput {
enrichedPayload: any;
@@ -37,13 +37,10 @@ export async function runClaimSubmitProcessor(
const payload = { claim: enrichedPayload, pdfs, images };
const endpoint =
input.variant === "claim-pre-auth" ? "/claim-pre-auth/async" : "/claimsubmit/async";
input.variant === "claim-pre-auth" ? "/claim-pre-auth" : "/claimsubmit";
// 1) Start async Python job
const sid = await startPythonJob(endpoint, payload);
// 2) Poll for result
const result = await pollPythonJob(sid, 10 * 60 * 1000); // claim submit can take up to 10 min
// 1) Call the Python service synchronously (BullMQ worker handles async)
const result = await callPythonSync(endpoint, payload, 10 * 60 * 1000);
// 3) Persist claimNumber if returned
if (result?.claimNumber && claimId) {

View File

@@ -11,8 +11,7 @@ import { storage } from "../../storage";
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
import forwardToPatientDataExtractorService from "../../services/patientDataExtractorService";
import {
startPythonJob,
pollPythonJob,
callPythonSync,
splitName,
createOrUpdatePatientByInsuranceId,
} from "./_shared";
@@ -45,26 +44,29 @@ export async function runEligibilityProcessor(
formDob,
} = input;
// 1) Fire the async Python job
const sid = await startPythonJob("/eligibility-check/async", {
// 1) Call the Python service synchronously (BullMQ worker handles async)
const seleniumResult = await callPythonSync("/eligibility-check", {
data: enrichedPayload,
});
// 2) Wait for completion
const seleniumResult = await pollPythonJob(sid);
const outputResult: EligibilityProcessorResult = {};
// 3) Extract name: prefer selenium extraction → PDF extractor → form input
// Normalize: treat empty strings from Python the same as null
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 (seleniumResult.firstName || seleniumResult.lastName) {
extracted.firstName = seleniumResult.firstName ?? null;
extracted.lastName = seleniumResult.lastName ?? null;
} else if (seleniumResult.name) {
const parts = splitName(seleniumResult.name);
extracted.firstName = parts.firstName;
extracted.lastName = parts.lastName;
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 (
@@ -81,14 +83,15 @@ export async function runEligibilityProcessor(
} as any);
if (extraction.name) {
const parts = splitName(extraction.name);
extracted.firstName = parts.firstName;
extracted.lastName = parts.lastName;
extracted.firstName = parts.firstName || null;
extracted.lastName = parts.lastName || null;
}
} catch (e) {
console.error("[eligibilityProcessor] PDF name extraction failed:", e);
}
}
// Final fallback: use form data sent by the frontend
const preferFirst = extracted.firstName || formFirstName || null;
const preferLast = extracted.lastName || formLastName || null;
@@ -113,7 +116,7 @@ export async function runEligibilityProcessor(
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE";
const updates: any = { status: newStatus };
if (seleniumResult.insurance) updates.insuranceProvider = seleniumResult.insurance;
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;