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())