feat: DentaQuest eligibility — PLAN_NOT_ACCEPTED status, DOB save, no retries

- Add PLAN_NOT_ACCEPTED to PatientStatus enum (prisma schema + db push)
- Selenium: return "plan not accepted" eligibility text instead of collapsing to inactive
- Backend processor: map "plan not accepted" → PLAN_NOT_ACCEPTED, fix insuranceProvider label
- _shared.ts: save DOB for existing patients when field is currently empty
- Frontend: show amber "Plan Not Accepted" badge in patient table and detail panel
- patient-form.tsx: display "Plan Not Accepted" label in status dropdown
- BullMQ: set attempts=1 (no retry on selenium failure)
- DDMA: remove first/last name from search (member ID + DOB only)
- patient-types.ts: allow alphanumeric insurance IDs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-04-17 23:47:50 -04:00
parent f5ec4a1480
commit 4505d5db85
12 changed files with 141 additions and 104 deletions

View File

@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"dev": "ts-node-dev --respawn --transpile-only --watch ../../packages/db/types src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},

View File

@@ -105,6 +105,10 @@ export async function createOrUpdatePatientByInsuranceId(options: {
updates.firstName = incomingFirst;
if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast)
updates.lastName = incomingLast;
if (dob && !patient.dateOfBirth) {
const parsed = new Date(dob);
if (!isNaN(parsed.getTime())) updates.dateOfBirth = parsed;
}
if (Object.keys(updates).length > 0) {
await storage.updatePatient(patient.id, updates);
patient = await storage.getPatientByInsuranceId(insuranceId);
@@ -126,9 +130,15 @@ export async function createOrUpdatePatientByInsuranceId(options: {
try {
patientData = insertPatientSchema.parse(createPayload);
} catch {
// Remove fields that may fail validation (invalid date or alphanumeric insuranceId)
const safePayload = { ...createPayload };
delete safePayload.dateOfBirth;
patientData = insertPatientSchema.parse(safePayload);
try {
patientData = insertPatientSchema.parse(safePayload);
} catch {
// Last resort: skip schema validation and cast directly
patientData = safePayload as InsertPatient;
}
}
await storage.createPatient(patientData);

View File

@@ -84,11 +84,16 @@ async function processDentaQuestResult(
}
const eligStatus = (seleniumResult?.eligibility ?? "").toLowerCase();
const newStatus = eligStatus === "active" || eligStatus === "y" ? "ACTIVE" : "INACTIVE";
const newStatus =
eligStatus === "active" || eligStatus === "y"
? "ACTIVE"
: eligStatus.includes("plan not accepted") || eligStatus.includes("plan_not_accepted")
? "PLAN_NOT_ACCEPTED"
: "INACTIVE";
await storage.updatePatient(patient.id, {
status: newStatus,
insuranceProvider: "Tufts SCO",
insuranceProvider: "DentaQuest",
});
output.patientUpdateStatus = `Patient status updated to ${newStatus}`;
@@ -223,12 +228,15 @@ async function pollUntilDone(
}
if (status === "error" || status === "not_found") {
throw new Error(st?.message || `DentaQuest session ended with status: ${status}`);
const terminalErr: any = new Error(st?.message || `DentaQuest session ended with status: ${status}`);
terminalErr.terminal = true;
throw terminalErr;
}
await new Promise((r) => setTimeout(r, pollIntervalMs));
} catch (err: any) {
const isTerminal =
err?.terminal === true ||
err?.response?.status === 404 ||
(typeof err?.message === "string" &&
(err.message.includes("not_found") ||

View File

@@ -37,7 +37,7 @@ export interface OcrJobData {
const defaultOpts = {
removeOnComplete: { count: 100 },
removeOnFail: { count: 50 },
attempts: 2,
attempts: 1,
backoff: { type: "exponential" as const, delay: 5000 },
};