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,424 @@
|
||||
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())
|
||||
Reference in new issue
Block a user