Files
ManikandanandClaude Opus 5.5 ba15a574ab 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>
2026-10-23 18:38:17 +05:30

247 lines
8.8 KiB
Python

r"""
Dental claims: "HIGH RISK" in Billing Notes (document #15), and the
claim's one per-claim box (SHOW_EACH_CLAIM) that reports it with the
sliding fee — since review item D9, not a box of _dental_checks' own.
python tests\test_dental_high_risk.py
"""
import sys
import pathlib
import inspect
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from ecw_bot.pages import dental_notes # noqa: E402
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
CASES = []
# Shaped like a live claim's Billing Notes, fictional text: 79+ characters
# from HIGH RISK to the end.
NOTES_HIGH = ("notesEligibility :2026-09-04 @ 12:04:10 MDT : Eligible : "
"Eligible; HIGH RISK for caries - caries risk assessment done, "
"fluoride varnish applied, recall 3 months")
NOTES_PLAIN = "notesEligibility :2026-09-04 @ 12:04:10 MDT : Eligible : Eligible"
DENTAL = """
<div id="dentalClaim7777777777777">
<textarea id="dentalClaimNotes" ng-model="ClaimData.notes"></textarea>
</div>
"""
def case(fn):
CASES.append(fn)
return fn
def _dental(page, notes):
page.set_content(DENTAL)
# Angular sets .value, not the text node — so does the test.
page.evaluate("t => document.querySelector('#dentalClaimNotes').value = t",
notes)
# ── keyword (no browser) ─────────────────────────────────────────────────────
@case
def high_risk_matches_any_case_and_spacing():
for t in (NOTES_HIGH, "high risk", "High-Risk pt", "HIGHRISK", "high risk"):
assert dental_notes.mentions_high_risk(t) is True, t
tail = " " + NOTES_PLAIN + "; " + NOTES_PLAIN # well over 79
for t in ("high risk", "High-Risk pt", "HIGHRISK", "high risk"):
assert dental_notes.is_high_risk(t + tail) is True, t
@case
def plain_notes_are_not_high_risk():
for t in (NOTES_PLAIN, "", None, "risk assessment high",
NOTES_PLAIN * 3):
assert dental_notes.is_high_risk(t) is False, t
# ── the length rule (operator, 2026-10-12; from HIGH RISK since 2026-10-21) ─
def _notes_of(length):
"""Notes whose part from HIGH RISK to the end is exactly `length`
characters, after a first line that must not count."""
head = "HIGH RISK for caries"
return "Pt called for appt; eligible\n" + head + "x" * (length - len(head))
@case
def high_risk_needs_79_characters_or_more():
assert dental_notes.HIGH_RISK_MIN_CHARS == 79
assert dental_notes.is_high_risk(_notes_of(79)) is True
assert dental_notes.is_high_risk(_notes_of(120)) is True
assert dental_notes.is_high_risk(_notes_of(78)) is False
@case
def trailing_blanks_do_not_count_toward_79():
assert dental_notes.is_high_risk(_notes_of(78) + " \n ") is False
assert dental_notes.is_high_risk(_notes_of(79) + " ") is True
@case
def text_before_high_risk_does_not_count():
"""Operator 2026-10-21: count from HIGH RISK, not the whole notes."""
before = NOTES_PLAIN * 3 + "\n" # 190+ characters before
assert dental_notes.is_high_risk(before + "HIGH RISK for caries") is False
assert dental_notes.high_risk_length(before + "HIGH RISK for caries") == 20
@case
def later_lines_after_high_risk_count():
"""HIGH RISK on line 2: line 2 and every line after it count."""
notes = ("Pt called for appt\nHIGH RISK - caries risk assessment done\n"
"fluoride varnish applied, recall in 3 months")
assert dental_notes.high_risk_length(notes) == len(notes) - 19
assert dental_notes.is_high_risk(notes) is True
assert dental_notes.high_risk_length("no marker here " * 10) == 0
@case
def short_notes_saying_high_risk_are_not_high_risk_and_say_why(page):
import io
import contextlib
_dental(page, _notes_of(50))
with contextlib.redirect_stdout(io.StringIO()) as out:
r = dental_notes.check(page, "5000000")
assert r["high_risk"] is False and r["match"] == "", r
assert r["length"] == 50, r
assert "50 characters (79 needed)" in out.getvalue(), out.getvalue()
@case
def message_says_yes_no_or_unknown():
assert "YES" in dental_notes.message({"high_risk": True, "match": "HIGH RISK"})
assert "NO" in dental_notes.message({"high_risk": False, "match": ""})
assert "UNKNOWN" in dental_notes.message({"high_risk": None, "match": ""})
# ── the Dental screen (browser) ──────────────────────────────────────────────
@case
def check_reads_billing_notes_and_flags_high_risk(page):
_dental(page, NOTES_HIGH)
r = dental_notes.check(page, "9115000")
assert r["high_risk"] is True, r
assert "HIGH RISK" in r["match"], r
@case
def check_says_not_high_risk_for_plain_notes(page):
_dental(page, NOTES_PLAIN)
assert dental_notes.check(page, "5000000")["high_risk"] is False
@case
def missing_notes_box_is_unknown_not_a_crash(page):
page.set_content("<div id='dentalClaim1'></div>")
assert dental_notes.check(page, "X")["high_risk"] is None
# ── the claim's one box (review items D9/P6, 2026-10-05) ────────────────────
# _dental_checks used to show a box of its own, ungated, and the per-claim
# box after it printed a page of "?". Now _dental_checks shows nothing and
# the per-claim box (_claim_box, gated by SHOW_EACH_CLAIM) carries the
# Dental results. Updated: these three cases used to assert the box came
# from _dental_checks itself.
HEAD = "Claim 1/1\n\nClaim No : 9115000"
def _checks(page, claim_id):
"""_dental_checks with info_box forbidden: it must not show a box."""
def _no_box(*a, **k):
raise AssertionError("_dental_checks must not show a box (D9/P6)")
old_box, old_sf = flow.msgbox.info_box, flow.cfg.CHECK_SLIDING_FEE
old_hr = flow.cfg.CHECK_HIGH_RISK
flow.msgbox.info_box = _no_box
flow.cfg.CHECK_SLIDING_FEE = False # no Patient Info on this page
flow.cfg.CHECK_HIGH_RISK = True
try:
return flow._dental_checks(page, claim_id)
finally:
flow.msgbox.info_box, flow.cfg.CHECK_SLIDING_FEE = old_box, old_sf
flow.cfg.CHECK_HIGH_RISK = old_hr
@case
def dental_checks_shows_no_box_and_the_claim_box_carries_high_risk(page):
_dental(page, NOTES_HIGH)
data = _checks(page, "9115000")
assert data["high_risk"]["high_risk"] is True, data
title, body = flow._claim_box(HEAD, "9115000", data,
flow.charge_summary.claim_outcome(True, data))
assert "9115000" in title and "HIGH RISK" in title, title
assert "YES" in body and "Read-only" in body, body
assert body.startswith(HEAD), body
@case
def dental_claim_box_title_has_no_high_risk_tag_when_not(page):
_dental(page, NOTES_PLAIN)
data = _checks(page, "5000000")
title, body = flow._claim_box(HEAD, "5000000", data, None)
assert "HIGH RISK" not in title, title
assert "not a high risk claim" in body, body
assert flow.claim_form.NOT_LOADED_NOTE not in body, body
@case
def dental_claim_box_leads_with_not_loaded_when_the_form_never_loaded(page):
"""
wait_claim_loaded() gave up: "not a high risk claim" was read off Billing
Notes that may simply not have arrived yet, so the Dental part of the
box says so first rather than showing it as a confident result (review
of unit C). The caller puts the reason on the data as `_not_evaluated`.
"""
_dental(page, NOTES_PLAIN)
why = "Dental form did not finish loading in 20s (0 service lines)"
data = _checks(page, "5000000")
data["_not_evaluated"] = why
_, body = flow._claim_box(HEAD, "5000000", data, None)
detail = body.split(flow.RULE + "\n", 1)[1].splitlines()
assert detail[0] == flow.claim_form.NOT_LOADED_NOTE, body
assert detail[1] == f"({why})", body
assert "not a high risk claim" in body, body
def main() -> int:
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()
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
if __name__ == "__main__":
sys.exit(main())