Eligibility (no OTP, fresh browser per run like MH): logs in, opens Benefits & Claims Information, searches by Subscriber ID (or Subscriber Last Name via "Click Here"), First Name and DOB, picks the matching row from the patient list, sets ACTIVE/INACTIVE from the Coverage range, reads Group Number into the patient's Group Number, opens View Details for the last name and plan name, and saves the details page PDF to the patient's Eligibility folder. A patient switching plans is matched by name + DOB so Altus overwrites the old insurance instead of creating a duplicate. Claims: Altus uses the same 2024 ADA form as BCBS with the Altus payer address (P.O. Box 1557, Providence, RI 02901-1557). Adds Altus Claim/PreAuth (ADA Form) buttons, Claim All and AI queue routing, Print ADA Form in the claim viewer, and an Altus fee schedule (copied from BCBS). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
/**
|
|
* jobRunner — the single source of truth for enqueueing async jobs.
|
|
*
|
|
* Uses InProcessQueue (no Redis) instead of BullMQ.
|
|
* When a job finishes the worker emits a `job:update` Socket.IO event
|
|
* to the originating client so the frontend can react in real time.
|
|
*/
|
|
import { InProcessQueue } from "./inProcessQueue";
|
|
import { io } from "../socket";
|
|
import { runEligibilityProcessor } from "./processors/eligibilityProcessor";
|
|
import { runClaimStatusProcessor } from "./processors/claimStatusProcessor";
|
|
import { runClaimSubmitProcessor } from "./processors/claimSubmitProcessor";
|
|
import { runOcrProcessor } from "./processors/ocrProcessor";
|
|
import { runDdmaEligibilityProcessor } from "./processors/ddmaEligibilityProcessor";
|
|
import { runDeltaIndivEligibilityProcessor } from "./processors/deltaIndivEligibilityProcessor";
|
|
import { runDeltaInsEligibilityProcessor } from "./processors/deltaInsEligibilityProcessor";
|
|
import { runUnitedSCOEligibilityProcessor } from "./processors/unitedSCOEligibilityProcessor";
|
|
import { runUnitedUcardEligibilityProcessor } from "./processors/unitedUcardEligibilityProcessor";
|
|
import { runDentaQuestEligibilityProcessor } from "./processors/dentaQuestEligibilityProcessor";
|
|
import { runCCAEligibilityProcessor } from "./processors/ccaEligibilityProcessor";
|
|
import { runCCAClaimProcessor } from "./processors/ccaClaimProcessor";
|
|
import { runCCAPreAuthProcessor } from "./processors/ccaPreAuthProcessor";
|
|
import { runDDMAClaimProcessor } from "./processors/ddmaClaimProcessor";
|
|
import { runDeltainsClaimProcessor } from "./processors/deltainsClaimProcessor";
|
|
import { runUnitedDHClaimProcessor } from "./processors/unitedDHClaimProcessor";
|
|
import { runUnitedDHPreAuthProcessor } from "./processors/unitedDHPreAuthProcessor";
|
|
import { runTuftsSCOPreAuthProcessor } from "./processors/tuftsSCOPreAuthProcessor";
|
|
import { runTuftsSCOClaimProcessor } from "./processors/tuftsSCOClaimProcessor";
|
|
import { runUnitedUcardClaimProcessor } from "./processors/unitedUcardClaimProcessor";
|
|
import { runEligibilityHistoryProcessor } from "./processors/eligibilityHistoryProcessor";
|
|
import { runCmspEligibilityHistoryRemainingProcessor } from "./processors/cmspEligibilityHistoryRemainingProcessor";
|
|
import { runBcbsMaEligibilityProcessor } from "./processors/bcbsMaEligibilityProcessor";
|
|
import { runMetLifeEligibilityProcessor } from "./processors/metlifeEligibilityProcessor";
|
|
import { runAltusEligibilityProcessor } from "./processors/altusEligibilityProcessor";
|
|
import type { SeleniumJobData, OcrJobData } from "./queues";
|
|
|
|
// ── Queue instances ──────────────────────────────────────────────────────────
|
|
// Selenium: 1 browser at a time (mirrors Python semaphore)
|
|
const seleniumQ = new InProcessQueue<SeleniumJobData>(1);
|
|
// OCR: allow 2 concurrent (mirrors Python MAX_CONCURRENCY=2)
|
|
const ocrQ = new InProcessQueue<OcrJobData>(2);
|
|
|
|
// ── WebSocket helper ─────────────────────────────────────────────────────────
|
|
function emitJobUpdate(
|
|
socketId: string | undefined,
|
|
jobId: string,
|
|
jobType: string,
|
|
status: "active" | "completed" | "failed",
|
|
extra: Record<string, any> = {}
|
|
) {
|
|
const payload = { jobId, jobType, status, ...extra };
|
|
if (socketId && io) {
|
|
io.to(socketId).emit("job:update", payload);
|
|
} else if (io) {
|
|
io.emit("job:update", payload);
|
|
}
|
|
}
|
|
|
|
// ── Selenium enqueue ─────────────────────────────────────────────────────────
|
|
export function enqueueSeleniumJob(data: SeleniumJobData): string {
|
|
const { jobType, socketId } = data;
|
|
|
|
const jobId = seleniumQ.add(data, async (job) => {
|
|
emitJobUpdate(socketId, job.id, jobType, "active", {
|
|
message: "Selenium browser starting…",
|
|
});
|
|
|
|
if (jobType === "eligibility-check") {
|
|
return runEligibilityProcessor({
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
});
|
|
}
|
|
if (jobType === "claim-status-check") {
|
|
return runClaimStatusProcessor({
|
|
enrichedPayload: data.enrichedPayload,
|
|
insuranceId: data.insuranceId!,
|
|
userId: data.userId,
|
|
});
|
|
}
|
|
if (jobType === "claim-submit" || jobType === "claim-pre-auth") {
|
|
return runClaimSubmitProcessor({
|
|
enrichedPayload: data.enrichedPayload,
|
|
files: data.files ?? [],
|
|
claimId: data.claimId,
|
|
variant: jobType === "claim-pre-auth" ? "claim-pre-auth" : "claimsubmit",
|
|
socketId: data.socketId,
|
|
userId: data.userId,
|
|
});
|
|
}
|
|
if (jobType === "ddma-eligibility-check") {
|
|
return runDdmaEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "metlife-eligibility-check") {
|
|
return runMetLifeEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "deltains-eligibility-check") {
|
|
return runDeltaInsEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "deltaindiv-eligibility-check") {
|
|
return runDeltaIndivEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "unitedsco-eligibility-check") {
|
|
return runUnitedSCOEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "uniteducard-eligibility-check") {
|
|
return runUnitedUcardEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "tuftssco-eligibility-check") {
|
|
return runDentaQuestEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "cca-claim-submit") {
|
|
return runCCAClaimProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "cca-preauth-submit") {
|
|
return runCCAPreAuthProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "ddma-claim-submit") {
|
|
return runDDMAClaimProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "deltains-claim-submit") {
|
|
return runDeltainsClaimProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "uniteddh-claim-submit") {
|
|
return runUnitedDHClaimProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "uniteddh-preauth-submit") {
|
|
return runUnitedDHPreAuthProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "tuftssco-preauth-submit") {
|
|
return runTuftsSCOPreAuthProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "tuftssco-claim-submit") {
|
|
return runTuftsSCOClaimProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "uniteducard-claim-submit") {
|
|
return runUnitedUcardClaimProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
claimId: data.claimId,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "cca-eligibility-check") {
|
|
return runCCAEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "cmsp-eligibility-history-remaining-check") {
|
|
return runCmspEligibilityHistoryRemainingProcessor({
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
});
|
|
}
|
|
if (jobType === "mh-eligibility-history-check") {
|
|
return runEligibilityHistoryProcessor({
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
});
|
|
}
|
|
if (jobType === "bcbs-ma-eligibility-check") {
|
|
return runBcbsMaEligibilityProcessor(
|
|
{
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
socketId: data.socketId,
|
|
},
|
|
job.id
|
|
);
|
|
}
|
|
if (jobType === "altus-eligibility-check") {
|
|
return runAltusEligibilityProcessor({
|
|
enrichedPayload: data.enrichedPayload,
|
|
userId: data.userId,
|
|
insuranceId: data.insuranceId!,
|
|
formFirstName: data.formFirstName,
|
|
formLastName: data.formLastName,
|
|
formDob: data.formDob,
|
|
});
|
|
}
|
|
throw new Error(`Unknown selenium jobType: ${jobType}`);
|
|
});
|
|
|
|
// Attach completion/failure callbacks after the job is in the queue.
|
|
// We poll the job object once per tick until it settles.
|
|
(async () => {
|
|
while (true) {
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
const job = seleniumQ.getJob(jobId);
|
|
if (!job) break;
|
|
if (job.status === "completed") {
|
|
emitJobUpdate(socketId, jobId, jobType, "completed", {
|
|
result: job.result,
|
|
});
|
|
console.log(`[seleniumQ] job ${jobId} (${jobType}) completed`);
|
|
break;
|
|
}
|
|
if (job.status === "failed") {
|
|
emitJobUpdate(socketId, jobId, jobType, "failed", {
|
|
error: job.error,
|
|
});
|
|
console.error(`[seleniumQ] job ${jobId} (${jobType}) failed:`, job.error);
|
|
break;
|
|
}
|
|
}
|
|
})();
|
|
|
|
return jobId;
|
|
}
|
|
|
|
// ── Job status query (HTTP polling fallback) ─────────────────────────────────
|
|
export function getSeleniumJobStatus(jobId: string) {
|
|
const job = seleniumQ.getJob(jobId);
|
|
if (!job) return null;
|
|
return { status: job.status, result: job.result ?? null, error: job.error ?? null };
|
|
}
|
|
|
|
// ── OCR enqueue ──────────────────────────────────────────────────────────────
|
|
export function enqueueOcrJob(data: OcrJobData): string {
|
|
const { socketId } = data;
|
|
|
|
const jobId = ocrQ.add(data, async () => {
|
|
emitJobUpdate(socketId, jobId, "ocr", "active", {
|
|
message: "OCR processing started…",
|
|
});
|
|
return runOcrProcessor({ files: data.files });
|
|
});
|
|
|
|
(async () => {
|
|
while (true) {
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
const job = ocrQ.getJob(jobId);
|
|
if (!job) break;
|
|
if (job.status === "completed") {
|
|
emitJobUpdate(socketId, jobId, "ocr", "completed", {
|
|
result: { rows: job.result },
|
|
});
|
|
console.log(`[ocrQ] job ${jobId} completed`);
|
|
break;
|
|
}
|
|
if (job.status === "failed") {
|
|
emitJobUpdate(socketId, jobId, "ocr", "failed", { error: job.error });
|
|
console.error(`[ocrQ] job ${jobId} failed:`, job.error);
|
|
break;
|
|
}
|
|
}
|
|
})();
|
|
|
|
return jobId;
|
|
}
|