From e47041baf696a0ae5b8b50e563bb7c56a47d63f7 Mon Sep 17 00:00:00 2001 From: Potenz Date: Mon, 3 Nov 2025 21:29:16 +0530 Subject: [PATCH] feat(claim-pre-auth) - added feature v1 --- apps/Backend/src/routes/claims.ts | 73 +++- .../seleniumInsuranceClaimPreAuthClient.ts | 52 +++ .../src/components/claims/claim-form.tsx | 121 ++++-- apps/Frontend/src/pages/claims-page.tsx | 75 +++- apps/SeleniumService/agent.py | 34 +- .../selenium_claimSubmitWorker.py | 2 +- .../SeleniumService/selenium_preAuthWorker.py | 350 ++++++++++++++++++ packages/db/types/claim-types.ts | 34 ++ 8 files changed, 701 insertions(+), 40 deletions(-) create mode 100644 apps/Backend/src/services/seleniumInsuranceClaimPreAuthClient.ts create mode 100644 apps/SeleniumService/selenium_preAuthWorker.py diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index aeac417..782fd14 100644 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -37,7 +37,7 @@ const upload = multer({ }); router.post( - "/selenium", + "/selenium-claim", upload.fields([ { name: "pdfs", maxCount: 10 }, { name: "images", maxCount: 10 }, @@ -108,7 +108,7 @@ router.post( return sendError(res, "Unauthorized: user info missing", 401); } - const { patientId, pdf_url } = req.body; + const { patientId, pdf_url, groupTitleKey } = req.body; if (!pdf_url) { return sendError(res, "Missing pdf_url"); @@ -126,7 +126,15 @@ router.post( }); const groupTitle = "Claims"; - const groupTitleKey = "INSURANCE_CLAIM"; + + // allowed keys + const allowedKeys = ["INSURANCE_CLAIM", "INSURANCE_CLAIM_PREAUTH"]; + if (!allowedKeys.includes(groupTitleKey)) { + return sendError( + res, + `Invalid groupTitleKey. Must be one of: ${allowedKeys.join(", ")}` + ); + } // ✅ Find or create PDF group for this claim let group = await storage.findPdfGroupByPatientTitleKey( @@ -158,6 +166,65 @@ router.post( } ); +router.post( + "/selenium-claim-pre-auth", + upload.fields([ + { name: "pdfs", maxCount: 10 }, + { name: "images", maxCount: 10 }, + ]), + async (req: Request, res: Response): Promise => { + if (!req.files || !req.body.data) { + return res + .status(400) + .json({ error: "Missing files or claim data for selenium" }); + } + + if (!req.user || !req.user.id) { + return res.status(401).json({ error: "Unauthorized: user info missing" }); + } + + try { + const claimData = JSON.parse(req.body.data); + const pdfs = + (req.files as Record).pdfs ?? []; + const images = + (req.files as Record).images ?? []; + + const credentials = await storage.getInsuranceCredentialByUserAndSiteKey( + req.user.id, + claimData.insuranceSiteKey + ); + if (!credentials) { + return res.status(404).json({ + error: + "No insurance credentials found for this provider. Kindly Update this at Settings Page.", + }); + } + + const enrichedData = { + ...claimData, + massdhpUsername: credentials.username, + massdhpPassword: credentials.password, + }; + + const result = await forwardToSeleniumClaimAgent(enrichedData, [ + ...pdfs, + ...images, + ]); + + res.json({ + ...result, + claimId: claimData.claimId, + }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ + error: err.message || "Failed to forward to selenium agent", + }); + } + } +); + // GET /api/claims/recent router.get("/recent", async (req: Request, res: Response) => { try { diff --git a/apps/Backend/src/services/seleniumInsuranceClaimPreAuthClient.ts b/apps/Backend/src/services/seleniumInsuranceClaimPreAuthClient.ts new file mode 100644 index 0000000..7317eb9 --- /dev/null +++ b/apps/Backend/src/services/seleniumInsuranceClaimPreAuthClient.ts @@ -0,0 +1,52 @@ +import axios from "axios"; + +export interface SeleniumPayload { + claim: any; + pdfs: { + originalname: string; + bufferBase64: string; + }[]; + images: { + originalname: string; + bufferBase64: string; + }[]; +} + +export async function forwardToSeleniumClaimAgent( + claimData: any, + files: Express.Multer.File[] +): Promise { + const pdfs = files + .filter((file) => file.mimetype === "application/pdf") + .map((file) => ({ + originalname: file.originalname, + bufferBase64: file.buffer.toString("base64"), + })); + + const images = files + .filter((file) => file.mimetype.startsWith("image/")) + .map((file) => ({ + originalname: file.originalname, + bufferBase64: file.buffer.toString("base64"), + })); + + const payload: SeleniumPayload = { + claim: claimData, + pdfs, + images, + }; + + const result = await axios.post( + "http://localhost:5002/claim-pre-auth", + payload + ); + if (result.data.status === "error") { + const errorMsg = + typeof result.data.message === "string" + ? result.data.message + : result.data.message?.msg || "Selenium agent error"; + throw new Error(errorMsg); + } + + return result.data; +} diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 6b8dd57..441eff1 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -33,6 +33,9 @@ import { import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { Claim, + ClaimFileMeta, + ClaimFormData, + ClaimPreAuthData, InputServiceLine, InsertAppointment, Patient, @@ -47,32 +50,8 @@ import { getDescriptionForCode, } from "@/utils/procedureCombosMapping"; import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos"; -import { DateInputField } from "../ui/dateInputField"; import { DateInput } from "../ui/dateInput"; -interface ClaimFileMeta { - filename: string; - mimeType: string; -} - -interface ClaimFormData { - patientId: number; - appointmentId: number; - userId: number; - staffId: number; - patientName: string; - memberId: string; - dateOfBirth: string; - remarks: string; - serviceDate: string; // YYYY-MM-DD - insuranceProvider: string; - insuranceSiteKey?: string; - status: string; // default "pending" - serviceLines: InputServiceLine[]; - claimId?: number; - claimFiles?: ClaimFileMeta[]; -} - interface ClaimFormProps { patientId: number; appointmentId?: number; @@ -81,7 +60,8 @@ interface ClaimFormProps { appointmentData: InsertAppointment | UpdateAppointment ) => Promise; onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void; - onHandleForMHSelenium: (data: ClaimFormData) => void; + onHandleForMHSeleniumClaim: (data: ClaimFormData) => void; + onHandleForMHSeleniumClaimPreAuth: (data: ClaimPreAuthData) => void; onClose: () => void; } @@ -90,7 +70,8 @@ export function ClaimForm({ appointmentId, onHandleAppointmentSubmit, onHandleUpdatePatient, - onHandleForMHSelenium, + onHandleForMHSeleniumClaim, + onHandleForMHSeleniumClaimPreAuth, onSubmit, onClose, }: ClaimFormProps) { @@ -334,6 +315,13 @@ export function ClaimForm({ return ""; } + // assumes input is either "" or "YYYY-MM-DD" + const toMMDDYYYY = (iso: string): string => { + if (!iso) return ""; + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso); + return m ? `${m[2]}-${m[3]}-${m[1]}` : ""; + }; + // MAIN FORM INITIAL STATE const [form, setForm] = useState({ patientId: patientId || 0, @@ -542,8 +530,9 @@ export function ClaimForm({ }); // 4. sending form data to selenium service - onHandleForMHSelenium({ + onHandleForMHSeleniumClaim({ ...f, + dateOfBirth: toMMDDYYYY(f.dateOfBirth), serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, @@ -557,7 +546,76 @@ export function ClaimForm({ onClose(); }; - // 2nd Button workflow - Only Creates Data, patient, appointmetn, claim, payment, not actually submits claim to MH site. + // 2st Button workflow - Mass Health Pre Auth Button Handler + const handleMHPreAuth = async ( + formToUse?: ClaimFormData & { uploadedFiles?: File[] } + ) => { + // Use the passed form, or fallback to current state + const f = formToUse ?? form; + + // 0. Validate required fields + const missingFields: string[] = []; + + if (!f.memberId?.trim()) missingFields.push("Member ID"); + if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth"); + if (!patient?.firstName?.trim()) missingFields.push("First Name"); + + if (missingFields.length > 0) { + toast({ + title: "Missing Required Fields", + description: `Please fill out the following field(s): ${missingFields.join(", ")}`, + variant: "destructive", + }); + return; + } + + // require at least one procedure code before proceeding + const filteredServiceLines = (f.serviceLines || []).filter( + (line) => (line.procedureCode ?? "").trim() !== "" + ); + if (filteredServiceLines.length === 0) { + toast({ + title: "No procedure codes", + description: + "Please add at least one procedure code before submitting the claim preAuth.", + variant: "destructive", + }); + return; + } + + // 2. Update patient + if (patient && typeof patient.id === "number") { + const { id, createdAt, userId, ...sanitizedFields } = patient; + const updatedPatientFields = { + id, + ...sanitizedFields, + insuranceProvider: "MassHealth", + }; + onHandleUpdatePatient(updatedPatientFields); + } else { + toast({ + title: "Error", + description: "Cannot update patient: Missing or invalid patient data", + variant: "destructive", + }); + } + + // 4. sending form data to selenium service + onHandleForMHSeleniumClaimPreAuth({ + ...f, + dateOfBirth: toMMDDYYYY(f.dateOfBirth), + serviceLines: filteredServiceLines, + staffId: Number(staff?.id), + patientId: patientId, + insuranceProvider: "Mass Health", + insuranceSiteKey: "MH", + }); + + // 5. Close form + onClose(); + }; + + // 3nd Button workflow - Only Creates Data, patient, appointmetn, claim, payment, not actually submits claim to MH site. const handleAddService = async () => { // 0. Validate required fields const missingFields: string[] = []; @@ -648,6 +706,7 @@ export function ClaimForm({ onClose(); }; + // for direct combo button. const applyComboAndThenMH = async ( comboId: keyof typeof PROCEDURE_COMBOS ) => { @@ -1237,7 +1296,11 @@ export function ClaimForm({ > MH -