Initial import: eCW Charge Validation bot and results Portal

Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+542
View File
@@ -0,0 +1,542 @@
r"""
The professional/HCFA "Claim" screen (Commercial payers) has no separate
CPT/HCPCS tab and a different set of per-field ids from the UB Claim form —
see hcfa_claim.html's own comment for exactly what differs and why.
python tests\test_hcfa_claim.py
Real live bug this guards (claim 9103000, Logs\run_20260917_162939.log):
`open_cpt_hcpcs_tab()` waited 30s for a tab that does not exist on this
screen, raised, and aborted the whole claim read before Progress Notes was
ever reached — so it never opened for that claim at all. Fixed by making
`open_cpt_hcpcs_tab()` a no-op here, and by giving `extract()` and
`set_cpt_pos_modifier()` a `data-fieldname` fallback for this screen's
differently-shaped CPT row fields.
Since then: ICD and the payer (#billingClaimTbl8) are read here too, and
close_claim() no longer Escapes this form's own window. The rest of
extract() (header, revenue, payments) is still UB-only.
"""
import sys
import pathlib
import inspect
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
FIXTURE = pathlib.Path(__file__).resolve().parent / "fixtures" / "hcfa_claim.html"
from ecw_bot.pages import claim_form # noqa: E402
from ecw_bot.selectors import claim_form as sel # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
@case
def is_ub_claim_form_is_false_for_the_hcfa_screen(page):
assert claim_form.is_ub_claim_form(page) is False
@case
def open_cpt_hcpcs_tab_is_a_no_op_here_and_does_not_raise(page):
"""The real bug: this used to wait 30s for a tab that will never appear,
then raise and abort the whole claim read. It must now return promptly
without touching the page at all."""
assert claim_form.open_cpt_hcpcs_tab(page) is False
# Still on the same screen, untouched — no exception propagated.
assert claim_form.is_ub_claim_form(page) is False
@case
def extract_reads_all_three_real_cpt_rows_via_data_fieldname(page):
data = claim_form.extract(page)
cpt = data.get("cpt") or []
assert len(cpt) == 3, cpt
row0, row1, row2 = cpt
assert row0["code"] == "99213", row0
assert row0["pos"] == "11" and row0["mod1"] == "", row0
assert row0["icd1"] == "1" and row0["icd2"] == "2" and row0["icd3"] == "3", row0
assert row1["code"] == "J7298", row1
assert row1["pos"] == "11" and row1["mod1"] == "", row1
assert row2["code"] == "G8510", row2
assert row2["icd1"] == "1", row2
@case
def extract_reads_the_icd_table_via_id_prefix_on_the_hcfa_screen(page):
data = claim_form.extract(page)
icd = data.get("icd") or []
assert len(icd) == 2, icd
assert icd[0]["#"] == "1" and icd[0]["Code"] == "Z30.430", icd[0]
assert icd[1]["#"] == "2" and icd[1]["Code"] == "Z13.31", icd[1]
@case
def extract_reads_the_payer_from_the_insurances_and_payment_grid(page):
"""
Every live HCFA snapshot had `insurance: []` (9109000, 9119000 —
extract() only knew the UB form's #UBClaimTbl3), so telehealth never
chose 95 and vaccine_admin_codes never fired on a Commercial claim.
#billingClaimTbl8 carries the UB table's columns; #billingClaimTbl5's
Name cell also holds an edit input, which the value-first cell reader
would return instead of the payer.
"""
page.goto(FIXTURE.as_uri())
ins = claim_form.extract(page).get("insurance") or []
assert len(ins) == 1, ins
assert ins[0]["Name"] == "United Health Care", ins[0]
assert ins[0]["Ins Id"] == "1660", ins[0]
@case
def set_cpt_pos_modifier_writes_via_data_fieldname_and_blurs_mod1(page):
changes = claim_form.set_cpt_pos_modifier(page, pos="2", modifier="95")
assert len(changes) == 3, changes
row0, row1, row2 = changes
assert row0["code"] == "99213" and row0["after"]["pos"] == "2" \
and row0["after"]["mod1"] == "95", row0
assert row1["code"] == "J7298" and row1["after"]["pos"] == "2" \
and row1["after"]["mod1"] == "95", row1
assert row2["code"] == "G8510" and row2["after"]["pos"] == "2" \
and row2["after"]["mod1"] == "95", row2
mod1_field = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
sel.SEL_CPT_MOD1).first
assert mod1_field.get_attribute("data-blurred") == "1", \
"M1 was filled but never blurred - updateCPTMod() would not fire"
@case
def retag_cpt_icd_pointer_moves_the_depression_pointer_onto_g8510_only(page):
"""
The real bug (claim 9103000): 99213 wrongly carried the depression
pointer ("2") in ICD2 alongside its own diagnosis in ICD1 (and, in this
fixture, a third pointer "3" in ICD3 to prove the shift-left behaviour).
G8510 wrongly carried "1" (Z30.430) instead of the depression pointer.
J7298 carries neither and must be left untouched entirely.
"""
page.goto(FIXTURE.as_uri())
changes = claim_form.retag_cpt_icd_pointer(
page, target_pointer="2", target_codes={"G8510", "G8431"})
by_code = {c["code"]: c for c in changes}
assert set(by_code) == {"99213", "G8510"}, changes
row0 = by_code["99213"]
assert row0["before"]["icd"] == ["1", "2", "3", ""], row0
assert row0["after"]["icd"] == ["1", "3", "", ""], row0
row2 = by_code["G8510"]
assert row2["before"]["icd"] == ["1", "", "", ""], row2
assert row2["after"]["icd"] == ["2", "", "", ""], row2
# Read back from the live DOM, not just the returned dict.
row0_fields = page.locator(sel.SEL_CPT_ROWS).nth(0)
assert [row0_fields.locator(s).first.input_value() for s in sel.SEL_CPT_ICD] \
== ["1", "3", "", ""]
row2_fields = page.locator(sel.SEL_CPT_ROWS).nth(2)
assert [row2_fields.locator(s).first.input_value() for s in sel.SEL_CPT_ICD] \
== ["2", "", "", ""]
# J7298 (row 1) never carried the pointer - untouched, not even read-written.
row1_fields = page.locator(sel.SEL_CPT_ROWS).nth(1)
assert row1_fields.locator(sel.SEL_CPT_ICD1).first.input_value() == "1"
@case
def retag_cpt_icd_pointer_is_a_no_op_on_an_already_correct_claim(page):
page.goto(FIXTURE.as_uri())
claim_form.retag_cpt_icd_pointer(
page, target_pointer="2", target_codes={"G8510", "G8431"})
# Second pass over the now-corrected claim: nothing left to change.
changes = claim_form.retag_cpt_icd_pointer(
page, target_pointer="2", target_codes={"G8510", "G8431"})
assert changes == [], changes
@case
def ensure_cpt_rows_checked_checks_the_unchecked_g8510_row(page):
"""
The real bug report (claim 9103000): G8510's own checkbox sat
unchecked - excluded from the claim - and needed checking once its
ICD pointer was corrected. 99213/J7298 are already checked and are not
target codes anyway; neither should be touched.
"""
page.goto(FIXTURE.as_uri())
changes = claim_form.ensure_cpt_rows_checked(
page, {"G8510", "G8431"})
assert changes == [{"row": 2, "code": "G8510"}], changes
checkbox = page.locator(sel.SEL_CPT_ROWS).nth(2).locator(
sel.SEL_CPT_ASSIGN_TO_PATIENT).first
assert checkbox.is_checked() is True
@case
def ensure_cpt_rows_checked_is_a_no_op_once_already_checked(page):
page.goto(FIXTURE.as_uri())
claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
assert changes == [], changes
def _inject_g8510_duplicate_row(page, checked=False, fee="0.00"):
"""
A second G8510 row, after the fixture's own (CPTROW_2) — injected the
same way `_inject_j2001_row` below adds its row, rather than growing
the shared fixture and its row-count assumptions.
"""
page.evaluate(
"""(args) => {
const tbody = document.querySelector(
'#claimCPTCodesTable8888888888888 tbody');
const addRowTr = tbody.querySelector('tr:last-child');
const tr = document.createElement('tr');
tr.id = 'CPTROW_3';
tr.innerHTML =
'<td><input type="checkbox" ng-click="updateBillToIns(3)"'
+ (args.checked ? ' checked' : '') + '></td>'
+ '<td><input data-fieldname="claimCPTCode" value="G8510" readonly></td>'
+ '<td><input data-fieldname="ClaimCPTBilledFee" value="' + args.fee + '"></td>';
tbody.insertBefore(tr, addRowTr);
}""", {"checked": checked, "fee": fee})
@case
def ensure_cpt_rows_checked_checks_only_the_first_row_for_a_code(page):
"""
Live claim 9131000: a second G8510 row, already excluded, got checked
here and then unchecked again by zero_and_uncheck_duplicate_cpt_rows()
— a no-op on the claim, reported as two contradictory changes. Only the
first real row for a code is this function's to check. Both start
unchecked: the first is checked, the second is left alone, and a second
pass (the first now checked) must not fall through to the duplicate.
"""
page.goto(FIXTURE.as_uri())
_inject_g8510_duplicate_row(page, checked=False)
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
assert changes == [{"row": 2, "code": "G8510"}], changes
rows = page.locator(sel.SEL_CPT_ROWS)
assert rows.nth(2).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is True
assert rows.nth(3).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
assert changes == [], changes
assert rows.nth(3).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False
@case
def ensure_cpt_rows_checked_checks_the_priced_copy_not_a_zero_fee_first_g8510(page):
"""
The operator's common rule (zero_fee_uncheck.py, 2026-09-30): a line
billed at zero is never billed. zero_fee_uncheck runs first and
unchecks a checked 0.00 G8510; checking it again here would be the
same check/uncheck toggle as claim 9131000, between two conditions
instead of two steps of one. Blank is zero by the same shared rule.
Flipped by the operator's ruling of the same day: the row kept for a
code is the first whose fee is NOT zero, so the priced G8510 further
down is the one re-included here — `kept_cpt_row_per_code()`, the same
helper `zero_and_uncheck_duplicate_cpt_rows()` uses. It used to be left
unchecked too ("first" by position alone), leaving no G8510 billed.
"""
for fee in ("0.00", ""):
page.goto(FIXTURE.as_uri())
page.evaluate("(fee) => { document.getElementById('billingClaimIpt17ngR2')"
".value = fee; }", fee)
_inject_g8510_duplicate_row(page, checked=False, fee="65.00")
changes = claim_form.ensure_cpt_rows_checked(page, {"G8510", "G8431"})
assert changes == [{"row": 3, "code": "G8510"}], (fee, changes)
rows = page.locator(sel.SEL_CPT_ROWS)
assert rows.nth(2).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False, fee
assert rows.nth(3).locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is True, fee
assert rows.nth(2).locator(sel.SEL_CPT_BILLED_FEE).first.input_value() == fee, \
"the fee must never be written"
assert rows.nth(3).locator(sel.SEL_CPT_BILLED_FEE).first.input_value() == "65.00"
@case
def set_cpt_pos_modifier_only_first_stops_after_row_0(page):
page.goto(FIXTURE.as_uri())
changes = claim_form.set_cpt_pos_modifier(
page, pos="2", modifier="93", only_first=True)
assert len(changes) == 1, changes
assert changes[0]["code"] == "99213", changes
row1_mod1 = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(
sel.SEL_CPT_MOD1).first
assert row1_mod1.input_value() != "93", \
"only_first=True must not touch the second row on the HCFA screen"
@case
def zero_and_uncheck_other_cpt_rows_unchecks_the_checked_rows_here(page):
"""
The checkbox half of document condition #22, only provable on this
screen: ub_claim.html's checkboxes carry no ng-click at all, but this
fixture's do (`ng-click="updateBillToIns($index)"`, verified live on
claim 9103000). 99213 and J7298 start checked with real fees; G8510
starts unchecked on purpose and is the kept code here, so it must
come back with no change entry at all.
"""
page.goto(FIXTURE.as_uri())
changes = claim_form.zero_and_uncheck_other_cpt_rows(
page, keep_codes={"G8510"})
by_code = {c["code"]: c for c in changes}
assert set(by_code) == {"99213", "J7298"}, changes
assert by_code["99213"]["unchecked"] is True
assert by_code["99213"]["fee"] == {"before": "210.00", "after": "0.00"}
assert by_code["J7298"]["unchecked"] is True
assert by_code["J7298"]["fee"] == {"before": "1200.00", "after": "0.00"}
assert "G8510" not in by_code, changes
row0_checkbox = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
sel.SEL_CPT_ASSIGN_TO_PATIENT).first
assert row0_checkbox.is_checked() is False
row2_fee = page.locator(sel.SEL_CPT_ROWS).nth(2).locator(
sel.SEL_CPT_BILLED_FEE).first
assert row2_fee.input_value() == "65.00", \
"the kept code's fee must be left alone"
def _inject_j2001_row(page, checked=True, fee="45.00"):
"""
hcfa_claim.html has no J2001 row of its own — injected the same way
the medicare_telehealth tests inject a G2025 row, mirroring the real
markup this fixture already uses for its own checked/fee rows.
"""
page.evaluate(
"""(args) => {
const tbody = document.querySelector(
'#claimCPTCodesTable8888888888888 tbody');
const addRowTr = tbody.querySelector('tr:last-child');
const tr = document.createElement('tr');
tr.id = 'CPTROW_3';
tr.innerHTML =
'<td><input type="checkbox" ng-click="updateBillToIns(3)"'
+ (args.checked ? ' ng-checked="cpt.BillToIns==\\'1\\'" checked' : '')
+ '></td>'
+ '<td><input data-fieldname="claimCPTCode" value="J2001" readonly></td>'
+ '<td><input data-fieldname="ClaimCPTBilledFee" value="' + args.fee + '"></td>';
tbody.insertBefore(tr, addRowTr);
}""", {"checked": checked, "fee": fee})
@case
def zero_and_uncheck_cpt_rows_excludes_only_the_target_codes(page):
"""
Document condition #5's J2001 half: "this code can be unchecked in
the claim" plus zeroing its fee, the same uncheck+zero shape as
condition #22's cleanup step but inverted — only the target codes are
touched, every other row (99213, J7298, G8510) is left alone.
"""
page.goto(FIXTURE.as_uri())
_inject_j2001_row(page, checked=True, fee="45.00")
changes = claim_form.zero_and_uncheck_cpt_rows(page, {"J2001"})
by_code = {c["code"]: c for c in changes}
assert set(by_code) == {"J2001"}, changes
assert by_code["J2001"]["unchecked"] is True
assert by_code["J2001"]["fee"] == {"before": "45.00", "after": "0.00"}
j2001_row = page.locator(sel.SEL_CPT_ROWS).nth(3)
assert j2001_row.locator(sel.SEL_CPT_ASSIGN_TO_PATIENT).first.is_checked() is False
assert j2001_row.locator(sel.SEL_CPT_BILLED_FEE).first.input_value() == "0.00"
row0_fee = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
sel.SEL_CPT_BILLED_FEE).first
assert row0_fee.input_value() == "210.00", \
"an untargeted row's fee must be left alone"
row0_checkbox = page.locator(sel.SEL_CPT_ROWS).nth(0).locator(
sel.SEL_CPT_ASSIGN_TO_PATIENT).first
assert row0_checkbox.is_checked() is True, \
"an untargeted row's checkbox must be left alone"
# A second pass has nothing left to touch.
assert claim_form.zero_and_uncheck_cpt_rows(page, {"J2001"}) == []
@case
def add_cpt_row_uses_the_real_add_button_and_fee_schedule_picker(page):
"""
The real flow (claim 9109000's own live evidence): click "Add", search
the picker for the code, select the one exact match, click the
picker's own OK — never typing straight into the grid's trailing "*"
row, which is a different field entirely.
"""
page.goto(FIXTURE.as_uri())
assert claim_form.cpt_code_present(page, "G2025") is False
assert claim_form.add_cpt_row(page, "G2025") is True
assert claim_form.cpt_code_present(page, "G2025") is True
assert page.locator(sel.SEL_CPT_ROWS).count() == 4
new_row = page.locator(sel.SEL_CPT_ROWS).last
assert new_row.locator(sel.SEL_CPT_CODE).first.input_value() == "G2025"
# The picker itself must be closed afterward, not left open over the claim.
assert page.locator("#claimFeeScheduleModal").is_visible() is False
@case
def add_cpt_row_searches_by_the_real_search_field_not_a_substring_of_everything(page):
"""
The catalog also carries G2026 (a decoy), which starts with the same
"G20" prefix — proves the search actually narrows to the exact code
asked for, not just whatever the picker happened to show first.
"""
page.goto(FIXTURE.as_uri())
assert claim_form.add_cpt_row(page, "G2026") is True
new_row = page.locator(sel.SEL_CPT_ROWS).last
assert new_row.locator(sel.SEL_CPT_CODE).first.input_value() == "G2026"
@case
def add_cpt_row_cancels_the_picker_and_returns_false_on_no_match(page):
"""
A code the seeded catalog has no match for at all. Must close the
picker with its own Cancel rather than leave it open — an open Fee
Schedule modal would block every click on the claim form behind it,
the exact class of bug dismiss_stray_modal() exists to recover from.
"""
page.goto(FIXTURE.as_uri())
assert claim_form.add_cpt_row(page, "NOPE99") is False
assert page.locator("#claimFeeScheduleModal").is_visible() is False
@case
def open_progress_notes_works_the_same_as_the_ub_form(page):
page.goto(FIXTURE.as_uri())
note = claim_form.open_progress_notes(page)
assert note.get("opened") not in (None, "absent", "disabled", "nothing"), note
@case
def close_claim_uses_the_real_cancel_not_escape_no_wasted_fallback(page):
"""
Live, on every HCFA claim the bot closed itself (9109000,
Logs\\run_20260929_160143.log): dismiss_stray_modal() took this form's
own uib window (.claimModalBackdrop) for a stray panel and pressed
Escape, which closed the form without closeClaimScreen() — then Cancel,
X and the Dental Cancel each timed out on buttons already gone. ~45s
and a false "Could not close the claim form", per claim.
"""
import time
page.goto(FIXTURE.as_uri())
assert page.locator("#billingClaimBtn36").first.is_visible()
started = time.monotonic()
assert claim_form.close_claim(page) is True
elapsed = time.monotonic() - started
assert page.evaluate("window.closeClaimScreenRan") is True, \
"the form was closed by Escape, not by its own Cancel"
assert elapsed < 5, f"took {elapsed:.1f}s — a fallback was tried"
assert not page.locator("#billingClaimBtn36").first.is_visible()
@case
def close_claim_still_dismisses_a_real_stray_panel_over_the_form(page):
"""
The claim's own window is spared, not the stray check as a whole: a
.claimModalBackdrop panel with no claim form inside it, sitting on top,
still gets its Escape — and the form under it is then closed by Cancel.
"""
page.goto(FIXTURE.as_uri())
page.evaluate("""() => {
const p = document.createElement('div');
p.className = 'modal fade ng-isolate-scope claimModalBackdrop in';
p.id = 'strayPanel';
p.style.cssText = 'position:fixed;top:0;left:0;width:100%;'
+ 'height:100%;z-index:2000;display:block;';
document.body.appendChild(p);
// Escape goes to the topmost window only: the stray panel, first.
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && p.isConnected) {
p.remove();
e.stopImmediatePropagation();
}
}, true);
}""")
assert claim_form.close_claim(page) is True
assert page.locator("#strayPanel").count() == 0
assert page.evaluate("window.closeClaimScreenRan") is True
@case
def save_claim_clicks_the_hcfa_ok_and_the_modal_closes(page):
page.goto(FIXTURE.as_uri())
assert page.locator("#claimScreenOkBtn8888888888888").first.is_visible()
assert claim_form.save_claim(page) is True
assert not page.locator("#claimScreenOkBtn8888888888888").first.is_visible()
def _run() -> int:
if not FIXTURE.exists():
print(f" FAIL fixture missing: {FIXTURE}")
return 1
try:
from playwright.sync_api import sync_playwright
except ImportError:
print(" SKIP playwright is not installed")
return 0
failures = 0
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
page = browser.new_page()
page.goto(FIXTURE.as_uri())
for fn in CASES:
name = fn.__name__.replace("_", " ")
try:
if len(inspect.signature(fn).parameters) == 0:
fn()
else:
fn(page)
print(f" ok {name}")
except AssertionError as exc:
print(f" FAIL {name}: {exc}")
failures += 1
except Exception as exc:
print(f" ERROR {name}: {type(exc).__name__}: {exc}")
failures += 1
browser.close()
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
return 1 if failures else 0
def main() -> int:
"""
shot() writes into the operator's dated Screenshots\ folder; a test
run must not, so the cases run with it pointed at a temp folder.
"""
import tempfile
from ecw_bot import settings as _cfg
orig = (_cfg.SCREENSHOTS, _cfg.SAVE_CORRECTIONS)
with tempfile.TemporaryDirectory() as tmp:
_cfg.SCREENSHOTS = pathlib.Path(tmp)
# These cases exercise eCW's own click and Add flow, as a LIVE-WRITE
# run does. VERIFY clicks no box and adds no row (eCW saves both at
# once, 2026-10-16): that is tests/test_verify_no_live_writes.py.
_cfg.SAVE_CORRECTIONS = True
try:
return _run()
finally:
_cfg.SCREENSHOTS, _cfg.SAVE_CORRECTIONS = orig
if __name__ == "__main__":
sys.exit(main())