Files
ecw-charge-validation/tests/test_conditions_telehealth.py
T
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

425 lines
16 KiB
Python

r"""
conditions/telehealth.py — the thin detect/apply/describe wrapper around
Condition 1's existing logic.
python tests\test_conditions_telehealth.py
The actual DOM mechanics (`note_mentions_telehealth`, `set_cpt_pos_modifier`)
are already covered exhaustively in test_telehealth_fix.py and do not move —
this file only proves the wrapper calls them correctly and shapes the result
the Condition Framework's runner expects. Reuses ub_claim.html, same as
test_telehealth_fix.py, for the one case that needs a real form.
"""
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" / "ub_claim.html"
from ecw_bot.conditions import telehealth # noqa: E402
from ecw_bot.conditions.base import ConditionResult # noqa: E402
from ecw_bot.selectors import claim_form as sel # noqa: E402
CASES = []
def case(fn):
CASES.append(fn)
return fn
@case
def detect_true_when_the_note_mentions_telehealth():
data = {"progress_notes": {"text": "Session Setting (PoS): TeleHealth"}}
assert telehealth.detect("C1", data) is True
@case
def detect_true_for_telephone_and_standalone_th_too():
assert telehealth.detect(
"C1", {"progress_notes": {"text": "Mode of Treatment: Telephone"}}
) is True
assert telehealth.detect(
"C1", {"progress_notes": {"text": "Visit type: TH"}}
) is True
@case
def detect_false_with_no_progress_notes_key_at_all():
"""A claim dict from extract() before Prog. Notes was opened — must not
raise on the missing key, just report nothing to detect."""
assert telehealth.detect("C1", {}) is False
@case
def detect_false_for_an_ordinary_note():
data = {"progress_notes": {"text": "Regular in-office visit, no video."}}
assert telehealth.detect("C1", data) is False
# --- modifier selection (condition #21 of the Charge Condition Document) --
#
# `_choose_modifier` returns `(modifier, decline_reason)` since review item
# D5 (2026-10-05), so apply() can tell "write this", "not covered"
# (Medicare) and "declined" apart.
@case
def audio_only_is_modifier_93_for_commercial_payers():
assert telehealth._choose_modifier("audio", "Aetna") == ("93", "")
@case
def video_is_modifier_95_for_commercial_payers():
assert telehealth._choose_modifier("video", "Aetna") == ("95", "")
@case
def medicaid_is_always_gt_even_for_an_audio_only_visit():
"""
The client's own expected results (claims 9123000, 9124000, 9122000 —
Medicaid-group telephone visits) keep or add GT, never 93: the
document's "93 for Audio only" applies to Commercial, not Medicaid.
"""
assert telehealth._choose_modifier("audio", "BH COLORADO ACCESS") \
== ("GT", "")
assert telehealth._choose_modifier("audio", "Medicaid") == ("GT", "")
@case
def every_rae_payer_gets_gt_like_medicaid_audio_or_video():
"""
The nine RAE payers are grouped "RAE payer", not "Medicaid", in
payer_groups.json — but they are Medicaid plans, and their telehealth
rule is the Medicaid one: POS 2 + GT (confirmed 2026-09-24).
"""
for payer in ("BH COLORADO ACCESS", "BH ROCKY MOUNTAIN HEALTH",
"BH ROCKY MTN HEALTH 2022", "BH ROCK MTN UB",
"Carelon Behavioral Health, Inc",
"CCH CO COMMUNITY HEALTH ALLIANCE", "HEALTH COLORADO INC",
"Northeast Colorado Health Department",
"Northeast Health Partners"):
for mode in ("audio", "video"):
assert telehealth._choose_modifier(mode, payer) == ("GT", ""), \
(payer, mode)
@case
def medicare_is_not_covered_in_every_mode():
"""
The actual bug this test guards: before this was fixed, _choose_modifier
checked mode == "audio" BEFORE the payer group, so an audio-only
Medicare claim got 93 from THIS condition too — while
medicare_telehealth.py also fired on the same row, double-writing it.
Payer group must be checked first, in every mode — and "not covered"
carries no decline reason: medicare_telehealth handles the claim, so
it is not a review.
"""
for mode in ("audio", "video"):
assert telehealth._choose_modifier(mode, "Humana Medicare") \
== telehealth.NOT_COVERED == (None, "")
@case
def a_medicare_advantage_plan_gets_the_commercial_modifier():
"""Ruling 2026-10-21: the dashboard's Commercial Ins — 93 / 95 (#21),
not G2025 (#22)."""
for plan in ("AARP Medicare Advantage", "Aetna Medicare ADV"):
assert telehealth._choose_modifier("audio", plan) == ("93", "")
assert telehealth._choose_modifier("video", plan) == ("95", "")
@case
def payers_with_no_rule_are_declined_with_a_reason_in_every_mode():
"""
Review item D5: the OLD expectation here was `_choose_modifier("audio",
"") == "93"` — an unmapped or blank payer on an audio visit got 93
written, because the audio branch ran before the payer lookup. An
unclassified Medicare plan then got rule #21 where #22 applies, and an
unmapped Medicaid MCO got 93 instead of GT. Now every payer with no
telehealth modifier rule is declined, audio or video, with the payer
and its group named.
"""
for payer, group in (("Some Unmapped Payer", "not in payer_groups.json"),
("OUTREACH PROGRAM", "Program"),
("DentaQuest", "Dentaquest")):
for mode in ("audio", "video"):
modifier, reason = telehealth._choose_modifier(mode, payer)
assert modifier is None, (payer, mode, modifier)
assert reason == (
f"telehealth visit, but payer {payer!r} ({group}) has no "
f"telehealth modifier rule — set POS/modifier by hand"), reason
# A blank payer is a read that found no name, not an unmapped payer:
# its reason says so instead of pointing at payer_groups.json (final
# review, 2026-10-05; this case used to expect "payer '' (not in
# payer_groups.json)").
for mode in ("audio", "video"):
for blank in ("", " "):
modifier, reason = telehealth._choose_modifier(mode, blank)
assert modifier is None, (blank, mode, modifier)
assert reason == ("telehealth visit, but no primary payer name "
"was read — set POS/modifier by hand"), reason
@case
def an_unloadable_mapping_is_declined_not_guessed():
original = telehealth.payer_groups.load_mapping
def broken():
raise ValueError("bad JSON")
telehealth.payer_groups.load_mapping = broken
try:
assert telehealth._choose_modifier("video", "Aetna") \
== (None, "payer_groups.json could not be loaded")
finally:
telehealth.payer_groups.load_mapping = original
# --- apply(), against the real fixture ------------------------------------
_MEDICAID_DATA = {
"progress_notes": {"text": "Session Setting (PoS): TeleHealth"},
"insurance": [{"Name": "BH COLORADO ACCESS"}],
}
@case
def apply_sets_pos_2_and_the_medicaid_modifier_on_the_first_row_only(page):
page.goto(FIXTURE.as_uri())
result = telehealth.apply(page, "9120000", _MEDICAID_DATA)
assert isinstance(result, ConditionResult)
assert result.condition_name == "telehealth"
assert result.changed is True
assert len(result.changes) == 1, result.changes
assert result.changes[0]["code"] == "H0023", result.changes
assert result.changes[0]["after"]["pos"] == "2", result.changes
assert result.changes[0]["after"]["mod1"] == "GT", result.changes
# Row 1 (90791) must be untouched — this condition is first-row-only.
row1_mod1 = page.locator(sel.SEL_CPT_ROWS).nth(1).locator(
sel.SEL_CPT_MOD1).first
assert row1_mod1.input_value() != "GT", \
"telehealth condition must not touch the second CPT row"
@case
def apply_keeps_gt_for_an_audio_only_visit_on_a_medicaid_payer(page):
page.goto(FIXTURE.as_uri())
data = {
"progress_notes": {"text": "Mode of Treatment: Telephone"},
"insurance": [{"Name": "BH COLORADO ACCESS"}],
}
result = telehealth.apply(page, "9120000", data)
assert result.changed is True
assert len(result.changes) == 1, result.changes
assert result.changes[0]["after"]["mod1"] == "GT", result.changes
@case
def apply_uses_93_for_an_audio_only_visit_on_a_commercial_payer(page):
page.goto(FIXTURE.as_uri())
data = {
"progress_notes": {"text": "Mode of Treatment: Telephone"},
"insurance": [{"Name": "Aetna"}],
}
result = telehealth.apply(page, "9120000", data)
assert result.changed is True
assert result.changes[0]["after"]["mod1"] == "93", result.changes
@case
def apply_declines_every_payer_with_no_telehealth_rule(page):
"""
Review item D5: unmapped, blank, Program and Dentaquest payers are
declined (a Review row with the reason), and nothing is written — the
old behaviour was a quiet none() on video and 93 written on audio.
"""
for payer in ("Some Unmapped Payer", "", "OUTREACH PROGRAM",
"DentaQuest"):
for note in ("Session Setting (PoS): TeleHealth",
"Mode of Treatment: Telephone"):
page.goto(FIXTURE.as_uri())
data = {"progress_notes": {"text": note},
"insurance": [{"Name": payer}]}
result = telehealth.apply(page, "9120000", data)
assert result.changed is False and result.changes == [], result
if payer:
assert f"payer {payer!r}" in result.review, result.review
assert "has no telehealth modifier rule" in result.review
else:
assert "no primary payer name was read" in result.review, \
result.review
row0 = page.locator(sel.SEL_CPT_ROWS).nth(0)
assert row0.locator(sel.SEL_CPT_POS).first.input_value() == "11", \
"a declined payer must leave the claim as read"
@case
def apply_uses_95_for_a_video_visit_on_a_commercial_payer(page):
page.goto(FIXTURE.as_uri())
data = {
"progress_notes": {"text": "TELEHEALTH: real-time audio and video"},
"insurance": [{"Name": "Aetna"}],
}
result = telehealth.apply(page, "9120000", data)
assert result.changed is True and result.review == "", result
assert result.changes[0]["after"]["mod1"] == "95", result.changes
@case
def apply_returns_none_not_review_for_a_medicare_payer(page):
"""medicare_telehealth fires on the same claim and handles it — a
review here would put a correctly handled claim in front of a human."""
page.goto(FIXTURE.as_uri())
data = {"progress_notes": {"text": "Mode of Treatment: Telephone"},
"insurance": [{"Name": "Humana Medicare"}]}
result = telehealth.apply(page, "9120000", data)
assert result == ConditionResult.none("telehealth"), result
@case
def apply_returns_none_for_an_already_correct_claim(page):
"""
Review item D3: a claim whose primary row already holds POS 2 (eCW
writes "02") and the right modifier is Pass, not "Corrections pending"
— none(), no review.
"""
page.goto(FIXTURE.as_uri())
row0 = page.locator(sel.SEL_CPT_ROWS).nth(0)
row0.locator(sel.SEL_CPT_POS).first.fill("02") # M1 is already GT
result = telehealth.apply(page, "9120000", _MEDICAID_DATA)
assert result == ConditionResult.none("telehealth"), result
assert row0.locator(sel.SEL_CPT_POS).first.input_value() == "02"
@case
def apply_sends_a_write_that_did_not_take_to_review(page):
"""A POS that reads back wrong after the fill (eCW reformatting it,
a field that ignores input) is kept in the result but marked review."""
page.goto(FIXTURE.as_uri())
page.evaluate("""() => {
const el = document.getElementById('uBClaimPOS0');
el.addEventListener('input', () => { el.value = '11'; });
}""")
result = telehealth.apply(page, "9120000", _MEDICAID_DATA)
assert result.changed is True, result
assert result.changes[0]["ok"] is False, result.changes
assert result.review == ("POS/modifier did not take on row 1 — set it "
"by hand"), result.review
assert "did NOT take" in telehealth.describe(result)
@case
def describe_and_summary_comment_say_what_actually_changed(page):
page.goto(FIXTURE.as_uri())
data = {"progress_notes": {"text": "TELEHEALTH: audio only visit"},
"insurance": [{"Name": "Aetna"}]}
result = telehealth.apply(page, "9120000", data)
text = telehealth.describe(result)
assert 'mode audio (matched "audio only")' in text, text
assert "93" in text and "H0023" in text, text
assert telehealth.summary_comment(result) == \
"Telehealth (POS 02, modifier 93 on H0023)"
page.goto(FIXTURE.as_uri())
result = telehealth.apply(page, "9120000", _MEDICAID_DATA)
assert "mode video (default — no audio-only evidence)" in \
telehealth.describe(result)
# The fixture's row already has M1 GT, so only POS was fixed — and only
# POS is named (final review, 2026-10-05; this used to expect
# "POS 02, modifier GT on H0023", reading the correct M1 as a fix).
assert telehealth.summary_comment(result) == \
"Telehealth (POS 02 on H0023)"
@case
def describe_mentions_the_changed_row(page):
page.goto(FIXTURE.as_uri())
result = telehealth.apply(page, "9120000", _MEDICAID_DATA)
text = telehealth.describe(result)
assert "H0023" in text, text
assert "POS" in text and "GT" in text, text
# _MEDICAID_DATA's note text is "Session Setting (PoS): TeleHealth" —
# the "telehealth" signal, not "telephone" or a TH tag — so the
# operator can see WHY this fired, not just that it did.
assert "telehealth" in text, text
@case
def a_live_grid_that_reads_empty_after_a_full_first_read_is_declined(page):
"""
Final review, 2026-10-05: the claim's first read had CPT rows, but the
live grid apply() re-read had none (not re-rendered after the tab
re-click). The fix was never applied — declined (Review), not none()
(Pass). With no CPT rows in the first read either it stays none().
"""
from ecw_bot.conditions.base import CPT_GRID_NOT_READ
page.goto(FIXTURE.as_uri())
page.evaluate("""() => document.querySelectorAll('tr[id^="CPTROW_"]')
.forEach(r => r.remove())""")
result = telehealth.apply(page, "9120000",
dict(_MEDICAID_DATA, cpt=[{"code": "H0023"}]))
assert result == ConditionResult.declined("telehealth",
CPT_GRID_NOT_READ), result
page.goto(FIXTURE.as_uri())
page.evaluate("""() => document.querySelectorAll('tr[id^="CPTROW_"]')
.forEach(r => r.remove())""")
result = telehealth.apply(page, "9120000", _MEDICAID_DATA)
assert result == ConditionResult.none("telehealth"), result
@case
def summary_comment_names_only_the_field_that_was_fixed():
def row(pos, mod):
return {"row": 0, "code": "99213", "changed": True, "ok": True,
"before": {"pos": pos, "mod1": mod},
"after": {"pos": "2", "mod1": "GT"}, "modifier": "GT"}
sc = telehealth.summary_comment
mk = lambda c: ConditionResult("telehealth", True, [c])
assert sc(mk(row("11", "GT"))) == "Telehealth (POS 02 on 99213)"
assert sc(mk(row("02", ""))) == "Telehealth (modifier GT on 99213)"
assert sc(mk(row("11", "95"))) == \
"Telehealth (POS 02, modifier GT on 99213)"
def main() -> 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()
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())