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>
247 lines
8.8 KiB
Python
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())
|