Portal Delete run (Super Admin), lab codes #3/#4, Z23 pointer #45, T1017 #46, payer note, login security-image wait

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-29 12:30:38 +05:30
1 parent a8af44e673
commit fafa5285eb
21 files changed
+1509 -63

No files matched your search

+45 -15
View File
@@ -12,6 +12,7 @@ spreadsheet, one client instead of a loop, and the second factor is a mailed
approval link rather than an authenticator code — see `twofactor.py`.
"""
import time
from datetime import datetime, timezone
from playwright.sync_api import Page, TimeoutError as PWTimeout
@@ -30,6 +31,38 @@ from .restricted import Restricted, assert_not_restricted
from .twofactor import handle_2fa
SECURITY_IMAGE = "text=VERIFY YOUR SECURITY IMAGE"
def wait_after_username(page: Page, timeout_ms: int, image: bool = True) -> str:
"""
After the username's Next: "image" once eCW's security image page shows
(only while `image`), "password" once the password box is visible AND
enabled — it is on the page from the start, disabled until the username
step is done. Neither within `timeout_ms` → a screenshot (and the DOM)
and RuntimeError. Polls: `Locator.is_visible()` never waits, so a
single look missed a security image page that came late (2026-10-28).
"""
pw = page.locator(sel.SEL_PASSWORD).first
img = page.locator(SECURITY_IMAGE).first
end = time.monotonic() + timeout_ms / 1000
while True:
try:
if image and img.is_visible():
return "image"
if pw.is_visible() and pw.is_enabled(timeout=1_000):
return "password"
except Exception:
pass # navigating; look again
if time.monotonic() > end:
break
page.wait_for_timeout(250)
shot(page, "username_step_disabled")
raise RuntimeError(
f"Username step did not finish in {timeout_ms // 1000}s: the password "
f"box stayed disabled (no security image page seen either)")
def _assert_login_page(page: Page) -> None:
"""
Confirm the real login form came back before typing any credentials.
@@ -90,21 +123,18 @@ def _do_login(page: Page, context, client_name: str):
except Exception:
pass
# Some logins interpose a security-image confirmation page.
try:
if page.locator("text=VERIFY YOUR SECURITY IMAGE").first.is_visible(timeout=3_000):
print("[LOGIN] Security image page")
msgbox.info_box(
f"eCW Bot — Security Image ({client_name})",
"The 'VERIFY YOUR SECURITY IMAGE' page came up.\n"
"Click Continue/OK in the browser.\n\n"
"Click OK once the password page appears.",
needs_person=True,
)
except Exception:
pass
page.wait_for_selector(sel.SEL_PASSWORD, timeout=cfg.LOGIN_TIMEOUT)
# Some logins interpose a security-image confirmation page — sometimes
# seconds after Next (2026-10-28 17:59: missed by a one-look check).
if wait_after_username(page, cfg.LOGIN_TIMEOUT) == "image":
print("[LOGIN] Security image page")
msgbox.info_box(
f"eCW Bot — Security Image ({client_name})",
"The 'VERIFY YOUR SECURITY IMAGE' page came up.\n"
"Click Continue/OK in the browser.\n\n"
"Click OK once the password page appears.",
needs_person=True,
)
wait_after_username(page, cfg.LOGIN_TIMEOUT, image=False)
# A renamed password field or Log In button still works through its
# fallback (selectors/login.py); this says so in the log.
fallback.report(page, ["Password field", "Log In button"],
+8 -2
View File
@@ -44,6 +44,7 @@ from . import depression_dx_pointer
from . import dummy_codes
from . import injection_admin_code
from . import j2930_j2001_update
from . import lab_codes
from . import medicaid_pregnancy_th
from . import medicaid_uncheck_cpts
from . import medicare_billing_screening
@@ -52,18 +53,23 @@ from . import medicare_telehealth
from . import medicare_vaccine_a6
from . import medicare_wellness_history
from . import priced_line_check
from . import t1017_non_billable
from . import telehealth
from . import vaccine_admin_codes
from . import vaccine_z23_pointer
from . import zero_fee_uncheck
from .base import run_all
CONDITIONS = [zero_fee_uncheck,
telehealth, medicare_telehealth, depression_dx_pointer,
j2930_j2001_update, dummy_codes, dental_dummy_codes,
dental_payer_check, medicaid_uncheck_cpts, medicare_billing_screening,
dental_payer_check, medicaid_uncheck_cpts, lab_codes,
t1017_non_billable,
medicare_billing_screening,
medicare_vaccine_a6, medicare_wellness_history,
medicaid_pregnancy_th,
bh_h_code_modifier, vaccine_admin_codes, injection_admin_code,
bh_h_code_modifier, vaccine_z23_pointer, vaccine_admin_codes,
injection_admin_code,
medicare_pps_code,
priced_line_check]
+126
View File
@@ -0,0 +1,126 @@
r"""
Condition (document #3, #4; EEWA-38, operator 2026-10-28): which lab codes
a payer takes.
A **lab code** is a CPT of 5 digits starting with 8 (80000-89999) — the
operator's rule, "8 series". 36416 (the finger stick billed beside some of
them) is not one and is never touched.
The claim's primary payer decides:
* **Medicare payers** (#3: Medicare, Devoted MCR, Humana MCR, Novitas,
Cigna Health Springs) — no lab code at all. The same exact eCW names
as #25's PPS Payers (`medicare_pps_code.is_pps_payer`).
* **Cigna Salud Employee** (#4: "Cigna Salud", the Salud employees'
plan) — any lab code.
* **Every other payer**, a blank one included (#4: "ALL except Cigna
Salud") — only the 13 in-house codes, `IN_HOUSE`.
A lab code the payer does not take is unchecked and its Billed Fee zeroed,
the treatment `j2930_j2001_update.py` gives its codes. The code only: the
fees in the document's list are not checked. Not on Dental claims.
"""
import re
from ..pages import claim_form
from . import lines
from .base import CPT_TAB_NOT_CONFIRMED, ConditionResult
from .medicare_pps_code import is_pps_payer
NAME = "lab_codes"
IN_HOUSE = frozenset((
"82948", # Glucose blood stick
"81025", # HCG, urine
"85018", # Hemoglobin
"82270", # Stool
"82274", # Insure
"87806", # HIV
"85610", # INR
"87880", # Strep group A
"80305", # Drug screen
"81001", # Urine dip
"81003", # Urine dip w/o microscopy
"87210", # Wet prep
"87636", # COVID / Flu A/B
))
CIGNA_SALUD = "cigna salud employee"
_LAB = re.compile(r"8\d{4}")
def is_lab_code(code) -> bool:
return bool(_LAB.fullmatch(str(code or "").strip()))
def _rule(data: dict) -> str:
""""medicare" (no lab code), "salud" (any) or "in_house" (the 13)."""
if is_pps_payer(data):
return "medicare"
if lines.primary_payer(data).lower() == CIGNA_SALUD:
return "salud"
return "in_house"
def _not_taken(code, rule: str) -> bool:
code = str(code or "").strip()
if not is_lab_code(code) or rule == "salud":
return False
return rule == "medicare" or code not in IN_HOUSE
def detect(claim_id: str, data: dict) -> bool:
if (data or {}).get("screen") == "dental":
return False
rule = _rule(data)
return any(_not_taken(row.get("code"), rule)
for row in (data or {}).get("cpt") or [])
def apply(page, claim_id: str, data: dict) -> ConditionResult:
"""
Re-clicks CPT/HCPCS for the same reason `medicaid_uncheck_cpts.py`'s
`apply()` does — eCW can leave the form on a different tab after the
Prog. Notes viewer cycle.
"""
try:
claim_form.open_cpt_hcpcs_tab(page)
except Exception as exc:
print(f"[COND {NAME} {claim_id}] Could not confirm the CPT/HCPCS "
f"tab before fixing: {exc} — leaving the claim as read")
return ConditionResult.declined(NAME, CPT_TAB_NOT_CONFIRMED)
rule = _rule(data)
changes = claim_form.zero_and_uncheck_cpt_rows_matching(
page, lambda code: _not_taken(code, rule))
if not changes:
return ConditionResult.none(NAME)
for c in changes:
c["rule"] = rule
return ConditionResult(condition_name=NAME, changed=True, changes=changes)
def _codes(result: ConditionResult) -> str:
return "/".join(dict.fromkeys(c["code"] for c in result.changes))
def summary_comment(result: ConditionResult) -> str:
if any(c.get("rule") == "medicare" for c in result.changes):
return (f"Lab code not billed to Medicare ({_codes(result)} - "
f"unchecked, fee 0)")
return (f"Lab code not in the in-house list ({_codes(result)} - "
f"unchecked, fee 0)")
def describe(result: ConditionResult) -> str:
out = ["Lab codes the payer does not take — excluded:"]
for c in result.changes:
bits = []
if c.get("unchecked"):
bits.append("checkbox unchecked")
if "fee" in c:
bits.append(f"fee {c['fee']['before']} -> {c['fee']['after']}")
out.append(f" Row {c['row'] + 1} ({c['code']}): {', '.join(bits)}")
return "\n".join(out)
@@ -0,0 +1,98 @@
r"""
Condition (document #46; EEWA-40, operator 2026-10-28): T1017 is a
non-billable appointment on Medicare and Medicaid, and a human's call on
any other payer.
Per the client's Charge Condition Document, row 46 (T1017, payer
Medicaid):
"If T1017 is received and it is not covered by Medicare, then please
change this to a non-billable appt"
The operator's reading, by the claim's primary payer:
* a **Medicare payer** (#25's exact eCW names,
`medicare_pps_code.is_pps_payer`) or **Medicaid** (the dashboard's
"Medicaid" group only, `medicaid_uncheck_cpts.is_medicaid_claim` — not
RAE, not CHP+) → the T1017 line is unchecked and its Billed Fee zeroed,
the treatment `j2930_j2001_update.py` gives its codes;
* **any other payer**, blank or unmapped included → nothing changed,
Review: "T1017 on <payer> - check by hand".
Not on Dental claims.
"""
from ..pages import claim_form
from . import lines
from .base import CPT_TAB_NOT_CONFIRMED, ConditionResult
from .medicaid_uncheck_cpts import is_medicaid_claim
from .medicare_pps_code import is_pps_payer
NAME = "t1017_non_billable"
T1017 = "T1017"
def _payer_rule(data: dict) -> str:
""""Medicare", "Medicaid" or "" (any other payer)."""
if is_pps_payer(data):
return "Medicare"
if is_medicaid_claim(data):
return "Medicaid"
return ""
def non_billable_for(data: dict) -> bool:
return bool(_payer_rule(data))
def detect(claim_id: str, data: dict) -> bool:
if (data or {}).get("screen") == "dental":
return False
return any((row.get("code") or "").strip().upper() == T1017
for row in (data or {}).get("cpt") or [])
def apply(page, claim_id: str, data: dict) -> ConditionResult:
"""
Re-clicks CPT/HCPCS for the same reason `medicaid_uncheck_cpts.py`'s
`apply()` does — eCW can leave the form on a different tab after the
Prog. Notes viewer cycle.
"""
try:
claim_form.open_cpt_hcpcs_tab(page)
except Exception as exc:
print(f"[COND {NAME} {claim_id}] Could not confirm the CPT/HCPCS "
f"tab before fixing: {exc} — leaving the claim as read")
return ConditionResult.declined(NAME, CPT_TAB_NOT_CONFIRMED)
rule = _payer_rule(data)
if not rule:
payer = lines.primary_payer(data) or "no payer"
reason = f"{T1017} on {payer} - check by hand"
print(f"[COND {NAME} {claim_id}] {reason}")
return ConditionResult.declined(NAME, reason)
changes = claim_form.zero_and_uncheck_cpt_rows(page, {T1017})
if not changes:
return ConditionResult.none(NAME)
for c in changes:
c["rule"] = rule
return ConditionResult(condition_name=NAME, changed=True, changes=changes)
def summary_comment(result: ConditionResult) -> str:
rule = next((c["rule"] for c in result.changes if c.get("rule")),
"Medicare / Medicaid")
return f"{T1017} not billable ({rule}) - unchecked, fee 0"
def describe(result: ConditionResult) -> str:
out = [f"{T1017} made non-billable:"]
for c in result.changes:
bits = []
if c.get("unchecked"):
bits.append("checkbox unchecked")
if "fee" in c:
bits.append(f"fee {c['fee']['before']} -> {c['fee']['after']}")
out.append(f" Row {c['row'] + 1} ({c['code']}): {', '.join(bits)}")
return "\n".join(out)
@@ -0,0 +1,103 @@
r"""
Condition (document #45; EEWA-39, operator 2026-10-28): every vaccine line
carries the Z23 diagnosis pointer.
Per the client's Charge Condition Document, row 45 (Vaccines codes):
"Make sure you are assigning the proper diagnosis code (Z23) for ALL
VACCINES"
The same mechanism as `depression_dx_pointer.py`: a CPT line points at a
diagnosis by its number in the ICD list (ICD1-4 hold "1", "2", ...). On a
claim with a vaccine code, Z23's number goes on every vaccine line that
lacks it, in the first empty slot (ICD1 → ICD4); the line's other
pointers stay (operator: "add in ICD1, ICD2"). Unlike #6, nothing is
removed and no other line is touched.
* **Vaccine** = the client's 19 vaccine product codes of #18
(`vaccine_admin_codes.VACCINE_CODES`), not the admin codes.
* No Z23 in the ICD list → Review "add it by hand" (the bot has no flow
for adding an ICD row). A line whose four slots are full → Review.
* All payers, Medicare included. Not on Dental claims.
"""
from ..pages import claim_form
from .base import CPT_TAB_NOT_CONFIRMED, ConditionResult
from .vaccine_admin_codes import VACCINE_CODES
NAME = "vaccine_z23_pointer"
Z23 = "Z23"
NO_Z23 = "Z23 not on the claim — add it by hand"
def z23_pointer(data: dict):
"""Z23's number in the ICD list ("2"), or None. Z23 with or without a
dot; Z23.0 is the same code written long, Z23.5 is not Z23."""
for row in (data or {}).get("icd") or []:
code = (row.get("Code") or "").strip().upper().rstrip(".")
if code in (Z23, Z23 + ".0"):
return (row.get("#") or "").strip() or None
return None
def _vaccine_codes(data: dict) -> list:
return [(row.get("code") or "").strip()
for row in (data or {}).get("cpt") or []
if (row.get("code") or "").strip() in VACCINE_CODES]
def detect(claim_id: str, data: dict) -> bool:
if (data or {}).get("screen") == "dental":
return False
return bool(_vaccine_codes(data))
def apply(page, claim_id: str, data: dict) -> ConditionResult:
"""
Re-clicks CPT/HCPCS for the same reason `depression_dx_pointer.py`'s
`apply()` does — eCW can leave the form on a different tab after the
Prog. Notes viewer cycle.
"""
try:
claim_form.open_cpt_hcpcs_tab(page)
except Exception as exc:
print(f"[COND {NAME} {claim_id}] Could not confirm the CPT/HCPCS "
f"tab before fixing: {exc} — leaving the claim as read")
return ConditionResult.declined(NAME, CPT_TAB_NOT_CONFIRMED)
pointer = z23_pointer(data)
if pointer is None:
print(f"[COND {NAME} {claim_id}] {NO_Z23} "
f"({'/'.join(dict.fromkeys(_vaccine_codes(data)))})")
return ConditionResult.declined(NAME, NO_Z23)
changes, full = claim_form.add_cpt_icd_pointer(
page, pointer=pointer, target_codes=VACCINE_CODES)
review = (f"Z23 pointer: no free pointer slot on "
f"{'/'.join(dict.fromkeys(full))} — add it by hand"
if full else "")
if not changes:
if not review:
print(f"[COND {NAME} {claim_id}] Z23 already on every vaccine "
f"line — nothing to change")
return ConditionResult.none(NAME)
return ConditionResult.declined(NAME, review)
return ConditionResult(condition_name=NAME, changed=True,
changes=changes, review=review)
def _codes(result: ConditionResult) -> str:
return "/".join(dict.fromkeys(c["code"] for c in result.changes))
def summary_comment(result: ConditionResult) -> str:
return f"Z23 pointer added to the vaccine line ({_codes(result)})"
def describe(result: ConditionResult) -> str:
out = ["Z23 ICD pointer on the vaccine lines:"]
for c in result.changes:
out.append(f" Row {c['row'] + 1} ({c['code']}): ICD "
f"{c['before']['icd']} -> {c['after']['icd']}")
if result.review:
out.append(f" {result.review}")
return "\n".join(out)
+42 -9
View File
@@ -91,6 +91,10 @@ COMMENTS = {
"CHP+ Dental - Claim Status to Patient"),
"medicaid_uncheck_cpts": (
"CPT not billable to Medicaid (15851/15852/73521 - unchecked, fee 0)"),
# Fallback only: the condition words its own comment (codes, payer rule).
"lab_codes": "Lab code the payer does not take - unchecked, fee 0",
# Fallback only: the condition words its own comment (which payer).
"t1017_non_billable": "T1017 not billable (Medicare / Medicaid) - unchecked, fee 0",
"medicare_billing_screening": (
"Screening codes not billed to Medicare (G8510/G8431)"),
# Fallback only: the condition words its own comment (which codes).
@@ -104,6 +108,8 @@ COMMENTS = {
"medicaid_pregnancy_th": (
"TH modifier added (pregnancy visit on Medicaid)"),
"vaccine_admin_codes": "CPT Code Missing ( vaccine admin code added )",
# Fallback only: the condition words its own comment (which codes).
"vaccine_z23_pointer": "Z23 pointer added to the vaccine line",
# Fallback only: the condition words its own comment (96372 added or not).
"injection_admin_code": "CPT Code Missing (injection admin code 96372)",
# Fallback only: the condition words its own comment by the path it
@@ -256,6 +262,24 @@ def review_reasons(data: dict) -> list:
return reasons
def _ecw_payer(data: dict) -> str:
"""The payer every condition goes by: the claim form's first insurance
row (`conditions.lines.primary_payer`, not imported — it brings the
page code with it)."""
rows = (data or {}).get("insurance") or []
return str(rows[0].get("Name") or "").strip() if rows else ""
def payer_note(sheet: str, ecw: str) -> str:
"""A note when the report's PAYER and eCW's differ (operator,
2026-10-28): the rules went by eCW's, and the reader should know. Not
when either is blank, nor for case or spacing alone."""
norm = lambda s: " ".join(str(s or "").split()).lower()
if not norm(sheet) or not norm(ecw) or norm(sheet) == norm(ecw):
return ""
return f"Payer: sheet says {sheet.strip()}, eCW says {ecw.strip()} - eCW used"
def claim_outcome(found: bool, data: dict) -> dict:
"""
Columns V-X for one claim, from what `_read_claim_form` returned.
@@ -318,15 +342,20 @@ def claim_outcome(found: bool, data: dict) -> dict:
# A save that happened is never hidden behind "Needs manual review"
# (final review, 2026-10-05): it leads the Comments.
lead = [SAVED_NOTE] if saved else []
return {"Comments": "; ".join(lead + comments + reasons),
"Current Status": "Needs manual review", "Category": "Review"}
if not failed:
return {"Comments": "; ".join([CLEAN] + comments),
"Current Status": "Ready to Submit", "Category": "Pass"}
return {"Comments": "; ".join(comments),
"Current Status": ("Ready to Submit" if saved
else "Corrections pending (not saved)"),
"Category": "Fail"}
out = {"Comments": "; ".join(lead + comments + reasons),
"Current Status": "Needs manual review", "Category": "Review"}
elif not failed:
out = {"Comments": "; ".join([CLEAN] + comments),
"Current Status": "Ready to Submit", "Category": "Pass"}
else:
out = {"Comments": "; ".join(comments),
"Current Status": ("Ready to Submit" if saved
else "Corrections pending (not saved)"),
"Category": "Fail"}
# Not a column: build_rows compares it with the report's PAYER.
if _ecw_payer(data):
out["_ecw_payer"] = _ecw_payer(data)
return out
def _money(text):
@@ -371,6 +400,10 @@ def build_rows(report_rows: list, header_row: int, claim_col: int,
done = outcomes.get(claim) or {}
if done:
vals.update(done)
note = payer_note(vals["PAYER"], done.get("_ecw_payer"))
if note:
vals["Comments"] = "; ".join(
c for c in (vals.get("Comments"), note) if c)
vals["Recevied date"] = run_date
vals["Completed On"] = run_date
vals["Assigned To "] = assigned_to
+39
View File
@@ -2005,6 +2005,45 @@ def retag_cpt_icd_pointer(page: Page, *, target_pointer: str,
return changes
def add_cpt_icd_pointer(page: Page, *, pointer: str, target_codes) -> tuple:
"""
Make every row whose code is in `target_codes` carry the ICD pointer
`pointer` among ICD1-4 — added in the first empty slot, the row's other
pointers kept (document #45: Z23 on every vaccine line). A row that
already carries it, and every other row, is left untouched; so is a row
whose four slots are full.
Returns `(changes, full)`: `changes` in `retag_cpt_icd_pointer()`'s
shape, `full` the codes of the rows that had no free slot.
"""
changes, full = [], []
wanted = {c.upper() for c in target_codes}
rows = page.locator(sel.SEL_CPT_ROWS)
for i in range(rows.count()):
row = rows.nth(i)
code_input = row.locator(sel.SEL_CPT_CODE).first
if code_input.count() == 0:
continue
code = (code_input.input_value(timeout=5_000) or "").strip()
if code.upper() not in wanted:
continue
icd_inputs = [row.locator(s).first for s in sel.SEL_CPT_ICD]
before = [(inp.input_value(timeout=5_000) or "").strip()
for inp in icd_inputs]
if pointer in before:
continue
if "" not in before:
full.append(code)
continue
slot = before.index("")
icd_inputs[slot].fill(pointer, timeout=10_000)
after = list(before)
after[slot] = pointer
changes.append({"row": i, "code": code,
"before": {"icd": before}, "after": {"icd": after}})
return changes, full
def kept_cpt_row_per_code(rows) -> dict:
"""
Which row is KEPT for each code when the same code sits on more than