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:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,251 @@
|
||||
r"""
|
||||
UB Claim form extraction, against a real browser.
|
||||
|
||||
python tests\test_claim_form.py
|
||||
|
||||
This one launches a **local headless chromium** and loads
|
||||
`tests\fixtures\ub_claim.html`. It never touches eCW: no proxy, no login, no
|
||||
2FA mail, no attempt against the guard. So unlike a live run it costs nothing
|
||||
and can be run as often as you like — which matters, because the alternative
|
||||
way to test this code is a three-minute login and one of four hourly attempts.
|
||||
|
||||
What it is really checking is the thing most likely to break silently:
|
||||
|
||||
* **No selector depends on eCW's per-open timestamp.** The fixture uses
|
||||
`9999999999999` where the captured DOM had `1788790287089`. A selector
|
||||
with the number baked in matches nothing here, and the assertions fail.
|
||||
* **Values are read from inputs, not text.** The ICD code, every CPT column,
|
||||
the dates and the claim status all live in inputs or selects. Reading
|
||||
`textContent` returns blanks for them and the run looks successful.
|
||||
* **eCW's blank padding rows are dropped.** Every grid is padded with empty
|
||||
`ng-repeat` rows to hold a fixed height, plus a `*` entry row. Counting
|
||||
those would report five CPT lines on a claim that has two.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
FIXTURE = pathlib.Path(__file__).resolve().parent / "fixtures" / "ub_claim.html"
|
||||
|
||||
from ecw_bot.pages import claim_form # noqa: E402
|
||||
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
||||
|
||||
CHECKS = []
|
||||
|
||||
|
||||
def check(fn):
|
||||
CHECKS.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
@check
|
||||
def claim_and_patient(page, data):
|
||||
assert data["claim_no"] == "9104000", data["claim_no"]
|
||||
p = data["patient"]
|
||||
assert p["name"] == "Sample, Pat, Q", p["name"]
|
||||
assert p["dob"] == "05/11/1979", p["dob"]
|
||||
assert p["account_no"] == "900123", p["account_no"]
|
||||
assert p["phone"] == "555-010-0100", p["phone"]
|
||||
# The visible text is ellipsised by eCW's own filter; title= has it whole.
|
||||
assert p["address"] == "100 SAMPLE ST, , CO 80000-0000", p["address"]
|
||||
|
||||
|
||||
@check
|
||||
def header_reads_inputs_and_the_select(page, data):
|
||||
h = data["header"]
|
||||
assert h["claim_date"] == "06/29/2026", h["claim_date"]
|
||||
assert h["bill_type"] == "131", h["bill_type"]
|
||||
assert h["facility"] == "MWC:Brighton Womens Clinic", h["facility"]
|
||||
assert h["attending"] == "Clinician, Casey M", h["attending"]
|
||||
# Both halves of the status: the code is what eCW submits, the label is what
|
||||
# a person recognises, and only checking one of them has burned us before.
|
||||
assert h["claim_status_code"] == "108ME", h["claim_status_code"]
|
||||
assert h["claim_status_label"] == "Medcor Ready", h["claim_status_label"]
|
||||
|
||||
|
||||
@check
|
||||
def icd_code_comes_from_the_input(page, data):
|
||||
icd = data["icd"]
|
||||
assert len(icd) == 1, f"expected 1 ICD row, got {len(icd)}: {icd}"
|
||||
row = icd[0]
|
||||
assert row["Code"] == "R69", row
|
||||
assert row["Name"] == "Illness, unspecified", row
|
||||
|
||||
|
||||
@check
|
||||
def cpt_grid_is_keyed_by_input_id(page, data):
|
||||
cpt = data["cpt"]
|
||||
# Two real rows. The bottom "*" entry row has no CPTROW_ id and must not
|
||||
# become a third.
|
||||
assert len(cpt) == 2, f"expected 2 CPT rows, got {len(cpt)}: {cpt}"
|
||||
a, b = cpt
|
||||
assert a["code"] == "H0023" and a["mod1"] == "GT", a
|
||||
assert a["pos"] == "11" and a["units"] == "1" and a["billed_fee"] == "74.00", a
|
||||
assert a["sdos"] == "06/29/2026" and a["edos"] == "06/29/2026", a
|
||||
assert a["icd1"] == "1" and a["icd2"] == "", a
|
||||
assert "Behavioral Health Outreach" in a["description"], a
|
||||
assert b["code"] == "90791" and b["units"] == "2", b
|
||||
assert b["billed_fee"] == "150.00", b
|
||||
|
||||
|
||||
@check
|
||||
def revenue_and_totals(page, data):
|
||||
rev = data["revenue"]
|
||||
assert len(rev) == 1, f"expected 1 revenue row, got {len(rev)}: {rev}"
|
||||
row = rev[0]
|
||||
assert row["42 RV CC"] == "0900", row
|
||||
assert row["43 Description"] == "Psychological Services", row
|
||||
assert row["44 HCPCS/Rates"] == "H0023 GT", row
|
||||
assert row["47 Total Charges"] == "74.00", row
|
||||
|
||||
t = data["totals"]
|
||||
assert t["total_charges"] == "74.00", t
|
||||
assert t["charges"] == "224.00" and t["balance"] == "224.00", t
|
||||
assert t["pt_balance"] == "0.00", t
|
||||
|
||||
|
||||
@check
|
||||
def insurance_and_payments(page, data):
|
||||
ins = data["insurance"]
|
||||
assert len(ins) == 1, f"expected 1 insurance row, got {len(ins)}: {ins}"
|
||||
assert ins[0]["Name"] == "BH COLORADO ACCESS", ins[0]
|
||||
assert ins[0]["Subscriber No."] == "J000000", ins[0]
|
||||
assert ins[0]["State"] == "CO", ins[0]
|
||||
|
||||
pay = data["payments"]
|
||||
assert len(pay) == 1, f"expected 1 payment row, got {len(pay)}: {pay}"
|
||||
assert pay[0]["Id"] == "2000001", pay[0]
|
||||
assert pay[0]["Date"] == "06/29/2026", pay[0]
|
||||
assert pay[0]["From"] == "Patient", pay[0]
|
||||
|
||||
|
||||
@check
|
||||
def notes_and_empty_error_table(page, data):
|
||||
assert data["notes"] == "Awaiting coding review.", data["notes"]
|
||||
# "Error Logs ()" on the live claim — an empty tbody must be [], not a row
|
||||
# of blanks.
|
||||
assert data["errors"] == [], data["errors"]
|
||||
|
||||
|
||||
@check
|
||||
def cpt_hcpcs_tab_opens(page, data):
|
||||
assert claim_form.open_cpt_hcpcs_tab(page) is True
|
||||
assert page.locator(sel.SEL_CPT_GRID).first.is_visible()
|
||||
|
||||
|
||||
@check
|
||||
def progress_notes_is_discovered_not_assumed(page, data):
|
||||
"""
|
||||
The whole chain: click, find the note wherever it landed, read it, close.
|
||||
|
||||
The fixture serves the note in an iframe, as eCW does. `opened == "frame"`
|
||||
is the assertion that matters - an earlier version of this code read the
|
||||
hosting modal's innerText and would have come back with nothing, because
|
||||
iframe content is not part of it.
|
||||
"""
|
||||
pn = claim_form.open_progress_notes(page)
|
||||
assert pn["opened"] == "frame", pn
|
||||
# Structure, not a text blob: eCW's own prisma-section labels.
|
||||
assert pn["assessments"][0]["code"] == "R69", pn["assessments"]
|
||||
assert pn["procedure_codes"][0]["code"] == "H0023", pn["procedure_codes"]
|
||||
assert pn["procedure_codes"][0]["modifiers"] == ["GT"], pn["procedure_codes"]
|
||||
assert pn["signoff_status"] == "Completed", pn["signoff_status"]
|
||||
assert pn["chart_locked"] is True, pn["chart_locked"]
|
||||
# And the viewer is gone, so the Cancel button underneath is reachable.
|
||||
assert page.locator(sel.SEL_PN_CLOSE).count() == 0 or page.locator(".modal#progressNoteViewerDiv9999999999999").first .is_visible() is False
|
||||
|
||||
|
||||
@check
|
||||
def summary_is_readable(page, data):
|
||||
data = dict(data)
|
||||
data["progress_notes"] = {"opened": "modal", "text": "x" * 120}
|
||||
text = claim_form.summary(data)
|
||||
for want in ("9104000", "Sample, Pat, Q", "Medcor Ready", "H0023",
|
||||
"90791", "224.00", "Prog. Notes"):
|
||||
assert want in text, f"{want!r} missing from summary:\n{text}"
|
||||
|
||||
|
||||
@check
|
||||
def no_selector_carries_the_timestamp(page, data):
|
||||
"""
|
||||
No selector VALUE in selectors/claim_form.py contains a long digit run.
|
||||
|
||||
Parsed with ast rather than scanned as text: the module docstring quotes the
|
||||
real timestamped ids on purpose, to explain the hazard, and a text scan
|
||||
flagged those quotes as the bug they warn about. Only actual assignments
|
||||
are checked.
|
||||
|
||||
The fixture stamps 9999999999999 where the captured DOM had 1788790287089,
|
||||
so a selector with the number baked in also fails the assertions above —
|
||||
this check is what names the cause instead of leaving eleven empty fields.
|
||||
"""
|
||||
import ast
|
||||
import re
|
||||
src = (ROOT / "src" / "ecw_bot" / "selectors" / "claim_form.py").read_text(
|
||||
encoding="utf-8")
|
||||
for node in ast.parse(src).body:
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
for const in ast.walk(node.value):
|
||||
if isinstance(const, ast.Constant) and isinstance(const.value, str):
|
||||
assert not re.search(r"\d{6,}", const.value), (
|
||||
f"timestamped selector: "
|
||||
f"{[t.id for t in node.targets if isinstance(t, ast.Name)]}"
|
||||
f" = {const.value!r}")
|
||||
|
||||
|
||||
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) # local only, no proxy
|
||||
page = browser.new_page()
|
||||
page.goto(FIXTURE.as_uri())
|
||||
data = claim_form.extract(page)
|
||||
|
||||
for fn in CHECKS:
|
||||
name = fn.__name__.replace("_", " ")
|
||||
try:
|
||||
fn(page, data)
|
||||
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(CHECKS) - failures}/{len(CHECKS)} 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
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
_cfg.SCREENSHOTS = pathlib.Path(tmp)
|
||||
try:
|
||||
return _run()
|
||||
finally:
|
||||
_cfg.SCREENSHOTS = orig
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user