feat: add United Ucard claim worker and fix batch-column site-key routing
Adds a "United Ucard Claim" button (claims/preauth insurance form) that submits through a full Selenium pipeline mirroring the Tufts SCO claim worker: frontend handler -> backend route/queue processor -> Python agent, reusing the persistent login/OTP session from the existing United Ucard eligibility check. Implements the UHC Dental claim wizard end-to-end (Provider Information, Patient Information search/select, Claim Information procedures + questions, Upload Documents, Review and Sign, Submit); the final confirmation-page PDF/claim-number capture is still a TODO stub pending that page's DOM. Also wires United Ucard into the AI chatbot's eligibility flow (insuranceAliases.json, siteKeyToAutoCheck, clarification options), and fixes a bug where the AI-column batch claim feature silently defaulted United Ucard patients to MassHealth because batchColumnDeriveSiteKey had no matching branch and no job-dispatch case for UNITED_UCARD. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -590,6 +590,7 @@ function batchColumnDeriveSiteKey(provider: string): string {
|
||||
if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
|
||||
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TUFTS_SCO";
|
||||
if ((p.includes("united") && p.includes("sco")) || p.includes("dentalhub") || p === "united_sco") return "UNITED_SCO";
|
||||
if (p.includes("ucard") || p.includes("onehealthcareid") || p.includes("one healthcare id")) return "UNITED_UCARD";
|
||||
return "MH"; // default fallback
|
||||
}
|
||||
|
||||
@@ -600,6 +601,7 @@ function batchColumnCanonicalName(siteKey: string): string {
|
||||
case "DDMA": return "Delta Dental MA";
|
||||
case "TUFTS_SCO": return "Tufts SCO";
|
||||
case "UNITED_SCO": return "United/DentalHub";
|
||||
case "UNITED_UCARD": return "United Ucard";
|
||||
default: return "MassHealth";
|
||||
}
|
||||
}
|
||||
@@ -979,6 +981,24 @@ router.post(
|
||||
enrichedPayload,
|
||||
claimId,
|
||||
});
|
||||
} else if (siteKey === "UNITED_UCARD") {
|
||||
const enrichedPayload = {
|
||||
claim: {
|
||||
...baseClaimPayload,
|
||||
insuranceProvider: "United Ucard",
|
||||
insuranceSiteKey: "UNITED_UCARD",
|
||||
uniteducardUsername: credentials.username,
|
||||
uniteducardPassword: credentials.password,
|
||||
providerName: resolvedNpiProvider?.providerName ?? "",
|
||||
claimFiles: diskClaimFiles,
|
||||
},
|
||||
};
|
||||
jobId = enqueueSeleniumJob({
|
||||
jobType: "uniteducard-claim-submit",
|
||||
userId: req.user.id,
|
||||
enrichedPayload,
|
||||
claimId,
|
||||
});
|
||||
} else {
|
||||
resultItem.error = `Unsupported insurance type: ${siteKey}`;
|
||||
results.push(resultItem);
|
||||
|
||||
@@ -24,6 +24,7 @@ import insuranceStatusUnitedDHClaimRoutes from "./insuranceStatusUnitedDHClaim";
|
||||
import insuranceStatusUnitedDHPreAuthRoutes from "./insuranceStatusUnitedDHPreAuth";
|
||||
import insuranceStatusTuftsSCOClaimRoutes from "./insuranceStatusTuftsSCOClaim";
|
||||
import insuranceStatusTuftsSCOPreAuthRoutes from "./insuranceStatusTuftsSCOPreAuth";
|
||||
import insuranceStatusUnitedUcardClaimRoutes from "./insuranceStatusUnitedUcardClaim";
|
||||
import paymentsRoutes from "./payments";
|
||||
import databaseManagementRoutes from "./database-management";
|
||||
import notificationsRoutes from "./notifications";
|
||||
@@ -78,6 +79,7 @@ router.use("/claims", insuranceStatusUnitedDHClaimRoutes);
|
||||
router.use("/claims", insuranceStatusUnitedDHPreAuthRoutes);
|
||||
router.use("/claims", insuranceStatusTuftsSCOClaimRoutes);
|
||||
router.use("/claims", insuranceStatusTuftsSCOPreAuthRoutes);
|
||||
router.use("/claims", insuranceStatusUnitedUcardClaimRoutes);
|
||||
router.use("/payments", paymentsRoutes);
|
||||
router.use("/database-management", databaseManagementRoutes);
|
||||
router.use("/notifications", notificationsRoutes);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { storage } from "../storage";
|
||||
import { enqueueSeleniumJob } from "../queue/jobRunner";
|
||||
import { forwardOtpToSeleniumUnitedUcardClaimAgent } from "../services/seleniumUnitedUcardClaimClient";
|
||||
import { io } from "../socket";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /uniteducard-claim
|
||||
*
|
||||
* Enqueues a United Ucard (UHC Dental) claim submission job.
|
||||
*
|
||||
* Body fields (JSON):
|
||||
* data — claim payload (memberId, dateOfBirth, serviceDate, serviceLines, patientName, etc.)
|
||||
* socketId — socket.io client id
|
||||
* claimId — existing claim DB id (optional)
|
||||
*
|
||||
* Response: { status: "queued", jobId: "…" }
|
||||
*/
|
||||
router.post("/uniteducard-claim", async (req: Request, res: Response): Promise<any> => {
|
||||
if (!req.user?.id) {
|
||||
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||
}
|
||||
|
||||
try {
|
||||
const claimData =
|
||||
typeof req.body.data === "string"
|
||||
? JSON.parse(req.body.data)
|
||||
: req.body.data ?? req.body ?? {};
|
||||
|
||||
// Fetch United Ucard (UHC Dental / OneHealthcareID) credentials
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(
|
||||
req.user.id,
|
||||
"UNITED_UCARD"
|
||||
);
|
||||
if (!credentials) {
|
||||
return res.status(404).json({
|
||||
error: "No United Ucard credentials found. Please add them on the Settings page.",
|
||||
});
|
||||
}
|
||||
|
||||
const seleniumSettings = await db.seleniumSettings.findUnique({ where: { userId: req.user.id } });
|
||||
|
||||
// Default provider (sorted by sortOrder asc, then id asc) — used for the
|
||||
// provider signature field on the Review and Sign step.
|
||||
const npiProviders = await storage.getNpiProvidersByUser(req.user.id);
|
||||
const primaryProvider = npiProviders[0];
|
||||
|
||||
const enrichedPayload = {
|
||||
claim: {
|
||||
...claimData,
|
||||
uniteducardUsername: credentials.username,
|
||||
uniteducardPassword: credentials.password,
|
||||
paymentGroupId: seleniumSettings?.paymentGroupId ?? "",
|
||||
providerName: primaryProvider?.providerName ?? "",
|
||||
},
|
||||
};
|
||||
|
||||
const socketId: string | undefined = req.body.socketId;
|
||||
const claimId: number | undefined = claimData.claimId
|
||||
? Number(claimData.claimId)
|
||||
: undefined;
|
||||
|
||||
const jobId = enqueueSeleniumJob({
|
||||
jobType: "uniteducard-claim-submit",
|
||||
userId: req.user.id,
|
||||
socketId,
|
||||
enrichedPayload,
|
||||
claimId,
|
||||
});
|
||||
|
||||
return res.json({ status: "queued", jobId });
|
||||
} catch (err: any) {
|
||||
console.error("[uniteducard-claim route] error:", err);
|
||||
return res.status(500).json({
|
||||
error: err.message || "Failed to enqueue United Ucard claim job",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /claims/uniteducard-claim/selenium/submit-otp
|
||||
* Body: { session_id, otp, socketId? }
|
||||
*/
|
||||
router.post("/uniteducard-claim/selenium/submit-otp", async (req: Request, res: Response): Promise<any> => {
|
||||
const { session_id: sessionId, otp, socketId } = req.body;
|
||||
if (!sessionId || !otp) {
|
||||
return res.status(400).json({ error: "session_id and otp are required" });
|
||||
}
|
||||
try {
|
||||
const r = await forwardOtpToSeleniumUnitedUcardClaimAgent(sessionId, otp);
|
||||
if (socketId && io) {
|
||||
io.to(socketId).emit("selenium:otp_submitted", { session_id: sessionId });
|
||||
}
|
||||
return res.json(r);
|
||||
} catch (err: any) {
|
||||
console.error("[uniteducard-claim] submit-otp failed:", err?.message);
|
||||
return res.status(500).json({ error: err?.message || "Failed to forward OTP" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user