/** * Processor for "uniteddh-preauth-submit" jobs. * Submits a dental pre-authorization to the United/DentalHub portal via Selenium. * * Flow: * 1. POST /uniteddh-preauth to Python agent → get session_id * 2. Emit selenium:uniteddh_preauth_started to frontend * 3. Poll until completed/error (emitting otp_required as needed) * 4. Save PDF + preAuthNumber, emit result */ import { forwardToSeleniumUnitedDHPreAuthAgent, getSeleniumUnitedDHPreAuthSessionStatus, } from "../../services/seleniumUnitedDHPreAuthClient"; 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 ?? ""); } function emitToSocket(socketId: string | undefined, event: string, payload: any) { if (!socketId || !io) return; try { const socket = io.sockets.sockets.get(socketId); if (socket) socket.emit(event, payload); } catch (_) {} } async function pollUntilDone( sessionId: string, socketId: string | undefined, jobId: string, pollTimeoutMs = 5 * 60 * 1000 ): Promise { const maxAttempts = 600; const pollIntervalMs = 500; const maxTransientErrors = 12; const noProgressLimit = 240; let transientErrors = 0; let consecutiveNoProgress = 0; let lastStatus: string | null = null; const deadline = Date.now() + pollTimeoutMs; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (Date.now() > deadline) { throw new Error(`UnitedDH preauth polling timeout for session ${sessionId}`); } try { const st = await getSeleniumUnitedDHPreAuthSessionStatus(sessionId); const status: string = st?.status ?? "unknown"; log("uniteddh-preauth-processor", `poll attempt=${attempt}`, { sessionId, status }); transientErrors = 0; const isTerminal = status === "completed" || status === "error" || status === "not_found"; if (status === lastStatus && !isTerminal) { consecutiveNoProgress++; } else { consecutiveNoProgress = 0; } lastStatus = status; if (consecutiveNoProgress >= noProgressLimit) { throw new Error(`No progress from Python agent (status="${status}") after ${consecutiveNoProgress} polls`); } if (status === "waiting_for_otp") { emitToSocket(socketId, "selenium:otp_required", { session_id: sessionId, jobId, message: "OTP required. Please enter the OTP shown by the DentalHub portal.", }); await new Promise((r) => setTimeout(r, pollIntervalMs)); continue; } if (status === "completed") return st.result; if (status === "error" || status === "not_found") { throw new Error(st?.message || `UnitedDH preauth session ended with status: ${status}`); } await new Promise((r) => setTimeout(r, pollIntervalMs)); } catch (err: any) { const isTerminal = err?.response?.status === 404 || (typeof err?.message === "string" && (err.message.includes("not_found") || err.message.includes("polling timeout"))); if (isTerminal) throw err; transientErrors++; if (transientErrors > maxTransientErrors) { throw new Error(`Too many transient errors polling UnitedDH preauth session ${sessionId}`); } const backoff = Math.min(30_000, 500 * Math.pow(2, transientErrors - 1)); await new Promise((r) => setTimeout(r, backoff)); } } throw new Error(`UnitedDH preauth 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_PREAUTH"); if (!group) { group = await storage.createPdfGroup(patientId, "PreAuth", "INSURANCE_CLAIM_PREAUTH"); } await storage.createPdfFile(group.id!, filename, resp.data); log("uniteddh-preauth-processor", "PDF saved", { patientId, filename }); } catch (err: any) { log("uniteddh-preauth-processor", "failed to save PDF (non-fatal)", { error: err?.message ?? err }); } } export interface UnitedDHPreAuthProcessorInput { enrichedPayload: any; userId: number; claimId?: number; socketId?: string; } export async function runUnitedDHPreAuthProcessor( input: UnitedDHPreAuthProcessorInput, jobId: string ): Promise<{ status: string; pdf_url?: string; preAuthNumber?: string }> { const { enrichedPayload, userId, claimId, socketId } = input; log("uniteddh-preauth-processor", "starting Python agent session", { claimId }); const agentResp = await forwardToSeleniumUnitedDHPreAuthAgent(enrichedPayload); if (!agentResp?.session_id) { throw new Error("Python agent did not return a session_id for UnitedDH preauth"); } const sessionId = agentResp.session_id as string; log("uniteddh-preauth-processor", "got session_id", { sessionId }); emitToSocket(socketId, "selenium:uniteddh_preauth_started", { session_id: sessionId, jobId }); const seleniumResult = await pollUntilDone(sessionId, socketId, jobId); if (!seleniumResult || seleniumResult.status === "error") { throw new Error(seleniumResult?.message ?? "UnitedDH preauth session returned an error"); } const preAuthNumber: string | undefined = seleniumResult.preAuthNumber ?? undefined; const pdf_url: string | undefined = seleniumResult.pdf_url ?? seleniumResult.pdf_path ?? undefined; if (claimId) { try { const updates: Record = { status: "PREAUTH" }; if (preAuthNumber) updates.preAuthNumber = preAuthNumber; await storage.updateClaim(claimId, updates); log("uniteddh-preauth-processor", "claim record updated", { claimId, preAuthNumber }); const claim = await storage.getClaim(claimId); if (claim?.patientId) { await storage.touchPatient(claim.patientId); log("uniteddh-preauth-processor", "patient touched", { patientId: claim.patientId }); } } catch (e) { log("uniteddh-preauth-processor", "failed to update claim record (non-fatal)", { error: e }); } } // Auto-save PDF when called without a frontend socket 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_preauth_completed", { jobId, claimId, preAuthNumber, pdf_url, message: preAuthNumber ? `United/DentalHub pre-authorization submitted — PreAuth #: ${preAuthNumber}` : (seleniumResult?.message ?? "United/DentalHub pre-authorization submitted successfully"), }); log("uniteddh-preauth-processor", "done", { claimId, preAuthNumber }); return { status: "success", pdf_url, preAuthNumber }; }