feat: add CCA claim submission with Selenium automation
- Add CCA claim submit Selenium worker (login, fill form, attach docs, submit, capture dashboard PDF) - Add CCA fee schedule (procedureCodesMH.json renamed, procedureCodesCCA.json added with D6010) - Add backend route /api/claims/cca-claim, processor, and Selenium client - Wire CCA claim handler in claims-page with job tracking and PDF preview popup - Add insurance type dropdown in claim form (same options as eligibility page) - Auto-populate insurance type from patient.insuranceProvider in claim form and patient edit form - Map fee schedule by insurance type in Map Price button and combo buttons - Fix CCA login speed (remove fixed sleeps, use readyState check) - Fix CCA claim DOB format bug (was sending MM-DD-YYYY, now sends YYYY-MM-DD) - Fix npiProviderId not saved for CCA claims - Change Add Service → CCA Claim button (blue), MH → MH Claim Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,7 @@ interface ClaimFormProps {
|
||||
onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void;
|
||||
onHandleForMHSeleniumClaim: (data: ClaimFormData) => void;
|
||||
onHandleForMHSeleniumClaimPreAuth: (data: ClaimPreAuthData) => void;
|
||||
onHandleForCCASeleniumClaim: (data: ClaimFormData) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -87,6 +88,7 @@ export function ClaimForm({
|
||||
onHandleUpdatePatient,
|
||||
onHandleForMHSeleniumClaim,
|
||||
onHandleForMHSeleniumClaimPreAuth,
|
||||
onHandleForCCASeleniumClaim,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: ClaimFormProps) {
|
||||
@@ -331,6 +333,7 @@ export function ClaimForm({
|
||||
missingTeethStatus: (claim.missingTeethStatus as MissingTeethStatus) ?? "No_missing",
|
||||
missingTeeth: (claim.missingTeeth as Record<string, "X" | "O">) ?? {},
|
||||
insuranceProvider: claim.insuranceProvider ?? "",
|
||||
insuranceSiteKey: claim.insuranceSiteKey || deriveInsuranceSiteKey(claim.insuranceProvider),
|
||||
...(claim.staffId ? { staffId: claim.staffId } : {}),
|
||||
claimFiles: claim.claimFiles ?? [],
|
||||
}));
|
||||
@@ -590,17 +593,41 @@ export function ClaimForm({
|
||||
uploadedFiles: [],
|
||||
});
|
||||
|
||||
// Map patient.insuranceProvider (free-text from eligibility) → insuranceSiteKey
|
||||
const deriveInsuranceSiteKey = (provider: string | null | undefined): string => {
|
||||
const p = (provider || "").toLowerCase().trim();
|
||||
if (!p) return "";
|
||||
if (p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
|
||||
if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
|
||||
if (p.includes("ddma")) return "DDMA";
|
||||
if (p.includes("delta ins") || p === "deltains") return "DeltaIns";
|
||||
if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TuftsSCO";
|
||||
if (p.includes("united sco") || p === "unitedsco") return "UnitedSCO";
|
||||
if (p.includes("cmsp")) return "CMSP";
|
||||
if (p.includes("bcbs") || p.includes("blue cross")) return "BCBS";
|
||||
if (p.includes("united aapr") || p === "unitedaapr") return "UnitedAAPR";
|
||||
if (p.includes("aetna")) return "Aetna";
|
||||
if (p.includes("altus")) return "Altus";
|
||||
if (p.includes("metlife")) return "MetlifeDental";
|
||||
if (p.includes("cigna")) return "Cigna";
|
||||
if (p.includes("delta wa") || p === "deltawa") return "DeltaWA";
|
||||
if (p.includes("delta il") || p === "deltail") return "DeltaIL";
|
||||
return "";
|
||||
};
|
||||
|
||||
// Sync patient data to form when patient updates
|
||||
useEffect(() => {
|
||||
if (patient) {
|
||||
const fullName =
|
||||
`${patient.firstName || ""} ${patient.lastName || ""}`.trim();
|
||||
const siteKey = deriveInsuranceSiteKey(patient.insuranceProvider);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: Number(patient.id),
|
||||
patientName: fullName,
|
||||
dateOfBirth: normalizeToIsoDateString(patient.dateOfBirth),
|
||||
memberId: patient.insuranceId || "",
|
||||
...(siteKey ? { insuranceSiteKey: siteKey } : {}),
|
||||
}));
|
||||
}
|
||||
}, [patient]);
|
||||
@@ -674,12 +701,13 @@ export function ClaimForm({
|
||||
}
|
||||
};
|
||||
|
||||
// Map Price function
|
||||
// Map Price function — uses the fee schedule for the selected insurance type
|
||||
const onMapPrice = () => {
|
||||
setForm((prev) =>
|
||||
mapPricesForForm({
|
||||
form: prev,
|
||||
patientDOB: patient?.dateOfBirth ?? "",
|
||||
insuranceSiteKey: prev.insuranceSiteKey,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -867,15 +895,12 @@ export function ClaimForm({
|
||||
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
|
||||
// 3rd Button workflow — CCA Claim: saves to DB then submits via Selenium
|
||||
const handleCCAClaim = async () => {
|
||||
const missingFields: string[] = [];
|
||||
|
||||
if (!form.memberId?.trim()) missingFields.push("Member ID");
|
||||
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
|
||||
if (!patient?.firstName?.trim()) missingFields.push("First Name");
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
toast({
|
||||
title: "Missing Required Fields",
|
||||
@@ -885,31 +910,26 @@ export function ClaimForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// require at least one procedure code before proceeding
|
||||
const filteredServiceLines = (form.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.",
|
||||
description: "Please add at least one procedure code before submitting the claim.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Create or update appointment
|
||||
// Create appointment if needed
|
||||
let appointmentIdToUse = appointmentId;
|
||||
|
||||
if (appointmentIdToUse == null) {
|
||||
const appointmentData = {
|
||||
patientId: patientId,
|
||||
const created = await onHandleAppointmentSubmit({
|
||||
patientId,
|
||||
date: serviceDate,
|
||||
staffId: appointmentStaffId ?? staff?.id,
|
||||
};
|
||||
const created = await onHandleAppointmentSubmit(appointmentData);
|
||||
|
||||
});
|
||||
if (typeof created === "number" && created > 0) {
|
||||
appointmentIdToUse = created;
|
||||
} else if (created && typeof (created as any).id === "number") {
|
||||
@@ -917,27 +937,40 @@ export function ClaimForm({
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create Claim(if not)
|
||||
// Filter out empty service lines (empty procedureCode)
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider: _npi, ...formToCreateClaim } = form;
|
||||
|
||||
// build claimFiles metadata from uploadedFiles (only filename + mimeType)
|
||||
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
|
||||
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({
|
||||
filename: f.name,
|
||||
mimeType: f.type,
|
||||
}));
|
||||
|
||||
const selectedNpiProviderId = npiProvider?.npiNumber
|
||||
? npiProviders.find((p) => p.npiNumber === npiProvider.npiNumber)?.id ?? null
|
||||
: null;
|
||||
|
||||
// Save claim to DB
|
||||
const createdClaim = await onSubmit({
|
||||
...formToCreateClaim,
|
||||
serviceLines: filteredServiceLines,
|
||||
staffId: appointmentStaffId ?? Number(staff?.id),
|
||||
patientId: patientId,
|
||||
insuranceProvider: "MassHealth",
|
||||
patientId,
|
||||
insuranceProvider: "CCA",
|
||||
appointmentId: appointmentIdToUse!,
|
||||
claimFiles: claimFilesMeta,
|
||||
...(selectedNpiProviderId ? { npiProviderId: selectedNpiProviderId } : {}),
|
||||
});
|
||||
|
||||
// Send to CCA Selenium — send raw YYYY-MM-DD so Python _format_dob converts correctly
|
||||
onHandleForCCASeleniumClaim({
|
||||
...form,
|
||||
serviceLines: filteredServiceLines,
|
||||
staffId: appointmentStaffId ?? Number(staff?.id),
|
||||
patientId,
|
||||
insuranceProvider: "CCA",
|
||||
appointmentId: appointmentIdToUse!,
|
||||
insuranceSiteKey: "CCA",
|
||||
claimId: createdClaim.id,
|
||||
});
|
||||
|
||||
// 4. Close form
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -1186,6 +1219,7 @@ export function ClaimForm({
|
||||
comboId,
|
||||
patient?.dateOfBirth ?? "",
|
||||
{ replaceAll: false, lineDate: form.serviceDate },
|
||||
form.insuranceSiteKey,
|
||||
);
|
||||
|
||||
setForm(nextForm);
|
||||
@@ -1337,6 +1371,35 @@ export function ClaimForm({
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-end items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Label className="flex items-center">Insurance Type</Label>
|
||||
<Select
|
||||
value={form.insuranceSiteKey || ""}
|
||||
onValueChange={(val) =>
|
||||
setForm((prev) => ({ ...prev, insuranceSiteKey: val }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-44 mr-4">
|
||||
<SelectValue placeholder="Select Insurance" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MH">MassHealth</SelectItem>
|
||||
<SelectItem value="CCA">CCA</SelectItem>
|
||||
<SelectItem value="DDMA">DDMA</SelectItem>
|
||||
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
|
||||
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
|
||||
<SelectItem value="UnitedSCO">United SCO</SelectItem>
|
||||
<SelectItem value="CMSP">CMSP</SelectItem>
|
||||
<SelectItem value="BCBS">BCBS</SelectItem>
|
||||
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
|
||||
<SelectItem value="Aetna">Aetna</SelectItem>
|
||||
<SelectItem value="Altus">Altus</SelectItem>
|
||||
<SelectItem value="MetlifeDental">Metlife Dental</SelectItem>
|
||||
<SelectItem value="Cigna">Cigna</SelectItem>
|
||||
<SelectItem value="DeltaWA">Delta WA</SelectItem>
|
||||
<SelectItem value="DeltaIL">Delta IL</SelectItem>
|
||||
<SelectItem value="Others">Others</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="flex items-center">Service Date</Label>
|
||||
<Popover
|
||||
open={serviceDateOpen}
|
||||
@@ -1421,6 +1484,7 @@ export function ClaimForm({
|
||||
comboKey as any,
|
||||
patient?.dateOfBirth ?? "",
|
||||
{ replaceAll: false, lineDate: prev.serviceDate },
|
||||
prev.insuranceSiteKey,
|
||||
);
|
||||
setTimeout(() => scrollToLine(0), 0);
|
||||
return next;
|
||||
@@ -1765,14 +1829,13 @@ export function ClaimForm({
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => handleMHSubmit()}
|
||||
>
|
||||
MH
|
||||
MH Claim
|
||||
</Button>
|
||||
<Button
|
||||
className="w-32"
|
||||
variant="secondary"
|
||||
onClick={handleAddService}
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleCCAClaim}
|
||||
>
|
||||
Add Service
|
||||
CCA Claim
|
||||
</Button>
|
||||
<Button className="w-32" variant="outline">
|
||||
Delta MA
|
||||
@@ -1862,6 +1925,35 @@ export function ClaimForm({
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-end items-center mb-4">
|
||||
<div className="flex gap-2">
|
||||
<Label className="flex items-center">Insurance Type</Label>
|
||||
<Select
|
||||
value={form.insuranceSiteKey || ""}
|
||||
onValueChange={(val) =>
|
||||
setForm((prev) => ({ ...prev, insuranceSiteKey: val }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-44 mr-4">
|
||||
<SelectValue placeholder="Select Insurance" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MH">MassHealth</SelectItem>
|
||||
<SelectItem value="CCA">CCA</SelectItem>
|
||||
<SelectItem value="DDMA">DDMA</SelectItem>
|
||||
<SelectItem value="DeltaIns">Delta Ins</SelectItem>
|
||||
<SelectItem value="TuftsSCO">Tufts SCO</SelectItem>
|
||||
<SelectItem value="UnitedSCO">United SCO</SelectItem>
|
||||
<SelectItem value="CMSP">CMSP</SelectItem>
|
||||
<SelectItem value="BCBS">BCBS</SelectItem>
|
||||
<SelectItem value="UnitedAAPR">United AAPR</SelectItem>
|
||||
<SelectItem value="Aetna">Aetna</SelectItem>
|
||||
<SelectItem value="Altus">Altus</SelectItem>
|
||||
<SelectItem value="MetlifeDental">Metlife Dental</SelectItem>
|
||||
<SelectItem value="Cigna">Cigna</SelectItem>
|
||||
<SelectItem value="DeltaWA">Delta WA</SelectItem>
|
||||
<SelectItem value="DeltaIL">Delta IL</SelectItem>
|
||||
<SelectItem value="Others">Others</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="flex items-center">Service Date</Label>
|
||||
<Popover
|
||||
open={serviceDateOpen}
|
||||
|
||||
Reference in New Issue
Block a user