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",
|
||||
text:
|
||||
"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) => ({
|
||||
type: "image_url",
|
||||
@@ -390,10 +391,11 @@ router.post(
|
||||
|
||||
let memberId: string | null = null;
|
||||
let dob: string | null = null;
|
||||
let insuranceProvider: string | null = null;
|
||||
try {
|
||||
const raw = String(response.content).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
|
||||
// used downstream (temp-patient creation, chatbot prefill, real eligibility check) —
|
||||
// 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, "");
|
||||
memberId = cleanedMemberId || null;
|
||||
dob = parsed.dob || null;
|
||||
insuranceProvider = parsed.insuranceProvider || null;
|
||||
} catch (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) {
|
||||
console.error("[detect-eligibility-info]", 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]);
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
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;
|
||||
try {
|
||||
const { codes, serviceDate } = JSON.parse(raw) as {
|
||||
|
||||
@@ -398,7 +398,7 @@ export function ChatbotButton() {
|
||||
});
|
||||
addMsg(
|
||||
"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
|
||||
}. Confirm to proceed.`
|
||||
);
|
||||
@@ -663,9 +663,56 @@ export function ChatbotButton() {
|
||||
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 text = freeTextInput.trim();
|
||||
if (!text || step === "ai-loading") return;
|
||||
if (step === "ai-loading") return;
|
||||
if (!text) {
|
||||
if (pendingFiles.length > 0) return handleScreenshotOnlyDetect();
|
||||
return;
|
||||
}
|
||||
setFreeTextInput("");
|
||||
addMsg("user", text);
|
||||
addMsg("bot", "Thinking...", true);
|
||||
@@ -1785,7 +1832,7 @@ export function ChatbotButton() {
|
||||
size="sm"
|
||||
className="h-9 w-9 p-0 shrink-0"
|
||||
onClick={handleFreeTextSubmit}
|
||||
disabled={!freeTextInput.trim() || step === "ai-loading"}
|
||||
disabled={(!freeTextInput.trim() && pendingFiles.length === 0) || step === "ai-loading"}
|
||||
>
|
||||
{step === "ai-loading" ? (
|
||||
<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 dob: string | null = detectResult.value?.data?.dob ?? null;
|
||||
const autoCheck: string = detectResult.value?.data?.autoCheck ?? "mh";
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chatbot:open-eligibility-id-ready", {
|
||||
detail: {
|
||||
memberId,
|
||||
dob,
|
||||
autoCheck: "mh",
|
||||
autoCheck,
|
||||
patient: {
|
||||
id: patient.id,
|
||||
firstName: patient.firstName,
|
||||
@@ -336,11 +337,11 @@ export default function AiCopyAgentPage() {
|
||||
try {
|
||||
const files = await itemsToFiles(items);
|
||||
|
||||
let detected: { memberId: string | null; dob: string | null };
|
||||
let detected: { memberId: string | null; dob: string | null; autoCheck: string };
|
||||
try {
|
||||
const json = await detectFromFiles(files);
|
||||
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) {
|
||||
console.error("Error detecting Member ID/DOB:", error);
|
||||
toast({
|
||||
@@ -351,7 +352,7 @@ export default function AiCopyAgentPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { memberId, dob } = detected;
|
||||
const { memberId, dob, autoCheck } = detected;
|
||||
|
||||
if (memberId && dob) {
|
||||
let tempPatient: TempPatient;
|
||||
@@ -395,7 +396,7 @@ export default function AiCopyAgentPage() {
|
||||
detail: {
|
||||
memberId,
|
||||
dob,
|
||||
autoCheck: "mh",
|
||||
autoCheck,
|
||||
patient: {
|
||||
id: tempPatient.id,
|
||||
firstName: tempPatient.firstName,
|
||||
@@ -424,7 +425,7 @@ export default function AiCopyAgentPage() {
|
||||
|
||||
window.dispatchEvent(
|
||||
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:
|
||||
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"):
|
||||
try:
|
||||
inp.click()
|
||||
@@ -655,17 +685,8 @@ class AutomationTuftsSCOPreAuth:
|
||||
|
||||
if tooth:
|
||||
try:
|
||||
tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']")
|
||||
if idx < len(tooth_inputs):
|
||||
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)
|
||||
self._fill_tooth_field(idx, tooth)
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
print(f"[TuftsSCO PreAuth step4] Could not fill tooth: {e}")
|
||||
|
||||
@@ -714,6 +735,24 @@ class AutomationTuftsSCOPreAuth:
|
||||
except Exception as 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")
|
||||
return "SUCCESS"
|
||||
|
||||
@@ -777,27 +816,56 @@ class AutomationTuftsSCOPreAuth:
|
||||
# ── Step 6: Click "Next step" ──────────────────────────────────────────────
|
||||
|
||||
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:
|
||||
print(f"[TuftsSCO PreAuth step6] Current URL: {self.driver.current_url}")
|
||||
btn = WebDriverWait(self.driver, 15).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//button[@data-testid='next-step-btn'] | "
|
||||
"//button[@aria-label='Next step']"
|
||||
))
|
||||
)
|
||||
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("[TuftsSCO PreAuth step6] Clicked 'Next step'")
|
||||
time.sleep(2)
|
||||
|
||||
advanced = False
|
||||
for attempt in range(3):
|
||||
btn = WebDriverWait(self.driver, 15).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//button[@data-testid='next-step-btn'] | "
|
||||
"//button[@aria-label='Next step']"
|
||||
))
|
||||
)
|
||||
disabled = (btn.get_attribute("disabled") is not None) or \
|
||||
((btn.get_attribute("aria-disabled") or "").lower() == "true")
|
||||
if disabled:
|
||||
print(f"[TuftsSCO PreAuth step6] 'Next step' is disabled (attempt {attempt + 1}) — "
|
||||
f"a form field may still be invalid/empty; waiting before retry")
|
||||
time.sleep(1.5)
|
||||
continue
|
||||
|
||||
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}")
|
||||
if not advanced:
|
||||
return "ERROR: step6 failed: 'Next step' click did not advance the wizard after 3 attempts"
|
||||
return "SUCCESS"
|
||||
except Exception as e:
|
||||
print(f"[TuftsSCO PreAuth step6] Exception: {e}")
|
||||
@@ -805,30 +873,65 @@ class AutomationTuftsSCOPreAuth:
|
||||
|
||||
# ── 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):
|
||||
"""On the pre-auth summary page, tick the acknowledgement checkbox then submit."""
|
||||
try:
|
||||
print(f"[TuftsSCO PreAuth step7] Current URL: {self.driver.current_url}")
|
||||
|
||||
checkbox = WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//input[@type='checkbox'] | "
|
||||
"//*[@role='checkbox'] | "
|
||||
"//label[contains(.,'submitting this')]//input | "
|
||||
"//*[contains(@aria-label,'submitting this')]"
|
||||
))
|
||||
)
|
||||
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)
|
||||
print("[TuftsSCO PreAuth step7] Checked acknowledgement checkbox")
|
||||
time.sleep(0.5)
|
||||
# A bare "//input[@type='checkbox']" would match the FIRST checkbox in the DOM,
|
||||
# which may not be the acknowledgement box (e.g. a row-selection checkbox in the
|
||||
# service-line table). Try specific, label-anchored selectors before falling back.
|
||||
checkbox = None
|
||||
for xpath in [
|
||||
"//label[contains(.,'submitting this')]//input[@type='checkbox']",
|
||||
"//*[contains(@aria-label,'submitting this')]",
|
||||
"//label[contains(translate(., 'ACKNOWLEDGE', 'acknowledge'),'acknowledge')]//input[@type='checkbox']",
|
||||
"//*[contains(translate(@aria-label, 'ACKNOWLEDGE', 'acknowledge'),'acknowledge')]",
|
||||
"//input[@type='checkbox']",
|
||||
"//*[@role='checkbox']",
|
||||
]:
|
||||
try:
|
||||
checkbox = WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, xpath))
|
||||
)
|
||||
print(f"[TuftsSCO PreAuth step7] Found acknowledgement checkbox via: {xpath}")
|
||||
break
|
||||
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")
|
||||
print(f"[TuftsSCO PreAuth step7] Buttons: {[b.get_attribute('aria-label') or b.text[:40] for b in all_btns]}")
|
||||
|
||||
@@ -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",
|
||||
"generatorVersion": "1.0.0",
|
||||
"generatedAt": "2026-07-21T01:43:44.482Z",
|
||||
"outputPath": "/home/ff/Desktop/DentalManagementMH07/packages/db/shared",
|
||||
"generatedAt": "2026-07-21T03:13:42.342Z",
|
||||
"outputPath": "/home/gg/Desktop/DentalManagementMH07/packages/db/shared",
|
||||
"files": [
|
||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||
"schemas/enums/UserScalarFieldEnum.schema.ts",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user