feat: chatbot screenshot-only eligibility detect, Sun Life/DentaQuest auto-check, chatbot attachment handoff to preauth; fix TuftsSCO preauth Selenium reliability
- Chatbot: submitting with only a screenshot attached (no text) now triggers the same "AI Detect & Check Eligibility" flow as the Copy Agent page - detect-eligibility-info now also extracts the visible insurance payer name and picks Tufts SCO auto-check when both Sun Life and DentaQuest are detected, else MassHealth - Chatbot-attached files are now handed off to both claim and preauth forms unconditionally, not just claims - TuftsSCO preauth Selenium worker: verify-and-retry the Tooth field (typing was silently getting reset by duplicate-procedure-code warning banners), add a final re-verification pass across all rows, fix acknowledgement-checkbox targeting/verification, and verify the "Next step" click actually advances the wizard instead of trusting a blind click
This commit is contained in:
@@ -378,7 +378,8 @@ router.post(
|
|||||||
type: "text",
|
type: "text",
|
||||||
text:
|
text:
|
||||||
"Extract the Member ID and Date of Birth from this dental/insurance eligibility screenshot. " +
|
"Extract the Member ID and Date of Birth from this dental/insurance eligibility screenshot. " +
|
||||||
'Respond with strict JSON only, no prose, no markdown fences: {"memberId": string|null, "dob": "YYYY-MM-DD"|null}',
|
"Also read any visible insurance payer/plan name text (e.g. \"Sun Life\", \"DentaQuest\", \"MassHealth\") exactly as shown. " +
|
||||||
|
'Respond with strict JSON only, no prose, no markdown fences: {"memberId": string|null, "dob": "YYYY-MM-DD"|null, "insuranceProvider": string|null}',
|
||||||
},
|
},
|
||||||
...files.map((f) => ({
|
...files.map((f) => ({
|
||||||
type: "image_url",
|
type: "image_url",
|
||||||
@@ -390,10 +391,11 @@ router.post(
|
|||||||
|
|
||||||
let memberId: string | null = null;
|
let memberId: string | null = null;
|
||||||
let dob: string | null = null;
|
let dob: string | null = null;
|
||||||
|
let insuranceProvider: string | null = null;
|
||||||
try {
|
try {
|
||||||
const raw = String(response.content).trim();
|
const raw = String(response.content).trim();
|
||||||
const jsonStr = raw.replace(/^```json\s*/i, "").replace(/```\s*$/, "").trim();
|
const jsonStr = raw.replace(/^```json\s*/i, "").replace(/```\s*$/, "").trim();
|
||||||
const parsed = JSON.parse(jsonStr) as { memberId?: string | null; dob?: string | null };
|
const parsed = JSON.parse(jsonStr) as { memberId?: string | null; dob?: string | null; insuranceProvider?: string | null };
|
||||||
// Sanitize once here so this is the single source of truth for the memberId
|
// Sanitize once here so this is the single source of truth for the memberId
|
||||||
// used downstream (temp-patient creation, chatbot prefill, real eligibility check) —
|
// used downstream (temp-patient creation, chatbot prefill, real eligibility check) —
|
||||||
// any mismatch in formatting would cause createOrUpdatePatientByInsuranceId to create
|
// any mismatch in formatting would cause createOrUpdatePatientByInsuranceId to create
|
||||||
@@ -401,11 +403,19 @@ router.post(
|
|||||||
const cleanedMemberId = (parsed.memberId || "").replace(/[^A-Za-z0-9]/g, "");
|
const cleanedMemberId = (parsed.memberId || "").replace(/[^A-Za-z0-9]/g, "");
|
||||||
memberId = cleanedMemberId || null;
|
memberId = cleanedMemberId || null;
|
||||||
dob = parsed.dob || null;
|
dob = parsed.dob || null;
|
||||||
|
insuranceProvider = parsed.insuranceProvider || null;
|
||||||
} catch (parseErr) {
|
} catch (parseErr) {
|
||||||
console.error("[detect-eligibility-info] failed to parse AI response", parseErr);
|
console.error("[detect-eligibility-info] failed to parse AI response", parseErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({ error: false, data: { memberId, dob } });
|
// Sun Life now brands the DentaQuest/Tufts SCO portal — screenshots showing both
|
||||||
|
// names together mean the Tufts SCO auto-check, not the MassHealth default.
|
||||||
|
const providerText = (insuranceProvider || "").toLowerCase();
|
||||||
|
const hasSunLife = /sun\s*life/.test(providerText);
|
||||||
|
const hasDentaQuest = /denta\s*quest/.test(providerText);
|
||||||
|
const autoCheck = hasSunLife && hasDentaQuest ? "tufts-sco" : "mh";
|
||||||
|
|
||||||
|
return res.status(200).json({ error: false, data: { memberId, dob, insuranceProvider, autoCheck } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[detect-eligibility-info]", err);
|
console.error("[detect-eligibility-info]", err);
|
||||||
return res.status(500).json({ error: true, message: "Failed to detect eligibility info", details: String(err) });
|
return res.status(500).json({ error: true, message: "Failed to detect eligibility info", details: String(err) });
|
||||||
|
|||||||
@@ -535,16 +535,18 @@ export function ClaimForm({
|
|||||||
};
|
};
|
||||||
}, [appointmentId, serviceDate, existingClaimId]);
|
}, [appointmentId, serviceDate, existingClaimId]);
|
||||||
|
|
||||||
|
// Attach any file the user uploaded/screenshotted in the chatbot to this claim/preauth,
|
||||||
|
// regardless of whether the user typed anything like "with the attachment" — the mere
|
||||||
|
// act of attaching it in chat is enough intent.
|
||||||
|
useEffect(() => {
|
||||||
|
const chatbotFiles = takeChatbotPendingFiles();
|
||||||
|
if (chatbotFiles.length === 0) return;
|
||||||
|
setForm((prev) => ({ ...prev, uploadedFiles: chatbotFiles }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Prefill service lines (and optional service date) from chatbot claim_only flow
|
// Prefill service lines (and optional service date) from chatbot claim_only flow
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const raw = sessionStorage.getItem("chatbot_claim_prefill");
|
const raw = sessionStorage.getItem("chatbot_claim_prefill");
|
||||||
const chatbotFiles = takeChatbotPendingFiles();
|
|
||||||
if (!raw && chatbotFiles.length === 0) return;
|
|
||||||
|
|
||||||
if (chatbotFiles.length > 0) {
|
|
||||||
setForm((prev) => ({ ...prev, uploadedFiles: chatbotFiles }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
try {
|
try {
|
||||||
const { codes, serviceDate } = JSON.parse(raw) as {
|
const { codes, serviceDate } = JSON.parse(raw) as {
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ export function ChatbotButton() {
|
|||||||
});
|
});
|
||||||
addMsg(
|
addMsg(
|
||||||
"bot",
|
"bot",
|
||||||
`I read this from your screenshots — ready to check ${detail.autoCheck === "mh" ? "MassHealth" : "eligibility"} for ${
|
`I read this from your screenshots — ready to check ${detail.autoCheck === "tufts-sco" ? "Tufts SCO" : detail.autoCheck === "mh" ? "MassHealth" : "eligibility"} for ${
|
||||||
detail.patient ? `${detail.patient.firstName} ${detail.patient.lastName}` : detail.memberId
|
detail.patient ? `${detail.patient.firstName} ${detail.patient.lastName}` : detail.memberId
|
||||||
}. Confirm to proceed.`
|
}. Confirm to proceed.`
|
||||||
);
|
);
|
||||||
@@ -663,9 +663,56 @@ export function ChatbotButton() {
|
|||||||
prefillAndNavigate(checkAndClaimData.memberId, checkAndClaimData.dob, checkAndClaimData.autoCheck);
|
prefillAndNavigate(checkAndClaimData.memberId, checkAndClaimData.dob, checkAndClaimData.autoCheck);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Screenshot(s) attached with no typed text: same intent as the Copy Agent's
|
||||||
|
// "AI Detect & Check Eligibility" button — read Member ID/DOB off the image(s).
|
||||||
|
const handleScreenshotOnlyDetect = async () => {
|
||||||
|
const files = pendingFiles;
|
||||||
|
if (!files.length || step === "ai-loading") return;
|
||||||
|
setPendingFiles([]);
|
||||||
|
addMsg("user", `📎 ${files.length} screenshot${files.length > 1 ? "s" : ""}`);
|
||||||
|
addMsg("bot", "Reading your screenshot...", true);
|
||||||
|
setStep("ai-loading");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
for (const file of files) formData.append("files", file);
|
||||||
|
const res = await apiRequest("POST", "/api/ai/detect-eligibility-info", formData);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json?.error) throw new Error(json.message || "Detection failed");
|
||||||
|
|
||||||
|
const memberId: string | null = json?.data?.memberId ?? null;
|
||||||
|
const dob: string | null = json?.data?.dob ?? null;
|
||||||
|
const autoCheck: string = json?.data?.autoCheck ?? "mh";
|
||||||
|
|
||||||
|
if (memberId && dob) {
|
||||||
|
setEligibilityIdData({
|
||||||
|
memberId,
|
||||||
|
dob,
|
||||||
|
siteKey: "",
|
||||||
|
autoCheck,
|
||||||
|
patient: null,
|
||||||
|
appointmentDate: null,
|
||||||
|
});
|
||||||
|
replaceLastMsg(`I read this from your screenshot — ready to check ${autoCheck === "tufts-sco" ? "Tufts SCO" : "MassHealth"} for ${memberId}. Confirm to proceed.`);
|
||||||
|
setStep("eligibility-id-ready");
|
||||||
|
} else {
|
||||||
|
setPasteInput([memberId, dob].filter(Boolean).join(" "));
|
||||||
|
replaceLastMsg("I saved your screenshot but couldn't fully read the Member ID/DOB. Please review and complete the fields below:");
|
||||||
|
setStep("eligibility-input");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
replaceLastMsg("Sorry, I couldn't read that screenshot. Please try again.");
|
||||||
|
setStep("menu");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFreeTextSubmit = async () => {
|
const handleFreeTextSubmit = async () => {
|
||||||
const text = freeTextInput.trim();
|
const text = freeTextInput.trim();
|
||||||
if (!text || step === "ai-loading") return;
|
if (step === "ai-loading") return;
|
||||||
|
if (!text) {
|
||||||
|
if (pendingFiles.length > 0) return handleScreenshotOnlyDetect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFreeTextInput("");
|
setFreeTextInput("");
|
||||||
addMsg("user", text);
|
addMsg("user", text);
|
||||||
addMsg("bot", "Thinking...", true);
|
addMsg("bot", "Thinking...", true);
|
||||||
@@ -1785,7 +1832,7 @@ export function ChatbotButton() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-9 w-9 p-0 shrink-0"
|
className="h-9 w-9 p-0 shrink-0"
|
||||||
onClick={handleFreeTextSubmit}
|
onClick={handleFreeTextSubmit}
|
||||||
disabled={!freeTextInput.trim() || step === "ai-loading"}
|
disabled={(!freeTextInput.trim() && pendingFiles.length === 0) || step === "ai-loading"}
|
||||||
>
|
>
|
||||||
{step === "ai-loading" ? (
|
{step === "ai-loading" ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
|||||||
@@ -301,13 +301,14 @@ export default function AiCopyAgentPage() {
|
|||||||
|
|
||||||
const memberId: string | null = detectResult.value?.data?.memberId ?? null;
|
const memberId: string | null = detectResult.value?.data?.memberId ?? null;
|
||||||
const dob: string | null = detectResult.value?.data?.dob ?? null;
|
const dob: string | null = detectResult.value?.data?.dob ?? null;
|
||||||
|
const autoCheck: string = detectResult.value?.data?.autoCheck ?? "mh";
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("chatbot:open-eligibility-id-ready", {
|
new CustomEvent("chatbot:open-eligibility-id-ready", {
|
||||||
detail: {
|
detail: {
|
||||||
memberId,
|
memberId,
|
||||||
dob,
|
dob,
|
||||||
autoCheck: "mh",
|
autoCheck,
|
||||||
patient: {
|
patient: {
|
||||||
id: patient.id,
|
id: patient.id,
|
||||||
firstName: patient.firstName,
|
firstName: patient.firstName,
|
||||||
@@ -336,11 +337,11 @@ export default function AiCopyAgentPage() {
|
|||||||
try {
|
try {
|
||||||
const files = await itemsToFiles(items);
|
const files = await itemsToFiles(items);
|
||||||
|
|
||||||
let detected: { memberId: string | null; dob: string | null };
|
let detected: { memberId: string | null; dob: string | null; autoCheck: string };
|
||||||
try {
|
try {
|
||||||
const json = await detectFromFiles(files);
|
const json = await detectFromFiles(files);
|
||||||
if (json?.error) throw new Error(json.message || "Detection failed");
|
if (json?.error) throw new Error(json.message || "Detection failed");
|
||||||
detected = { memberId: json?.data?.memberId ?? null, dob: json?.data?.dob ?? null };
|
detected = { memberId: json?.data?.memberId ?? null, dob: json?.data?.dob ?? null, autoCheck: json?.data?.autoCheck ?? "mh" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error detecting Member ID/DOB:", error);
|
console.error("Error detecting Member ID/DOB:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -351,7 +352,7 @@ export default function AiCopyAgentPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { memberId, dob } = detected;
|
const { memberId, dob, autoCheck } = detected;
|
||||||
|
|
||||||
if (memberId && dob) {
|
if (memberId && dob) {
|
||||||
let tempPatient: TempPatient;
|
let tempPatient: TempPatient;
|
||||||
@@ -395,7 +396,7 @@ export default function AiCopyAgentPage() {
|
|||||||
detail: {
|
detail: {
|
||||||
memberId,
|
memberId,
|
||||||
dob,
|
dob,
|
||||||
autoCheck: "mh",
|
autoCheck,
|
||||||
patient: {
|
patient: {
|
||||||
id: tempPatient.id,
|
id: tempPatient.id,
|
||||||
firstName: tempPatient.firstName,
|
firstName: tempPatient.firstName,
|
||||||
@@ -424,7 +425,7 @@ export default function AiCopyAgentPage() {
|
|||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("chatbot:open-eligibility-id-ready", {
|
new CustomEvent("chatbot:open-eligibility-id-ready", {
|
||||||
detail: { memberId, dob, autoCheck: "mh", patient: null },
|
detail: { memberId, dob, autoCheck, patient: null },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -596,6 +596,36 @@ class AutomationTuftsSCOPreAuth:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[TuftsSCO PreAuth step4] Warning: could not fill {label}: {e}")
|
print(f"[TuftsSCO PreAuth step4] Warning: could not fill {label}: {e}")
|
||||||
|
|
||||||
|
def _fill_tooth_field(self, idx, tooth, context=""):
|
||||||
|
"""Fill the Tooth input for row idx, verifying the DOM value against what was typed
|
||||||
|
and retrying if it didn't stick. Entering the same procedure code on multiple rows
|
||||||
|
(e.g. one code across several teeth) triggers a duplicate-code warning banner that can
|
||||||
|
re-render the form and silently reset earlier rows' Tooth values — callers should re-run
|
||||||
|
this for every row after the whole form is filled, not just once at type-time."""
|
||||||
|
tooth = str(tooth).strip()
|
||||||
|
actual = None
|
||||||
|
for attempt in range(3):
|
||||||
|
tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']")
|
||||||
|
if idx >= len(tooth_inputs):
|
||||||
|
return None
|
||||||
|
tooth_inp = tooth_inputs[idx]
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_inp)
|
||||||
|
self.driver.execute_script("arguments[0].focus();", tooth_inp)
|
||||||
|
time.sleep(0.2)
|
||||||
|
tooth_inp.send_keys(Keys.CONTROL + "a")
|
||||||
|
tooth_inp.send_keys(Keys.DELETE)
|
||||||
|
tooth_inp.send_keys(tooth)
|
||||||
|
time.sleep(0.3)
|
||||||
|
actual = (tooth_inp.get_attribute("value") or "").strip()
|
||||||
|
if actual == tooth:
|
||||||
|
print(f"[TuftsSCO PreAuth step4] tooth[{idx}]{context}: confirmed '{tooth}'")
|
||||||
|
return actual
|
||||||
|
print(f"[TuftsSCO PreAuth step4] tooth[{idx}]{context}: attempt {attempt + 1} — "
|
||||||
|
f"expected '{tooth}', field shows {actual!r}, retrying")
|
||||||
|
print(f"[TuftsSCO PreAuth step4] WARNING: tooth[{idx}]{context} did not stick — "
|
||||||
|
f"expected '{tooth}', field shows {actual!r} after 3 attempts")
|
||||||
|
return actual
|
||||||
|
|
||||||
def _fill_text_input(self, inp, value, label="field"):
|
def _fill_text_input(self, inp, value, label="field"):
|
||||||
try:
|
try:
|
||||||
inp.click()
|
inp.click()
|
||||||
@@ -655,17 +685,8 @@ class AutomationTuftsSCOPreAuth:
|
|||||||
|
|
||||||
if tooth:
|
if tooth:
|
||||||
try:
|
try:
|
||||||
tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']")
|
self._fill_tooth_field(idx, tooth)
|
||||||
if idx < len(tooth_inputs):
|
time.sleep(0.1)
|
||||||
tooth_inp = tooth_inputs[idx]
|
|
||||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_inp)
|
|
||||||
self.driver.execute_script("arguments[0].focus();", tooth_inp)
|
|
||||||
time.sleep(0.2)
|
|
||||||
tooth_inp.send_keys(Keys.CONTROL + "a")
|
|
||||||
tooth_inp.send_keys(Keys.DELETE)
|
|
||||||
tooth_inp.send_keys(str(tooth))
|
|
||||||
print(f"[TuftsSCO PreAuth step4] tooth[{idx}]: typed '{tooth}'")
|
|
||||||
time.sleep(0.3)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[TuftsSCO PreAuth step4] Could not fill tooth: {e}")
|
print(f"[TuftsSCO PreAuth step4] Could not fill tooth: {e}")
|
||||||
|
|
||||||
@@ -714,6 +735,24 @@ class AutomationTuftsSCOPreAuth:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[TuftsSCO PreAuth step4] Could not fill billed amount: {e}")
|
print(f"[TuftsSCO PreAuth step4] Could not fill billed amount: {e}")
|
||||||
|
|
||||||
|
# Adding a row with a procedure code already used by an earlier row triggers a
|
||||||
|
# duplicate-code warning banner that re-renders the form — this can silently reset
|
||||||
|
# a previously-confirmed Tooth value on an earlier row. Re-check every row now that
|
||||||
|
# no more rows will be added (no more duplicate-code banners can fire after this).
|
||||||
|
print("[TuftsSCO PreAuth step4] Final tooth verification pass...")
|
||||||
|
for idx, line in enumerate(active_lines):
|
||||||
|
tooth = str(line.get("toothNumber") or line.get("tooth") or "").strip()
|
||||||
|
if not tooth:
|
||||||
|
continue
|
||||||
|
tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']")
|
||||||
|
if idx >= len(tooth_inputs):
|
||||||
|
continue
|
||||||
|
actual = (tooth_inputs[idx].get_attribute("value") or "").strip()
|
||||||
|
if actual == tooth:
|
||||||
|
continue
|
||||||
|
print(f"[TuftsSCO PreAuth step4] Final check: tooth[{idx}] regressed to {actual!r}, re-filling '{tooth}'")
|
||||||
|
self._fill_tooth_field(idx, tooth, context=" (final pass)")
|
||||||
|
|
||||||
print("[TuftsSCO PreAuth step4] Done")
|
print("[TuftsSCO PreAuth step4] Done")
|
||||||
return "SUCCESS"
|
return "SUCCESS"
|
||||||
|
|
||||||
@@ -777,27 +816,56 @@ class AutomationTuftsSCOPreAuth:
|
|||||||
# ── Step 6: Click "Next step" ──────────────────────────────────────────────
|
# ── Step 6: Click "Next step" ──────────────────────────────────────────────
|
||||||
|
|
||||||
def step6_click_next(self):
|
def step6_click_next(self):
|
||||||
"""Click the 'Next step' button."""
|
"""Click the 'Next step' button and verify the wizard actually advanced to the
|
||||||
|
acknowledgement page — the URL doesn't change (SPA), so a click that's silently
|
||||||
|
ignored (e.g. button still disabled from a validation error) would otherwise look
|
||||||
|
identical to a successful one in the logs."""
|
||||||
try:
|
try:
|
||||||
print(f"[TuftsSCO PreAuth step6] Current URL: {self.driver.current_url}")
|
print(f"[TuftsSCO PreAuth step6] Current URL: {self.driver.current_url}")
|
||||||
btn = WebDriverWait(self.driver, 15).until(
|
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
advanced = False
|
||||||
"//button[@data-testid='next-step-btn'] | "
|
for attempt in range(3):
|
||||||
"//button[@aria-label='Next step']"
|
btn = WebDriverWait(self.driver, 15).until(
|
||||||
))
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
)
|
"//button[@data-testid='next-step-btn'] | "
|
||||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
|
"//button[@aria-label='Next step']"
|
||||||
time.sleep(0.5)
|
))
|
||||||
self.driver.execute_script("""
|
)
|
||||||
var el = arguments[0];
|
disabled = (btn.get_attribute("disabled") is not None) or \
|
||||||
el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true}));
|
((btn.get_attribute("aria-disabled") or "").lower() == "true")
|
||||||
el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true}));
|
if disabled:
|
||||||
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true}));
|
print(f"[TuftsSCO PreAuth step6] 'Next step' is disabled (attempt {attempt + 1}) — "
|
||||||
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true}));
|
f"a form field may still be invalid/empty; waiting before retry")
|
||||||
""", btn)
|
time.sleep(1.5)
|
||||||
print("[TuftsSCO PreAuth step6] Clicked 'Next step'")
|
continue
|
||||||
time.sleep(2)
|
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.driver.execute_script("""
|
||||||
|
var el = arguments[0];
|
||||||
|
el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
""", btn)
|
||||||
|
print(f"[TuftsSCO PreAuth step6] Clicked 'Next step' (attempt {attempt + 1})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
WebDriverWait(self.driver, 6).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH,
|
||||||
|
"//label[contains(.,'submitting this')] | "
|
||||||
|
"//*[contains(@aria-label,'submitting this')]"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
advanced = True
|
||||||
|
print("[TuftsSCO PreAuth step6] Confirmed wizard advanced to the acknowledgement step")
|
||||||
|
break
|
||||||
|
except TimeoutException:
|
||||||
|
print(f"[TuftsSCO PreAuth step6] Page did not advance after click (attempt {attempt + 1}), retrying")
|
||||||
|
|
||||||
print(f"[TuftsSCO PreAuth step6] URL after Next: {self.driver.current_url}")
|
print(f"[TuftsSCO PreAuth step6] URL after Next: {self.driver.current_url}")
|
||||||
|
if not advanced:
|
||||||
|
return "ERROR: step6 failed: 'Next step' click did not advance the wizard after 3 attempts"
|
||||||
return "SUCCESS"
|
return "SUCCESS"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[TuftsSCO PreAuth step6] Exception: {e}")
|
print(f"[TuftsSCO PreAuth step6] Exception: {e}")
|
||||||
@@ -805,30 +873,65 @@ class AutomationTuftsSCOPreAuth:
|
|||||||
|
|
||||||
# ── Step 7: Acknowledge + submit ────────────────────────────────────────────
|
# ── Step 7: Acknowledge + submit ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_checkbox_checked(el):
|
||||||
|
try:
|
||||||
|
if el.is_selected():
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
state = (el.get_attribute("aria-checked") or el.get_attribute("checked") or "").lower()
|
||||||
|
return state in ("true", "checked")
|
||||||
|
|
||||||
def step7_submit_preauth(self):
|
def step7_submit_preauth(self):
|
||||||
"""On the pre-auth summary page, tick the acknowledgement checkbox then submit."""
|
"""On the pre-auth summary page, tick the acknowledgement checkbox then submit."""
|
||||||
try:
|
try:
|
||||||
print(f"[TuftsSCO PreAuth step7] Current URL: {self.driver.current_url}")
|
print(f"[TuftsSCO PreAuth step7] Current URL: {self.driver.current_url}")
|
||||||
|
|
||||||
checkbox = WebDriverWait(self.driver, 15).until(
|
# A bare "//input[@type='checkbox']" would match the FIRST checkbox in the DOM,
|
||||||
EC.presence_of_element_located((By.XPATH,
|
# which may not be the acknowledgement box (e.g. a row-selection checkbox in the
|
||||||
"//input[@type='checkbox'] | "
|
# service-line table). Try specific, label-anchored selectors before falling back.
|
||||||
"//*[@role='checkbox'] | "
|
checkbox = None
|
||||||
"//label[contains(.,'submitting this')]//input | "
|
for xpath in [
|
||||||
"//*[contains(@aria-label,'submitting this')]"
|
"//label[contains(.,'submitting this')]//input[@type='checkbox']",
|
||||||
))
|
"//*[contains(@aria-label,'submitting this')]",
|
||||||
)
|
"//label[contains(translate(., 'ACKNOWLEDGE', 'acknowledge'),'acknowledge')]//input[@type='checkbox']",
|
||||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", checkbox)
|
"//*[contains(translate(@aria-label, 'ACKNOWLEDGE', 'acknowledge'),'acknowledge')]",
|
||||||
time.sleep(0.3)
|
"//input[@type='checkbox']",
|
||||||
self.driver.execute_script("""
|
"//*[@role='checkbox']",
|
||||||
var el = arguments[0];
|
]:
|
||||||
el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true}));
|
try:
|
||||||
el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true}));
|
checkbox = WebDriverWait(self.driver, 5).until(
|
||||||
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true}));
|
EC.presence_of_element_located((By.XPATH, xpath))
|
||||||
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true}));
|
)
|
||||||
""", checkbox)
|
print(f"[TuftsSCO PreAuth step7] Found acknowledgement checkbox via: {xpath}")
|
||||||
print("[TuftsSCO PreAuth step7] Checked acknowledgement checkbox")
|
break
|
||||||
time.sleep(0.5)
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if checkbox is None:
|
||||||
|
return "ERROR: step7 failed: could not find acknowledgement checkbox"
|
||||||
|
|
||||||
|
for attempt in range(3):
|
||||||
|
if self._is_checkbox_checked(checkbox):
|
||||||
|
break
|
||||||
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", checkbox)
|
||||||
|
time.sleep(0.3)
|
||||||
|
self.driver.execute_script("""
|
||||||
|
var el = arguments[0];
|
||||||
|
el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true}));
|
||||||
|
""", checkbox)
|
||||||
|
time.sleep(0.5)
|
||||||
|
if attempt > 0:
|
||||||
|
print(f"[TuftsSCO PreAuth step7] Acknowledgement checkbox retry {attempt + 1}")
|
||||||
|
|
||||||
|
if self._is_checkbox_checked(checkbox):
|
||||||
|
print("[TuftsSCO PreAuth step7] Confirmed acknowledgement checkbox is checked")
|
||||||
|
else:
|
||||||
|
print("[TuftsSCO PreAuth step7] WARNING: acknowledgement checkbox did not register as checked after 3 attempts")
|
||||||
|
|
||||||
all_btns = self.driver.find_elements(By.XPATH, "//button")
|
all_btns = self.driver.find_elements(By.XPATH, "//button")
|
||||||
print(f"[TuftsSCO PreAuth step7] Buttons: {[b.get_attribute('aria-label') or b.text[:40] for b in all_btns]}")
|
print(f"[TuftsSCO PreAuth step7] Buttons: {[b.get_attribute('aria-label') or b.text[:40] for b in all_btns]}")
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Appointment" ADD COLUMN "npiProviderId" INTEGER;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_npiProviderId_fkey" FOREIGN KEY ("npiProviderId") REFERENCES "NpiProvider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"generatorVersion": "1.0.0",
|
"generatorVersion": "1.0.0",
|
||||||
"generatedAt": "2026-07-21T01:43:44.482Z",
|
"generatedAt": "2026-07-21T03:13:42.342Z",
|
||||||
"outputPath": "/home/ff/Desktop/DentalManagementMH07/packages/db/shared",
|
"outputPath": "/home/gg/Desktop/DentalManagementMH07/packages/db/shared",
|
||||||
"files": [
|
"files": [
|
||||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||||
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user