fix: CCA claim submission grid targeting, retry, and dashboard claim-number lookup

- Fix NameError (col_idx vs col_expr) that silently corrupted grid-fill logs
- Poll for new grid rows to render before clicking, instead of failing immediately
- Abort submission with a clear error if code/tooth/billed amount fail to fill,
  instead of silently submitting an incomplete claim
- Compute grid column positions live from header metadata instead of stale
  hardcoded indices; billed amount was landing on a hidden helper column
  (TAB_FROM_CODE_CELL) via td[last()] instead of the real Billed Amt column
- Read claim/encounter number from the top (newest) dashboard row instead
  of the bottom row
This commit is contained in:
2026-07-25 09:49:53 -04:00
parent d5cecb0b54
commit f457d6282a

View File

@@ -446,14 +446,13 @@ class AutomationCCAClaimSubmit:
time.sleep(1)
print(f"[CCA Claim step4] Capturing dashboard — URL: {self.driver.current_url}")
# Extract Encounter ID from the LAST row (newest claim is at the bottom)
# Extract Encounter ID from the FIRST row (newest claim is at the top)
claim_number = None
# Try table cells first (Encounter ID is the first column)
for sel in [
(By.XPATH, "//table//tbody//tr[last()]/td[1]"),
(By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[last()]/td[1]"),
(By.XPATH, "//table//tbody//tr[last()-0]/td[1]"),
(By.XPATH, "//table//tbody//tr[1]/td[1]"),
(By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[1]/td[1]"),
]:
try:
cell = WebDriverWait(self.driver, 8).until(
@@ -461,7 +460,7 @@ class AutomationCCAClaimSubmit:
text = cell.text.strip()
if re.match(r'\d{10,}', text):
claim_number = text
print(f"[CCA Claim step4] Encounter ID from last row: {claim_number}")
print(f"[CCA Claim step4] Encounter ID from top row: {claim_number}")
break
except Exception:
continue
@@ -591,14 +590,12 @@ class AutomationCCAClaimSubmit:
# ------------------------------------------------------------------ #
def _build_col_map(self):
"""
Map column names to 1-based td positions using ej-mappingname divs.
Falls back to hardcoded positions based on the known PDF column order.
Map column names to 1-based td positions using ej-mappingname divs,
in the order they actually appear in the live grid header (1-based
index = td position). Falls back to hardcoded positions (stale —
do not trust for BILLED_AMOUNT) only if the live scan fails.
"""
# Hardcoded based on PDF column order:
# rownum(1), Code(2), Desc(3), Tooth(4),
# Surf1-5(5-9), OralCavity1-4(10-13), DiagPtr1-4(14-17),
# EPSDT(18), Qty(19), Auth(20), ServiceDate(21), BilledAmt(22)
col_map = {
fallback_col_map = {
"CODE": 2,
"TOOTH": 4,
"SURF1": 5, "S1": 5,
@@ -608,10 +605,9 @@ class AutomationCCAClaimSubmit:
"SURF5": 9, "S5": 9,
"QUANTITY": 19, "QTY": 19,
"DOS": 21, "SERVICE DATE": 21, "SERVICEDATE": 21,
"BILLED_AMOUNT": 22, "BILLED AMT": 22, "BILLED A": 22,
"BILLED_AMOUNT": 19, "BILLED AMT": 19, "BILLED A": 19,
}
# Try to confirm/override positions using ej-mappingname divs in header
try:
divs = self.driver.find_elements(
By.XPATH,
@@ -619,14 +615,29 @@ class AutomationCCAClaimSubmit:
"//div[@id='ServicesGrid']//div[@ej-mappingname]"
)
print(f"[CCA Claim grid] Found {len(divs)} ej-mappingname header divs")
for div in divs:
mapping = div.get_attribute("ej-mappingname") or ""
col_map = {}
for i, div in enumerate(divs):
mapping = (div.get_attribute("ej-mappingname") or "").strip().upper()
text = div.text.strip()
print(f"[CCA Claim grid] Header: mapping={mapping!r} text={text!r}")
print(f"[CCA Claim grid] Header[{i+1}]: mapping={mapping!r} text={text!r}")
if mapping:
col_map[mapping] = i + 1
# Aliases for the surface columns, which have empty mapping
# names in some grid states — fill in from SURFACE1..5 if present.
for n in range(1, 6):
key = f"SURFACE{n}"
if key in col_map:
col_map[f"SURF{n}"] = col_map[key]
col_map[f"S{n}"] = col_map[key]
if col_map:
return col_map
except Exception as e:
print(f"[CCA Claim grid] Header scan error: {e}")
return col_map
print("[CCA Claim grid] Falling back to hardcoded column map")
return fallback_col_map
def _dbl_click_col(self, row_num, col_idx):
"""Double-click the cell at (row_num, col_idx) — both 1-based.
@@ -733,38 +744,51 @@ class AutomationCCAClaimSubmit:
cell_clicked = False
# --- Single-click cell to activate (yellow), then fill input ---
def click_cell_and_fill(col_expr, input_id, value):
def click_cell_and_fill(col_expr, input_id, value, row_timeout=6):
"""
Click the cell at (row_num, col_expr) to activate it (turns yellow),
then fill the pre-rendered input by ID.
col_expr can be a number like 2 or an XPath expression like 'last()' or 'last()-1'.
Polls for up to row_timeout seconds since a new row is rendered
asynchronously by the grid after the previous row commits.
"""
# Find the cell in the grid CONTENT rows (not headers)
cell = None
for xpath in [
xpaths = [
f"(//div[contains(@class,'e-gridcontent')]//tr)[{row_num}]/td[{col_expr}]",
f"(//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]",
f"(//div[@id='Services']//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]",
f"(//tr[@aria-rowindex='{row_num-1}'])/td[{col_expr}]",
]:
try:
cell = self.driver.find_element(By.XPATH, xpath)
break
except Exception:
continue
]
if cell:
try:
self.driver.execute_script(
"arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});",
cell)
cell.click()
deadline = time.monotonic() + row_timeout
clicked = False
while time.monotonic() < deadline and not clicked:
cell = None
for xpath in xpaths:
try:
cell = self.driver.find_element(By.XPATH, xpath)
break
except Exception:
continue
if cell:
try:
self.driver.execute_script(
"arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});",
cell)
cell.click()
clicked = True
time.sleep(0.3)
print(f"[CCA Claim grid] Clicked row={row_num} col={col_expr}")
except Exception as e:
print(f"[CCA Claim grid] Cell click failed, retrying: {e}")
time.sleep(0.3)
else:
print(f"[CCA Claim grid] Cell not found yet row={row_num} col={col_expr}, retrying")
time.sleep(0.3)
print(f"[CCA Claim grid] Clicked row={row_num} col={col_idx}")
except Exception as e:
print(f"[CCA Claim grid] Cell click failed: {e}")
else:
print(f"[CCA Claim grid] Cell not found row={row_num} col={col_idx}")
if not clicked:
print(f"[CCA Claim grid] Gave up clicking row={row_num} col={col_expr} after {row_timeout}s")
return False
# Fill the pre-rendered input
try:
@@ -785,23 +809,31 @@ class AutomationCCAClaimSubmit:
print(f"[CCA Claim grid] Fill {input_id} failed: {e}")
return False
# CODE (col 2)
click_cell_and_fill(2, "ServicesCODE", code)
# CODE
code_ok = click_cell_and_fill(col_map["CODE"], "ServicesCODE", code)
# TOOTH (col 4) — skip if empty
# TOOTH — skip if empty
tooth_ok = True
if tooth:
click_cell_and_fill(4, "ServicesTOOTH", tooth)
tooth_ok = click_cell_and_fill(col_map["TOOTH"], "ServicesTOOTH", tooth)
# Surfaces (col 5-9) — skip if empty
# Surfaces — skip if empty
if surface_chars:
surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3",
"ServicesSURF4","ServicesSURF5"]
for si, char in enumerate(surface_chars[:5]):
surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3",
"ServicesSURF4","ServicesSURF5"]
click_cell_and_fill(5 + si, surf_ids[si], char)
click_cell_and_fill(col_map[f"SURF{si+1}"], surf_ids[si], char)
# Billed Amount — last column
# Billed Amount
# QTY and Service Date auto-fill after CODE is entered and this cell is clicked
click_cell_and_fill("last()", "ServicesBILLED_AMOUNT", billed_str)
billed_ok = click_cell_and_fill(col_map["BILLED_AMOUNT"], "ServicesBILLED_AMOUNT", billed_str)
if not (code_ok and tooth_ok and billed_ok):
missing = [name for name, ok in
[("code", code_ok), ("tooth", tooth_ok), ("billed amount", billed_ok)]
if not ok]
return (f"ERROR: failed to fill {', '.join(missing)} for service line "
f"{idx+1} (code={code}) — claim NOT submitted")
time.sleep(0.3)