feat(claim-pre-auth) - added feature v1
This commit is contained in:
@@ -37,7 +37,7 @@ const upload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/selenium",
|
"/selenium-claim",
|
||||||
upload.fields([
|
upload.fields([
|
||||||
{ name: "pdfs", maxCount: 10 },
|
{ name: "pdfs", maxCount: 10 },
|
||||||
{ name: "images", maxCount: 10 },
|
{ name: "images", maxCount: 10 },
|
||||||
@@ -108,7 +108,7 @@ router.post(
|
|||||||
return sendError(res, "Unauthorized: user info missing", 401);
|
return sendError(res, "Unauthorized: user info missing", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { patientId, pdf_url } = req.body;
|
const { patientId, pdf_url, groupTitleKey } = req.body;
|
||||||
|
|
||||||
if (!pdf_url) {
|
if (!pdf_url) {
|
||||||
return sendError(res, "Missing pdf_url");
|
return sendError(res, "Missing pdf_url");
|
||||||
@@ -126,7 +126,15 @@ router.post(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groupTitle = "Claims";
|
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
|
// ✅ Find or create PDF group for this claim
|
||||||
let group = await storage.findPdfGroupByPatientTitleKey(
|
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<any> => {
|
||||||
|
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<string, Express.Multer.File[]>).pdfs ?? [];
|
||||||
|
const images =
|
||||||
|
(req.files as Record<string, Express.Multer.File[]>).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
|
// GET /api/claims/recent
|
||||||
router.get("/recent", async (req: Request, res: Response) => {
|
router.get("/recent", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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<any> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -33,6 +33,9 @@ import {
|
|||||||
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
||||||
import {
|
import {
|
||||||
Claim,
|
Claim,
|
||||||
|
ClaimFileMeta,
|
||||||
|
ClaimFormData,
|
||||||
|
ClaimPreAuthData,
|
||||||
InputServiceLine,
|
InputServiceLine,
|
||||||
InsertAppointment,
|
InsertAppointment,
|
||||||
Patient,
|
Patient,
|
||||||
@@ -47,32 +50,8 @@ import {
|
|||||||
getDescriptionForCode,
|
getDescriptionForCode,
|
||||||
} from "@/utils/procedureCombosMapping";
|
} from "@/utils/procedureCombosMapping";
|
||||||
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos";
|
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos";
|
||||||
import { DateInputField } from "../ui/dateInputField";
|
|
||||||
import { DateInput } from "../ui/dateInput";
|
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 {
|
interface ClaimFormProps {
|
||||||
patientId: number;
|
patientId: number;
|
||||||
appointmentId?: number;
|
appointmentId?: number;
|
||||||
@@ -81,7 +60,8 @@ interface ClaimFormProps {
|
|||||||
appointmentData: InsertAppointment | UpdateAppointment
|
appointmentData: InsertAppointment | UpdateAppointment
|
||||||
) => Promise<number | { id: number }>;
|
) => Promise<number | { id: number }>;
|
||||||
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
|
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
|
||||||
onHandleForMHSelenium: (data: ClaimFormData) => void;
|
onHandleForMHSeleniumClaim: (data: ClaimFormData) => void;
|
||||||
|
onHandleForMHSeleniumClaimPreAuth: (data: ClaimPreAuthData) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +70,8 @@ export function ClaimForm({
|
|||||||
appointmentId,
|
appointmentId,
|
||||||
onHandleAppointmentSubmit,
|
onHandleAppointmentSubmit,
|
||||||
onHandleUpdatePatient,
|
onHandleUpdatePatient,
|
||||||
onHandleForMHSelenium,
|
onHandleForMHSeleniumClaim,
|
||||||
|
onHandleForMHSeleniumClaimPreAuth,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
}: ClaimFormProps) {
|
}: ClaimFormProps) {
|
||||||
@@ -334,6 +315,13 @@ export function ClaimForm({
|
|||||||
return "";
|
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
|
// MAIN FORM INITIAL STATE
|
||||||
const [form, setForm] = useState<ClaimFormData & { uploadedFiles: File[] }>({
|
const [form, setForm] = useState<ClaimFormData & { uploadedFiles: File[] }>({
|
||||||
patientId: patientId || 0,
|
patientId: patientId || 0,
|
||||||
@@ -542,8 +530,9 @@ export function ClaimForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 4. sending form data to selenium service
|
// 4. sending form data to selenium service
|
||||||
onHandleForMHSelenium({
|
onHandleForMHSeleniumClaim({
|
||||||
...f,
|
...f,
|
||||||
|
dateOfBirth: toMMDDYYYY(f.dateOfBirth),
|
||||||
serviceLines: filteredServiceLines,
|
serviceLines: filteredServiceLines,
|
||||||
staffId: Number(staff?.id),
|
staffId: Number(staff?.id),
|
||||||
patientId: patientId,
|
patientId: patientId,
|
||||||
@@ -557,7 +546,76 @@ export function ClaimForm({
|
|||||||
onClose();
|
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 () => {
|
const handleAddService = async () => {
|
||||||
// 0. Validate required fields
|
// 0. Validate required fields
|
||||||
const missingFields: string[] = [];
|
const missingFields: string[] = [];
|
||||||
@@ -648,6 +706,7 @@ export function ClaimForm({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// for direct combo button.
|
||||||
const applyComboAndThenMH = async (
|
const applyComboAndThenMH = async (
|
||||||
comboId: keyof typeof PROCEDURE_COMBOS
|
comboId: keyof typeof PROCEDURE_COMBOS
|
||||||
) => {
|
) => {
|
||||||
@@ -1237,7 +1296,11 @@ export function ClaimForm({
|
|||||||
>
|
>
|
||||||
MH
|
MH
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-32" variant="secondary">
|
<Button
|
||||||
|
className="w-32"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleMHPreAuth()}
|
||||||
|
>
|
||||||
MH PreAuth
|
MH PreAuth
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export default function ClaimsPage() {
|
|||||||
);
|
);
|
||||||
const response = await apiRequest(
|
const response = await apiRequest(
|
||||||
"POST",
|
"POST",
|
||||||
"/api/claims/selenium",
|
"/api/claims/selenium-claim",
|
||||||
formData
|
formData
|
||||||
);
|
);
|
||||||
const result1 = await response.json();
|
const result1 = await response.json();
|
||||||
@@ -337,7 +337,8 @@ export default function ClaimsPage() {
|
|||||||
|
|
||||||
const result2 = await handleMHSeleniumPdfDownload(
|
const result2 = await handleMHSeleniumPdfDownload(
|
||||||
result1,
|
result1,
|
||||||
selectedPatientId
|
selectedPatientId,
|
||||||
|
"INSURANCE_CLAIM"
|
||||||
);
|
);
|
||||||
return result2;
|
return result2;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -358,7 +359,8 @@ export default function ClaimsPage() {
|
|||||||
// 5. selenium pdf download handler
|
// 5. selenium pdf download handler
|
||||||
const handleMHSeleniumPdfDownload = async (
|
const handleMHSeleniumPdfDownload = async (
|
||||||
data: any,
|
data: any,
|
||||||
selectedPatientId: number | null
|
selectedPatientId: number | null,
|
||||||
|
groupTitleKey: "INSURANCE_CLAIM" | "INSURANCE_CLAIM_PREAUTH"
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (!selectedPatientId) {
|
if (!selectedPatientId) {
|
||||||
@@ -375,6 +377,7 @@ export default function ClaimsPage() {
|
|||||||
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
||||||
patientId: selectedPatientId,
|
patientId: selectedPatientId,
|
||||||
pdf_url: data.pdf_url,
|
pdf_url: data.pdf_url,
|
||||||
|
groupTitleKey,
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
if (result.error) throw new Error(result.error);
|
if (result.error) throw new Error(result.error);
|
||||||
@@ -416,6 +419,69 @@ export default function ClaimsPage() {
|
|||||||
clearUrlParams(["newPatient", "appointmentId"]);
|
clearUrlParams(["newPatient", "appointmentId"]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pre Auth section
|
||||||
|
const handleMHClaimPreAuthSubmitSelenium = async (data: any) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("data", JSON.stringify(data));
|
||||||
|
const uploadedFiles: File[] = data.uploadedFiles ?? [];
|
||||||
|
|
||||||
|
uploadedFiles.forEach((file: File) => {
|
||||||
|
if (file.type === "application/pdf") {
|
||||||
|
formData.append("pdfs", file);
|
||||||
|
} else if (file.type.startsWith("image/")) {
|
||||||
|
formData.append("images", file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "Submitting claim pre auth to Selenium...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/claims/selenium-claim-pre-auth",
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
const result1 = await response.json();
|
||||||
|
if (result1.error) throw new Error(result1.error);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "Submitted to Selenium. Awaiting PDF...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Selenium service notified",
|
||||||
|
description:
|
||||||
|
"Your claim pre auth data was successfully sent to Selenium, Waitinig for its response.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result2 = await handleMHSeleniumPdfDownload(
|
||||||
|
result1,
|
||||||
|
selectedPatientId,
|
||||||
|
"INSURANCE_CLAIM_PREAUTH"
|
||||||
|
);
|
||||||
|
return result2;
|
||||||
|
} catch (error: any) {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: error.message || "Selenium submission failed",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Selenium service error",
|
||||||
|
description: error.message || "An error occurred.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SeleniumTaskBanner
|
<SeleniumTaskBanner
|
||||||
@@ -470,7 +536,8 @@ export default function ClaimsPage() {
|
|||||||
onSubmit={handleClaimSubmit}
|
onSubmit={handleClaimSubmit}
|
||||||
onHandleAppointmentSubmit={handleAppointmentSubmit}
|
onHandleAppointmentSubmit={handleAppointmentSubmit}
|
||||||
onHandleUpdatePatient={handleUpdatePatient}
|
onHandleUpdatePatient={handleUpdatePatient}
|
||||||
onHandleForMHSelenium={handleMHClaimSubmitSelenium}
|
onHandleForMHSeleniumClaim={handleMHClaimSubmitSelenium}
|
||||||
|
onHandleForMHSeleniumClaimPreAuth={handleMHClaimPreAuthSubmitSelenium}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import asyncio
|
|||||||
from selenium_claimSubmitWorker import AutomationMassHealth
|
from selenium_claimSubmitWorker import AutomationMassHealth
|
||||||
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
||||||
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
||||||
|
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -27,7 +28,7 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Endpoint: Step 1 — Start the automation of submitting Claim.
|
# Endpoint: 1 — Start the automation of submitting Claim.
|
||||||
@app.post("/claimsubmit")
|
@app.post("/claimsubmit")
|
||||||
async def start_workflow(request: Request):
|
async def start_workflow(request: Request):
|
||||||
global active_jobs, waiting_jobs
|
global active_jobs, waiting_jobs
|
||||||
@@ -55,7 +56,7 @@ async def start_workflow(request: Request):
|
|||||||
async with lock:
|
async with lock:
|
||||||
active_jobs -= 1
|
active_jobs -= 1
|
||||||
|
|
||||||
# Endpoint: Step 2 — Start the automation of cheking eligibility
|
# Endpoint: 2 — Start the automation of cheking eligibility
|
||||||
@app.post("/eligibility-check")
|
@app.post("/eligibility-check")
|
||||||
async def start_workflow(request: Request):
|
async def start_workflow(request: Request):
|
||||||
global active_jobs, waiting_jobs
|
global active_jobs, waiting_jobs
|
||||||
@@ -82,7 +83,7 @@ async def start_workflow(request: Request):
|
|||||||
async with lock:
|
async with lock:
|
||||||
active_jobs -= 1
|
active_jobs -= 1
|
||||||
|
|
||||||
# Endpoint: Step 3 — Start the automation of cheking claim status
|
# Endpoint: 3 — Start the automation of cheking claim status
|
||||||
@app.post("/claim-status-check")
|
@app.post("/claim-status-check")
|
||||||
async def start_workflow(request: Request):
|
async def start_workflow(request: Request):
|
||||||
global active_jobs, waiting_jobs
|
global active_jobs, waiting_jobs
|
||||||
@@ -109,6 +110,33 @@ async def start_workflow(request: Request):
|
|||||||
async with lock:
|
async with lock:
|
||||||
active_jobs -= 1
|
active_jobs -= 1
|
||||||
|
|
||||||
|
# Endpoint: 4 — Start the automation of cheking claim pre auth
|
||||||
|
@app.post("/claim-pre-auth")
|
||||||
|
async def start_workflow(request: Request):
|
||||||
|
global active_jobs, waiting_jobs
|
||||||
|
data = await request.json()
|
||||||
|
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs += 1
|
||||||
|
|
||||||
|
async with semaphore:
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs -= 1
|
||||||
|
active_jobs += 1
|
||||||
|
try:
|
||||||
|
bot = AutomationMassHealthPreAuth(data)
|
||||||
|
result = bot.main_workflow("https://providers.massdhp.com/providers_login.asp")
|
||||||
|
|
||||||
|
if result.get("status") != "success":
|
||||||
|
return {"status": "error", "message": result.get("message")}
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
finally:
|
||||||
|
async with lock:
|
||||||
|
active_jobs -= 1
|
||||||
|
|
||||||
# ✅ Status Endpoint
|
# ✅ Status Endpoint
|
||||||
@app.get("/status")
|
@app.get("/status")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class AutomationMassHealth:
|
|||||||
|
|
||||||
# Fill DOB parts
|
# Fill DOB parts
|
||||||
try:
|
try:
|
||||||
dob_parts = self.dateOfBirth.split("/")
|
dob_parts = self.dateOfBirth.split("-")
|
||||||
month= dob_parts[0].zfill(2) # "12"
|
month= dob_parts[0].zfill(2) # "12"
|
||||||
day= dob_parts[1].zfill(2) # "13"
|
day= dob_parts[1].zfill(2) # "13"
|
||||||
year = dob_parts[2] # "1965"
|
year = dob_parts[2] # "1965"
|
||||||
|
|||||||
350
apps/SeleniumService/selenium_preAuthWorker.py
Normal file
350
apps/SeleniumService/selenium_preAuthWorker.py
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common import TimeoutException
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
from selenium.webdriver.support.ui import Select
|
||||||
|
import time
|
||||||
|
import tempfile
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
|
class AutomationMassHealthPreAuth:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.headless = False
|
||||||
|
self.driver = None
|
||||||
|
|
||||||
|
self.data = data
|
||||||
|
self.claim = data.get("claim", {})
|
||||||
|
self.upload_files = data.get("pdfs", []) + data.get("images", [])
|
||||||
|
|
||||||
|
# Flatten values for convenience
|
||||||
|
self.memberId = self.claim.get("memberId", "")
|
||||||
|
self.dateOfBirth = self.claim.get("dateOfBirth", "")
|
||||||
|
self.remarks = self.claim.get("remarks", "")
|
||||||
|
self.massdhp_username = self.claim.get("massdhpUsername", "")
|
||||||
|
self.massdhp_password = self.claim.get("massdhpPassword", "")
|
||||||
|
self.serviceLines = self.claim.get("serviceLines", [])
|
||||||
|
self.missingTeethStatus = self.claim.get("missingTeethStatus", "")
|
||||||
|
self.missingTeeth = self.claim.get("missingTeeth", {})
|
||||||
|
|
||||||
|
|
||||||
|
def config_driver(self):
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
if self.headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
s = Service(ChromeDriverManager().install())
|
||||||
|
driver = webdriver.Chrome(service=s, options=options)
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Enter email
|
||||||
|
email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='Email' and @type='text']")))
|
||||||
|
email_field.clear()
|
||||||
|
email_field.send_keys(self.massdhp_username)
|
||||||
|
|
||||||
|
# Enter password
|
||||||
|
password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='Pass' and @type='password']")))
|
||||||
|
password_field.clear()
|
||||||
|
password_field.send_keys(self.massdhp_password)
|
||||||
|
|
||||||
|
# Click login
|
||||||
|
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Login']")))
|
||||||
|
login_button.click()
|
||||||
|
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while logging in: {e}")
|
||||||
|
return "ERROR:LOGIN FAILED"
|
||||||
|
|
||||||
|
def step1(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
try:
|
||||||
|
claim_upload_link = wait.until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//a[text()='PA Upload']"))
|
||||||
|
)
|
||||||
|
claim_upload_link.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Fill Member ID
|
||||||
|
member_id_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text1"]')))
|
||||||
|
member_id_input.clear()
|
||||||
|
member_id_input.send_keys(self.memberId)
|
||||||
|
|
||||||
|
# Fill DOB parts
|
||||||
|
try:
|
||||||
|
dob_parts = self.dateOfBirth.split("-")
|
||||||
|
month= dob_parts[0].zfill(2) # "12"
|
||||||
|
day= dob_parts[1].zfill(2) # "13"
|
||||||
|
year = dob_parts[2] # "1965"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing DOB: {e}")
|
||||||
|
return "ERROR: PARSING DOB"
|
||||||
|
|
||||||
|
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text2"]'))).send_keys(month)
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text3"]'))).send_keys(day)
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text4"]'))).send_keys(year)
|
||||||
|
|
||||||
|
# Rendering Provider NPI dropdown
|
||||||
|
npi_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Select1"]')))
|
||||||
|
select_npi = Select(npi_dropdown)
|
||||||
|
select_npi.select_by_index(1)
|
||||||
|
|
||||||
|
# Office Location dropdown
|
||||||
|
location_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Select2"]')))
|
||||||
|
select_location = Select(location_dropdown)
|
||||||
|
select_location.select_by_index(1)
|
||||||
|
|
||||||
|
# Click Continue button
|
||||||
|
continue_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@type="submit" and @value="Continue"]')))
|
||||||
|
continue_btn.click()
|
||||||
|
|
||||||
|
|
||||||
|
# Check for error message
|
||||||
|
try:
|
||||||
|
error_msg = WebDriverWait(self.driver, 5).until(EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//td[@class='text_err_msg' and contains(text(), 'Invalid Member ID or Date of Birth')]")
|
||||||
|
))
|
||||||
|
if error_msg:
|
||||||
|
print("Error: Invalid Member ID or Date of Birth.")
|
||||||
|
return "ERROR: INVALID MEMBERID OR DOB"
|
||||||
|
except TimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while step1 i.e Cheking the MemberId and DOB in: {e}")
|
||||||
|
return "ERROR:STEP1"
|
||||||
|
|
||||||
|
def step2(self):
|
||||||
|
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
# already waiting in step1 last part, so no time sleep.
|
||||||
|
|
||||||
|
# 1 - Procedure Codes part
|
||||||
|
try:
|
||||||
|
for proc in self.serviceLines:
|
||||||
|
# Wait for Procedure Code dropdown and select code
|
||||||
|
select_element = wait.until(EC.presence_of_element_located((By.XPATH, "//select[@id='Select3']")))
|
||||||
|
Select(select_element).select_by_value(proc['procedureCode'])
|
||||||
|
|
||||||
|
# not Filling Procedure Date
|
||||||
|
|
||||||
|
# Fill Oral Cavity Area if present
|
||||||
|
if proc.get("oralCavityArea"):
|
||||||
|
oral_xpath = "//input[@name='OralCavityArea']"
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, oral_xpath))).clear()
|
||||||
|
self.driver.find_element(By.XPATH, oral_xpath).send_keys(proc["oralCavityArea"])
|
||||||
|
|
||||||
|
# Fill Tooth Number if present
|
||||||
|
if proc.get("toothNumber"):
|
||||||
|
tooth_num_dropdown = wait.until(EC.presence_of_element_located((By.XPATH, "//select[@name='ToothNumber']")))
|
||||||
|
select_tooth = Select(tooth_num_dropdown)
|
||||||
|
select_tooth.select_by_value(proc["toothNumber"])
|
||||||
|
|
||||||
|
|
||||||
|
# Fill Tooth Surface if present
|
||||||
|
if proc.get("toothSurface"):
|
||||||
|
surfaces = proc["toothSurface"].split(",")
|
||||||
|
for surface in surfaces:
|
||||||
|
surface = surface.strip()
|
||||||
|
checkbox_xpath = f"//input[@type='checkbox' and @name='TS_{surface}']"
|
||||||
|
checkbox = wait.until(EC.element_to_be_clickable((By.XPATH, checkbox_xpath)))
|
||||||
|
if not checkbox.is_selected():
|
||||||
|
checkbox.click()
|
||||||
|
|
||||||
|
|
||||||
|
# Fill Fees if present
|
||||||
|
if proc.get("totalBilled"):
|
||||||
|
fees_xpath = "//input[@name='ProcedureFee']"
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, fees_xpath))).clear()
|
||||||
|
self.driver.find_element(By.XPATH, fees_xpath).send_keys(proc["totalBilled"])
|
||||||
|
|
||||||
|
# Click "Add Procedure" button
|
||||||
|
add_proc_xpath = "//input[@type='submit' and @value='Add Procedure']"
|
||||||
|
wait.until(EC.element_to_be_clickable((By.XPATH, add_proc_xpath))).click()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while filling Procedure Codes: {e}")
|
||||||
|
return "ERROR:PROCEDURE CODES"
|
||||||
|
|
||||||
|
# 2 - Upload PDFs:
|
||||||
|
try:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
for file_obj in self.upload_files:
|
||||||
|
base64_data = file_obj["bufferBase64"]
|
||||||
|
file_name = file_obj.get("originalname", "tempfile.bin")
|
||||||
|
|
||||||
|
# Ensure valid extension fallback if missing
|
||||||
|
if not any(file_name.lower().endswith(ext) for ext in [".pdf", ".jpg", ".jpeg", ".png", ".webp"]):
|
||||||
|
file_name += ".bin"
|
||||||
|
|
||||||
|
# Full path with original filename inside temp dir
|
||||||
|
tmp_file_path = os.path.join(tmp_dir, file_name)
|
||||||
|
|
||||||
|
# Decode and save
|
||||||
|
with open(tmp_file_path, "wb") as tmp_file:
|
||||||
|
tmp_file.write(base64.b64decode(base64_data))
|
||||||
|
|
||||||
|
# Upload using Selenium
|
||||||
|
file_input = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='FileName' and @type='file']")))
|
||||||
|
file_input.send_keys(tmp_file_path)
|
||||||
|
|
||||||
|
upload_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Upload File']")))
|
||||||
|
upload_button.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while uploading PDFs: {e}")
|
||||||
|
return "ERROR:PDF FAILED"
|
||||||
|
|
||||||
|
# 3 - Indicate Missing Teeth Part
|
||||||
|
try:
|
||||||
|
# Handle the missing teeth section based on the status
|
||||||
|
missing_status = self.missingTeethStatus.strip() if self.missingTeethStatus else "No_missing"
|
||||||
|
|
||||||
|
if missing_status == "No_missing":
|
||||||
|
missing_teeth_no = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@type='checkbox' and @name='PAU_Step3_Checkbox1']")))
|
||||||
|
missing_teeth_no.click()
|
||||||
|
|
||||||
|
elif missing_status == "endentulous":
|
||||||
|
missing_teeth_edentulous = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@type='checkbox' and @name='PAU_Step3_Checkbox2']")))
|
||||||
|
missing_teeth_edentulous.click()
|
||||||
|
|
||||||
|
elif missing_status == "Yes_missing":
|
||||||
|
missing_teeth_dict = self.missingTeeth
|
||||||
|
|
||||||
|
# For each tooth in the missing teeth dict, select the dropdown option
|
||||||
|
for tooth_name, value in missing_teeth_dict.items():
|
||||||
|
if value: # only if there's a value to select
|
||||||
|
select_element = wait.until(EC.presence_of_element_located((By.XPATH, f"//select[@name='{tooth_name}']")))
|
||||||
|
select_obj = Select(select_element)
|
||||||
|
select_obj.select_by_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for upload button and click it
|
||||||
|
update_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Update Missing Teeth']")))
|
||||||
|
update_button.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while filling missing teeth: {e}")
|
||||||
|
return "ERROR:MISSING TEETH FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
# 4 - Update Remarks
|
||||||
|
try:
|
||||||
|
if self.remarks.strip():
|
||||||
|
textarea = wait.until(EC.presence_of_element_located((By.XPATH, "//textarea[@name='Remarks']")))
|
||||||
|
textarea.clear()
|
||||||
|
textarea.send_keys(self.remarks)
|
||||||
|
|
||||||
|
# Wait for update button and click it
|
||||||
|
update_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Update Remarks']")))
|
||||||
|
update_button.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while filling remarks: {e}")
|
||||||
|
return "ERROR:REMARKS FAILED"
|
||||||
|
|
||||||
|
# 5 - close buton
|
||||||
|
try:
|
||||||
|
close_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Submit Request']")))
|
||||||
|
close_button.click()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Switch to alert and accept it
|
||||||
|
try:
|
||||||
|
wait.until(EC.alert_is_present())
|
||||||
|
|
||||||
|
alert = self.driver.switch_to.alert
|
||||||
|
alert.accept()
|
||||||
|
except TimeoutException:
|
||||||
|
print("No alert appeared after clicking the button.")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while Closing: {e}")
|
||||||
|
return "ERROR:CLOSE FAILED"
|
||||||
|
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
|
||||||
|
def reach_to_pdf(self):
|
||||||
|
wait = WebDriverWait(self.driver, 90)
|
||||||
|
try:
|
||||||
|
pdf_link_element = wait.until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//a[contains(@href, '.pdf')]"))
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
pdf_relative_url = pdf_link_element.get_attribute("href")
|
||||||
|
|
||||||
|
if not pdf_relative_url.startswith("http"):
|
||||||
|
full_pdf_url = f"https://providers.massdhp.com{pdf_relative_url}"
|
||||||
|
else:
|
||||||
|
full_pdf_url = pdf_relative_url
|
||||||
|
|
||||||
|
print("FULL PDF LINK: ",full_pdf_url)
|
||||||
|
return full_pdf_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: {str(e)}")
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if self.driver:
|
||||||
|
self.driver.quit()
|
||||||
|
|
||||||
|
def main_workflow(self, url):
|
||||||
|
try:
|
||||||
|
self.config_driver()
|
||||||
|
self.driver.maximize_window()
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
login_result = self.login()
|
||||||
|
if login_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": login_result}
|
||||||
|
|
||||||
|
input("Hey")
|
||||||
|
step1_result = self.step1()
|
||||||
|
if step1_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": step1_result}
|
||||||
|
|
||||||
|
step2_result = self.step2()
|
||||||
|
if step2_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": step2_result}
|
||||||
|
|
||||||
|
reachToPdf_result = self.reach_to_pdf()
|
||||||
|
if reachToPdf_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": reachToPdf_result}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"pdf_url": reachToPdf_result
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": e
|
||||||
|
}
|
||||||
@@ -82,3 +82,37 @@ export type ClaimWithServiceLines = Claim & {
|
|||||||
staff?: Staff | null;
|
staff?: Staff | null;
|
||||||
claimFiles?: ClaimFileMeta[] | null;
|
claimFiles?: ClaimFileMeta[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export 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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimPreAuthData {
|
||||||
|
patientId: 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[];
|
||||||
|
claimFiles?: ClaimFileMeta[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user