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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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}`;