/** * Processor for "cca-claim-submit" jobs. * Submits a dental claim to CCA via the ScionDental portal. * * Flow: * 1. POST /cca-claim to Python agent → get session_id * 2. Emit selenium:cca_claim_started to frontend * 3. Poll until completed/error * 4. Emit result and update claim status in DB */ import { storage } from "../../storage"; import { forwardToSeleniumCCAClaimAgent, getSeleniumCCAClaimSessionStatus, } from "../../services/seleniumCCAClaimClient"; import { io } from "../../socket"; 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, pollTimeoutMs = 10 * 60 * 1000 ): Promise { const maxAttempts = 1200; const pollIntervalMs = 500; const maxTransientErrors = 12; let transientErrors = 0; const deadline = Date.now() + pollTimeoutMs; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (Date.now() > deadline) { throw new Error(`CCA claim polling timeout for session ${sessionId}`); } try { const st = await getSeleniumCCAClaimSessionStatus(sessionId); const status: string = st?.status ?? "unknown"; log("cca-claim-processor", `poll attempt=${attempt}`, { sessionId, status }); transientErrors = 0; if (status === "completed") return st.result; if (status === "error" || status === "not_found") { throw new Error(st?.message || `CCA claim 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 CCA claim session ${sessionId}`); } const backoff = Math.min(30_000, 500 * Math.pow(2, transientErrors - 1)); await new Promise((r) => setTimeout(r, backoff)); } } throw new Error(`CCA claim polling exhausted all attempts for session ${sessionId}`); } export interface CCAClaimProcessorInput { enrichedPayload: any; userId: number; claimId?: number; socketId?: string; } export async function runCCAClaimProcessor( input: CCAClaimProcessorInput, jobId: string ): Promise<{ status: string; claimNumber?: string | null; pdfFileId?: number | null }> { const { enrichedPayload, userId, claimId, socketId } = input; log("cca-claim-processor", "starting Python agent session", { claimId }); const agentResp = await forwardToSeleniumCCAClaimAgent(enrichedPayload); if (!agentResp?.session_id) { throw new Error("Python agent did not return a session_id for CCA claim"); } const sessionId = agentResp.session_id as string; log("cca-claim-processor", "got session_id", { sessionId }); emitToSocket(socketId, "selenium:cca_claim_started", { session_id: sessionId, jobId }); const seleniumResult = await pollUntilDone(sessionId); if (!seleniumResult || seleniumResult.status === "error") { throw new Error(seleniumResult?.message ?? "CCA claim session returned an error"); } const claimNumber: string | null = seleniumResult?.claimNumber ?? null; const pdfBase64: string = seleniumResult?.pdfBase64 ?? ""; const pdfFilename: string = seleniumResult?.pdfFilename || `cca_claim_${claimId ?? "unknown"}_${Date.now()}.pdf`; // Save PDF to patient's Claims document group let pdfFileId: number | null = null; if (pdfBase64 && enrichedPayload?.claim?.patientId) { try { const patientId = Number(enrichedPayload.claim.patientId); const pdfBuffer = Buffer.from(pdfBase64, "base64"); let group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); if (!group) { group = await storage.createPdfGroup(patientId, "Claims", "INSURANCE_CLAIM"); } const created = await storage.createPdfFile(group.id!, pdfFilename, pdfBuffer); if (created && typeof created === "object" && "id" in created) { pdfFileId = Number((created as any).id); } log("cca-claim-processor", "PDF saved", { pdfFilename, pdfFileId, patientId }); } catch (e: any) { log("cca-claim-processor", "failed to save PDF", { error: e?.message }); } } // Update claim: status → REVIEW, persist claimNumber if (claimId) { try { const updates: Record = { status: "REVIEW" }; if (claimNumber) updates.claimNumber = claimNumber; await storage.updateClaim(claimId, updates); log("cca-claim-processor", "claim updated", { claimId, claimNumber, status: "REVIEW" }); } catch (e: any) { log("cca-claim-processor", "failed to update claim", { error: e?.message }); } } emitToSocket(socketId, "selenium:cca_claim_completed", { jobId, claimId, claimNumber, pdfFileId, pdfFilename, message: claimNumber ? `CCA claim submitted — claim number: ${claimNumber}` : "CCA claim submitted successfully", }); log("cca-claim-processor", "done", { claimId, claimNumber }); return { status: "success", claimNumber, pdfFileId }; }