fix: United Ucard claim advances past Upload Documents
Fill the required Provider Narrative (claim remarks with shorthand expanded, or a plain-language summary of the procedures), find the Review and Sign button by its navy pill instead of the sidebar step, and verify each step is actually visible since every step lives in the DOM at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,7 @@ class AutomationUnitedUcardClaimSubmit:
|
||||
self.lastName = self.data.get("lastName", "")
|
||||
self.serviceLines = self.data.get("serviceLines", [])
|
||||
self.claimFiles = self.data.get("claimFiles", [])
|
||||
self.remarks = self.data.get("remarks", "")
|
||||
self.providerName = self.data.get("providerName", "")
|
||||
self.uniteducard_username = self.data.get("uniteducardUsername", "")
|
||||
self.uniteducard_password = self.data.get("uniteducardPassword", "")
|
||||
@@ -792,28 +793,212 @@ class AutomationUnitedUcardClaimSubmit:
|
||||
|
||||
print(f"[UnitedUcard Claim step5] Attached {attached}/{len(self.claimFiles)} file(s)")
|
||||
|
||||
try:
|
||||
review_label = WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//div[normalize-space(text())='Review and Sign']"))
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", review_label)
|
||||
time.sleep(0.3)
|
||||
target = review_label
|
||||
try:
|
||||
target = review_label.find_element(By.XPATH,
|
||||
"ancestor::*[self::button or self::a or @role='button' or @data-state][1]")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
target.click()
|
||||
except Exception:
|
||||
self.driver.execute_script("arguments[0].click();", target)
|
||||
print("[UnitedUcard Claim] Clicked 'Review and Sign'")
|
||||
time.sleep(2)
|
||||
result = self._fill_provider_narrative()
|
||||
if isinstance(result, str) and result.startswith("ERROR"):
|
||||
return result
|
||||
|
||||
if self._click_review_and_sign_button():
|
||||
return True
|
||||
return "ERROR: Clicked 'Review and Sign' but the Review and Sign step never appeared (stuck on Upload Documents)"
|
||||
|
||||
@staticmethod
|
||||
def _describe_procedure(code):
|
||||
"""Plain-language name for a CDT code (falls back to the code itself)."""
|
||||
m = re.match(r"^D(\d{4})", code.upper())
|
||||
n = int(m.group(1)) if m else -1
|
||||
if 2140 <= n <= 2161:
|
||||
return "amalgam filling"
|
||||
if 2330 <= n <= 2394:
|
||||
return "composite filling"
|
||||
if 2510 <= n <= 2664:
|
||||
return "inlay/onlay"
|
||||
if 2710 <= n <= 2799:
|
||||
return "crown"
|
||||
if 3310 <= n <= 3348:
|
||||
return "root canal treatment"
|
||||
if n in (7140, 7210):
|
||||
return "extraction"
|
||||
if n == 1110:
|
||||
return "adult prophylaxis"
|
||||
if n == 1120:
|
||||
return "child prophylaxis"
|
||||
return code
|
||||
|
||||
@staticmethod
|
||||
def _expand_narrative_shorthand(text):
|
||||
"""Expand office shorthand, e.g. "comp #8 MDB" -> "composite filling on #8 MDB"."""
|
||||
text = re.sub(r"\bcomp(?:osite)?\b(?:\s+on\b)?", "composite filling on", text, flags=re.I)
|
||||
text = re.sub(r"\bamal(?:gam)?\b(?:\s+on\b)?", "amalgam filling on", text, flags=re.I)
|
||||
return text
|
||||
|
||||
def _build_provider_narrative(self):
|
||||
"""
|
||||
Claim remarks (with shorthand expanded), or a description built from
|
||||
the service lines, e.g. "Composite filling on #8 MDB."
|
||||
"""
|
||||
text = self._expand_narrative_shorthand((self.remarks or "").strip())
|
||||
if not text:
|
||||
parts = []
|
||||
for line in self.serviceLines:
|
||||
code = str(line.get("procedureCode", "")).strip()
|
||||
if not code:
|
||||
continue
|
||||
tooth = str(line.get("toothNumber") or line.get("tooth") or "").strip()
|
||||
surface = str(line.get("toothSurface") or line.get("surface") or "").strip().upper()
|
||||
desc = self._describe_procedure(code)
|
||||
if tooth:
|
||||
desc += f" on #{tooth}"
|
||||
if surface:
|
||||
desc += f" {surface}"
|
||||
parts.append(desc)
|
||||
text = "; ".join(parts)
|
||||
if text:
|
||||
text += "."
|
||||
text = " ".join(text.split())
|
||||
if text:
|
||||
text = text[0].upper() + text[1:]
|
||||
return text[:250]
|
||||
|
||||
def _fill_provider_narrative(self):
|
||||
"""
|
||||
The site requires a Provider Narrative before "Review and Sign" will
|
||||
advance (the box turns red otherwise).
|
||||
|
||||
Confirmed against the live site (2026-10-22):
|
||||
<textarea id="narrative" class="narrative-textarea" maxlength="250"
|
||||
placeholder="Type provider narrative here"></textarea>
|
||||
"""
|
||||
narrative = self._build_provider_narrative()
|
||||
try:
|
||||
box = WebDriverWait(self.driver, 10).until(
|
||||
EC.visibility_of_element_located((By.ID, "narrative"))
|
||||
)
|
||||
except TimeoutException:
|
||||
return "ERROR: 'Review and Sign' button not found on Upload Documents step"
|
||||
return "ERROR: Provider Narrative box (#narrative) not found on Upload Documents step"
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", box)
|
||||
box.click()
|
||||
box.clear()
|
||||
box.send_keys(narrative)
|
||||
print(f"[UnitedUcard Claim] Entered Provider Narrative: {narrative}")
|
||||
return True
|
||||
|
||||
def _click_review_and_sign_button(self):
|
||||
"""
|
||||
Click "Review and Sign" on the Upload Documents step and confirm the
|
||||
Review and Sign step actually rendered. Every step lives in the DOM
|
||||
at once (hidden when inactive), so presence checks prove nothing —
|
||||
wait for a *visible* button, and for #userSignatureText to become
|
||||
visible afterwards.
|
||||
|
||||
Confirmed against the live site (2026-10-22):
|
||||
<textarea id="narrative" class="narrative-textarea" ...> (Provider Narrative, above the button)
|
||||
<div data-state="Disabled" ...><div ...>Review and Sign</div></div>
|
||||
"""
|
||||
# "Review and Sign" also appears in the left step-nav sidebar (greyed
|
||||
# out, not clickable) — target the bottom primary pill, falling back
|
||||
# to the lowest visible match on the page.
|
||||
primary_xpath = "//*[@data-variant='Primary'][normalize-space(.)='Review and Sign']"
|
||||
any_xpath = "//*[normalize-space(.)='Review and Sign' and not(*[normalize-space(.)='Review and Sign'])]"
|
||||
|
||||
def blue_button(driver):
|
||||
# The real button is the visible navy (#002677 = rgb(0, 38, 119))
|
||||
# pill; the sidebar step label has no such background.
|
||||
return driver.execute_script("""
|
||||
var hits = Array.from(document.querySelectorAll('div, button, a')).filter(function(el) {
|
||||
if (el.textContent.trim() !== 'Review and Sign') return false;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width === 0 || r.height === 0) return false;
|
||||
var bg = getComputedStyle(el).backgroundColor.replace(/\\s/g, '');
|
||||
return bg === 'rgb(0,38,119)' || bg === 'rgba(0,38,119,1)';
|
||||
});
|
||||
return hits.length ? hits[hits.length - 1] : null;
|
||||
""")
|
||||
|
||||
def visible_label(driver):
|
||||
el = blue_button(driver)
|
||||
if el:
|
||||
return el
|
||||
shown = [el for el in driver.find_elements(By.XPATH, primary_xpath) if el.is_displayed()]
|
||||
if not shown:
|
||||
shown = [el for el in driver.find_elements(By.XPATH, any_xpath) if el.is_displayed()]
|
||||
if not shown:
|
||||
return False
|
||||
return max(shown, key=lambda el: el.rect.get("y", 0))
|
||||
|
||||
def review_step_shown():
|
||||
try:
|
||||
WebDriverWait(self.driver, 6).until(
|
||||
EC.visibility_of_element_located((By.ID, "userSignatureText"))
|
||||
)
|
||||
return True
|
||||
except TimeoutException:
|
||||
return False
|
||||
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
review_label = WebDriverWait(self.driver, 15).until(visible_label)
|
||||
except TimeoutException:
|
||||
print("[UnitedUcard Claim] No visible 'Review and Sign' button found")
|
||||
return False
|
||||
|
||||
# Make sure the Provider Narrative textarea isn't holding focus.
|
||||
self.driver.execute_script("if (document.activeElement) document.activeElement.blur();")
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", review_label)
|
||||
time.sleep(0.5)
|
||||
|
||||
if attempt == 1:
|
||||
try:
|
||||
review_label.click()
|
||||
how = "click"
|
||||
except Exception:
|
||||
self.driver.execute_script("arguments[0].click();", review_label)
|
||||
how = "JS click"
|
||||
elif attempt == 2:
|
||||
self.driver.execute_script("arguments[0].click();", review_label)
|
||||
how = "JS click"
|
||||
else:
|
||||
self.driver.execute_script(
|
||||
"var el = arguments[0];"
|
||||
"['pointerdown','mousedown','pointerup','mouseup','click'].forEach(function(t){"
|
||||
" el.dispatchEvent(new MouseEvent(t, {bubbles: true, cancelable: true, view: window}));"
|
||||
"});", review_label)
|
||||
how = "mouse events"
|
||||
|
||||
rect = review_label.rect
|
||||
print(f"[UnitedUcard Claim] Clicked 'Review and Sign' (attempt {attempt}, {how}) "
|
||||
f"on <{review_label.tag_name} data-variant={review_label.get_attribute('data-variant')}> "
|
||||
f"at x={rect.get('x')}, y={rect.get('y')}, w={rect.get('width')}")
|
||||
if review_step_shown():
|
||||
return True
|
||||
print("[UnitedUcard Claim] Review and Sign step not visible yet")
|
||||
|
||||
return False
|
||||
|
||||
def _check_checkbox_by_id(self, element_id, label=""):
|
||||
try:
|
||||
box = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.ID, element_id))
|
||||
)
|
||||
except TimeoutException:
|
||||
return f"ERROR: Checkbox #{element_id} ({label}) not found on Review and Sign step"
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", box)
|
||||
time.sleep(0.2)
|
||||
if not box.is_selected():
|
||||
try:
|
||||
box.click()
|
||||
except Exception:
|
||||
# Styled checkbox: the real input is hidden, so click its label,
|
||||
# falling back to a JS click on the input itself.
|
||||
try:
|
||||
self.driver.find_element(By.CSS_SELECTOR, f"label[for='{element_id}']").click()
|
||||
except Exception:
|
||||
self.driver.execute_script("arguments[0].click();", box)
|
||||
if not box.is_selected():
|
||||
self.driver.execute_script(
|
||||
"arguments[0].checked = true;"
|
||||
"arguments[0].dispatchEvent(new Event('change', {bubbles: true}));", box)
|
||||
print(f"[UnitedUcard Claim] Checked '{label or element_id}'")
|
||||
return True
|
||||
|
||||
def step6_review_and_sign(self):
|
||||
"""
|
||||
@@ -824,11 +1009,19 @@ class AutomationUnitedUcardClaimSubmit:
|
||||
<input type="checkbox" name="MemberPatientSignatureOnFile" id="MemberPatientSignatureOnFile_0" value="yes">
|
||||
<input type="checkbox" name="PHSubscriberSignatureOnFile" id="PHSubscriberSignatureOnFile_0" value="yes">
|
||||
"""
|
||||
# Step5 already confirmed the step is visible; this is a safety net.
|
||||
try:
|
||||
WebDriverWait(self.driver, 10).until(
|
||||
EC.visibility_of_element_located((By.ID, "userSignatureText"))
|
||||
)
|
||||
except TimeoutException:
|
||||
return "ERROR: Review and Sign step is not visible (still on Upload Documents?)"
|
||||
|
||||
for element_id, label in [
|
||||
("MemberPatientSignatureOnFile_0", "Patient signature on file"),
|
||||
("PHSubscriberSignatureOnFile_0", "Subscriber signature on file"),
|
||||
]:
|
||||
result = self._click_radio_by_id(element_id, label)
|
||||
result = self._check_checkbox_by_id(element_id, label)
|
||||
if isinstance(result, str) and result.startswith("ERROR"):
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user