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,601 @@
|
||||
r"""
|
||||
conditions/medicare_telehealth.py — document condition #22: POS=2/M1=93-or-95
|
||||
(per audio/video mode) on the claim's primary CPT row, PLUS adding G2025 with
|
||||
the same modifier and zeroing/unchecking every other CPT row once G2025 is
|
||||
confirmed on the claim.
|
||||
|
||||
python tests\test_conditions_medicare_telehealth.py
|
||||
|
||||
Two fixtures, deliberately: ub_claim.html for the base detect/POS/M1 shape
|
||||
(same as test_conditions_telehealth.py), and hcfa_claim.html for the full
|
||||
G2025 flow — the Add-button-plus-Fee-Schedule-picker interaction is only
|
||||
verified evidence on the "Claim"/HCFA and Dental screens (see
|
||||
hcfa_claim.html's own comment), not the UB form. On ub_claim.html, the
|
||||
condition correctly reports "could not confirm G2025" and skips the
|
||||
cleanup step rather than guessing at a flow with no evidence behind it
|
||||
there — that honest degradation is itself part of what these tests prove.
|
||||
|
||||
Two other things this file exists to prove, beyond the basic
|
||||
detect/apply/describe shape:
|
||||
|
||||
* **this condition and the general telehealth condition never both fire
|
||||
on the same claim** — Medicare is excluded from telehealth.py's own
|
||||
modifier map specifically so this doesn't happen (see
|
||||
test_conditions_telehealth.py's `video_modifier_is_none_for_medicare_too`);
|
||||
* **G2025 gets the SAME audio/video modifier as the primary row**, not a
|
||||
hardcoded 95 — the document's own wording ("add G2025 with 95 modifier")
|
||||
is the video case; an audio-only Medicare visit needs 93 on both rows,
|
||||
the same reasoning `telehealth_mode()` already applies elsewhere.
|
||||
"""
|
||||
|
||||
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"
|
||||
HCFA_FIXTURE = pathlib.Path(__file__).resolve().parent / "fixtures" / "hcfa_claim.html"
|
||||
|
||||
from ecw_bot.conditions import medicare_telehealth # noqa: E402
|
||||
from ecw_bot.conditions.base import ConditionResult, run_all # noqa: E402
|
||||
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
||||
from ecw_bot.pages import claim_form # noqa: E402
|
||||
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
_MEDICARE_DATA = {
|
||||
"progress_notes": {"text": "Session Setting (PoS): TeleHealth"},
|
||||
"insurance": [{"Name": "Humana Medicare"}],
|
||||
}
|
||||
|
||||
|
||||
@case
|
||||
def detect_true_for_telehealth_plus_medicare():
|
||||
assert medicare_telehealth.detect("C1", _MEDICARE_DATA) is True
|
||||
|
||||
|
||||
@case
|
||||
def detect_false_when_the_note_does_not_mention_telehealth():
|
||||
data = {
|
||||
"progress_notes": {"text": "Regular in-office visit."},
|
||||
"insurance": [{"Name": "Humana Medicare"}],
|
||||
}
|
||||
assert medicare_telehealth.detect("C1", data) is False
|
||||
|
||||
|
||||
@case
|
||||
def detect_false_for_a_non_medicare_payer():
|
||||
data = {
|
||||
"progress_notes": {"text": "Session Setting (PoS): TeleHealth"},
|
||||
"insurance": [{"Name": "BH COLORADO ACCESS"}], # Medicaid
|
||||
}
|
||||
assert medicare_telehealth.detect("C1", data) is False
|
||||
|
||||
|
||||
@case
|
||||
def detect_false_with_no_insurance_row_at_all():
|
||||
data = {"progress_notes": {"text": "Session Setting (PoS): TeleHealth"}}
|
||||
assert medicare_telehealth.detect("C1", data) is False
|
||||
|
||||
|
||||
@case
|
||||
def apply_sets_pos_2_and_mod1_95_on_the_first_row_only(page):
|
||||
"""
|
||||
ub_claim.html has no Add-flow evidence (see this file's module
|
||||
docstring), so G2025 must come back "could not confirm" here — the
|
||||
POS/M1 change on the primary row must still land regardless, and the
|
||||
honest failure to confirm G2025 must mean the cleanup step is SKIPPED
|
||||
entirely, not applied against a claim with nothing else billable.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
result = medicare_telehealth.apply(page, "9120000", _MEDICARE_DATA)
|
||||
assert isinstance(result, ConditionResult)
|
||||
assert result.condition_name == "medicare_telehealth"
|
||||
assert result.changed is True
|
||||
|
||||
primary = result.changes[0]
|
||||
assert primary["code"] == "H0023", result.changes
|
||||
assert primary["after"]["pos"] == "2", result.changes
|
||||
assert primary["after"]["mod1"] == "95", result.changes
|
||||
|
||||
assert [c for c in result.changes if c.get("added")] == [], result.changes
|
||||
assert [c for c in result.changes if "fee" in c] == [], result.changes
|
||||
|
||||
|
||||
@case
|
||||
def apply_uses_93_for_an_audio_only_medicare_visit(page):
|
||||
"""
|
||||
Real live example (a progress note reading "THV Audio Only: Telehealth
|
||||
audio phone call only. No video."): Medicare telehealth is NOT always
|
||||
modifier 95 — an audio-only visit still needs 93 on the primary row,
|
||||
the same as any other payer's audio-only visit.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
data = {
|
||||
"progress_notes": {"text": "THV Audio Only: Telehealth audio "
|
||||
"phone call only. No video."},
|
||||
"insurance": [{"Name": "Humana Medicare"}],
|
||||
}
|
||||
result = medicare_telehealth.apply(page, "9120000", data)
|
||||
assert result.changed is True
|
||||
assert result.changes[0]["after"]["mod1"] == "93", result.changes
|
||||
|
||||
|
||||
@case
|
||||
def apply_adds_g2025_via_the_real_fee_schedule_picker_on_the_hcfa_screen(page):
|
||||
"""
|
||||
The full real flow, on the screen where it is actually verified (see
|
||||
hcfa_claim.html's own comment: claim 9109000's live evidence): click
|
||||
Add, search the Fee Schedule picker for G2025, select the one exact
|
||||
match, click its OK — never typing straight into the grid's trailing
|
||||
"*" row. "Other Primary CPT codes" then means every code but G2025,
|
||||
INCLUDING 99213 (which just got POS=2/M1=95) and G8510 (already
|
||||
excluded, but its fee is not yet zero) — per the document's own
|
||||
wording, not a guess.
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert result.changed is True
|
||||
|
||||
primary = result.changes[0]
|
||||
assert primary["code"] == "99213", result.changes
|
||||
|
||||
added = [c for c in result.changes if c.get("added")]
|
||||
assert len(added) == 1, result.changes
|
||||
assert added[0]["code"] == "G2025" and added[0]["modifier"] == "95", added
|
||||
|
||||
zeroed = {c["code"] for c in result.changes if "fee" in c}
|
||||
assert zeroed == {"99213", "J7298", "G8510"}, result.changes
|
||||
|
||||
g2025_row = page.locator(sel.SEL_CPT_ROWS).last
|
||||
assert g2025_row.locator(sel.SEL_CPT_CODE).first.input_value() == "G2025"
|
||||
assert g2025_row.locator(sel.SEL_CPT_MOD1).first.input_value() == "95"
|
||||
|
||||
|
||||
@case
|
||||
def apply_leaves_g2025_alone_and_still_cleans_up_when_it_is_already_present(page):
|
||||
"""
|
||||
G2025 already on the claim (a prior run, or an operator added it by
|
||||
hand): add_cpt_row() must never be attempted a second time, but the
|
||||
unchecked/zero-fee cleanup on every OTHER row still has to run. G2025
|
||||
is seeded directly here (not via add_cpt_row) - this case is about
|
||||
what apply() does once G2025 already exists, not about the add
|
||||
mechanism itself.
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
page.evaluate(
|
||||
"""(code) => {
|
||||
const tbody = document.querySelector(
|
||||
'#claimCPTCodesTable8888888888888 tbody');
|
||||
const addRowTr = tbody.querySelector('tr:last-child');
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = 'CPTROW_3';
|
||||
tr.innerHTML =
|
||||
'<td><input data-fieldname="claimCPTCode" value="' + code + '" readonly></td>'
|
||||
+ '<td><input data-fieldname="ClaimCPTMOD1" value="95"></td>'
|
||||
+ '<td><input data-fieldname="ClaimCPTBilledFee" value="25.00"></td>';
|
||||
tbody.insertBefore(tr, addRowTr);
|
||||
}""", "G2025")
|
||||
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
added = [c for c in result.changes if c.get("added")]
|
||||
assert added == [], result.changes # never re-added
|
||||
|
||||
zeroed = {c["code"] for c in result.changes if "fee" in c}
|
||||
assert zeroed == {"99213", "J7298", "G8510"}, result.changes
|
||||
g2025_row = page.locator(sel.SEL_CPT_ROWS).last
|
||||
assert g2025_row.locator(sel.SEL_CPT_MOD1).first.input_value() == "95", \
|
||||
"the pre-existing G2025 row's own modifier must be left untouched"
|
||||
|
||||
|
||||
@case
|
||||
def describe_names_the_row_and_the_modifier(page):
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
text = medicare_telehealth.describe(result)
|
||||
assert "99213" in text, text
|
||||
assert "95" in text, text
|
||||
assert "G2025" in text, text
|
||||
# _MEDICARE_DATA's note text is "Session Setting (PoS): TeleHealth" —
|
||||
# the "telehealth" signal — so the operator can see WHY this fired.
|
||||
assert "telehealth" in text, text
|
||||
|
||||
|
||||
@case
|
||||
def run_all_fires_only_medicare_telehealth_not_the_general_condition(page):
|
||||
"""
|
||||
Against the REAL registry (conditions.CONDITIONS), not fakes — proves
|
||||
telehealth.py and medicare_telehealth.py do not both write to the same
|
||||
Medicare claim's primary row. If they did, the second condition's fill()
|
||||
would overwrite the first's, and the confirmation dialog would show the
|
||||
same row twice.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
results = run_all(page, "9120000", _MEDICARE_DATA)
|
||||
assert len(results) == 1, results
|
||||
assert results[0].condition_name == "medicare_telehealth", results
|
||||
|
||||
|
||||
@case
|
||||
def run_all_still_fires_only_medicare_on_an_audio_only_medicare_claim(page):
|
||||
"""
|
||||
The exact scenario that would have double-fired before telehealth.py's
|
||||
_choose_modifier checked payer group before the audio/video split: an
|
||||
audio-only Medicare visit. Both conditions' detect() would return True
|
||||
for this claim, so this proves only one of them actually writes.
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
data = {
|
||||
"progress_notes": {"text": "THV Audio Only: Telehealth audio "
|
||||
"phone call only. No video."},
|
||||
"insurance": [{"Name": "Humana Medicare"}],
|
||||
}
|
||||
results = run_all(page, "9120000", data)
|
||||
assert len(results) == 1, results
|
||||
assert results[0].condition_name == "medicare_telehealth", results
|
||||
assert results[0].changes[0]["after"]["mod1"] == "93", results
|
||||
|
||||
|
||||
@case
|
||||
def a_g2025_that_cannot_be_added_is_reported_failed_and_sent_to_review(page):
|
||||
"""
|
||||
Review item D6: on ub_claim.html the Add flow has no evidence, so
|
||||
G2025 cannot be confirmed. The primary row DID change (POS 11 -> 2),
|
||||
so the result is still changed=True — the operator must see it — but
|
||||
it carries a G2025 failed record, a review reason (which keeps Save
|
||||
away), a plain describe() line, and a summary comment that says so
|
||||
instead of the fixed "G2025 added … other CPTs unchecked".
|
||||
"""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
result = medicare_telehealth.apply(page, "9120000", _MEDICARE_DATA)
|
||||
assert result.changed is True, result
|
||||
assert result.review == medicare_telehealth.G2025_NOT_ADDED == (
|
||||
"G2025 NOT added — add it by hand; the other CPT rows were NOT "
|
||||
"unchecked"), result.review
|
||||
failed = [c for c in result.changes if c.get("failed")]
|
||||
assert len(failed) == 1 and failed[0]["code"] == "G2025" \
|
||||
and failed[0]["row"] is None and failed[0]["modifier"] == "95", \
|
||||
result.changes
|
||||
|
||||
text = medicare_telehealth.describe(result)
|
||||
assert " G2025 NOT added — add it by hand; other rows NOT unchecked" \
|
||||
in text, text
|
||||
assert medicare_telehealth.summary_comment(result) == (
|
||||
"Medicare Telehealth (POS 02 / modifier 95 on H0023; G2025 NOT "
|
||||
"added — add by hand)")
|
||||
|
||||
# Through the runner: the review reaches the report (what the save
|
||||
# gate and the Charge Summary read), the comment replaces the fixed one.
|
||||
page.goto(FIXTURE.as_uri())
|
||||
report = {}
|
||||
run_all(page, "9120000", _MEDICARE_DATA,
|
||||
conditions=[medicare_telehealth], report=report)
|
||||
assert report["review"] == [{"name": "medicare_telehealth",
|
||||
"reason": medicare_telehealth.G2025_NOT_ADDED}
|
||||
], report
|
||||
assert "G2025 NOT added" in report["comments"]["medicare_telehealth"]
|
||||
|
||||
|
||||
@case
|
||||
def a_failed_g2025_with_nothing_else_changed_is_declined(page):
|
||||
"""Primary row already right (POS 02 / 95) and G2025 cannot be added:
|
||||
nothing changed, but the claim still lacks G2025 — declined, not
|
||||
none(), so it is a Review row rather than Pass."""
|
||||
page.goto(FIXTURE.as_uri())
|
||||
row0 = page.locator(sel.SEL_CPT_ROWS).nth(0)
|
||||
row0.locator(sel.SEL_CPT_POS).first.fill("02")
|
||||
row0.locator(sel.SEL_CPT_MOD1).first.fill("95")
|
||||
result = medicare_telehealth.apply(page, "9120000", _MEDICARE_DATA)
|
||||
assert result == ConditionResult.declined(
|
||||
"medicare_telehealth", medicare_telehealth.G2025_NOT_ADDED), result
|
||||
|
||||
|
||||
@case
|
||||
def a_g2025_modifier_that_does_not_take_is_sent_to_review(page):
|
||||
"""
|
||||
The add worked but M1 on the new G2025 row did not take (read back
|
||||
after `_set_row_modifier`, review item D6). Simulated by making the
|
||||
modifier write a no-op: the G2025 row reads back with a blank M1.
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
original = claim_form._set_row_modifier
|
||||
claim_form._set_row_modifier = lambda page, code, modifier: True
|
||||
try:
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
finally:
|
||||
claim_form._set_row_modifier = original
|
||||
assert result.changed is True, result
|
||||
assert result.review == ("G2025 added but its modifier did not take — "
|
||||
"check M1"), result.review
|
||||
assert [c for c in result.changes if c.get("added")], result.changes
|
||||
|
||||
|
||||
@case
|
||||
def g2025_modifier_ok_is_true_when_the_modifier_took(page):
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
g = claim_form.apply_medicare_telehealth_g2025(page, "95")
|
||||
assert g["g2025_added"] is True and g["g2025_modifier_ok"] is True, g
|
||||
|
||||
|
||||
@case
|
||||
def apply_twice_on_the_hcfa_screen_is_none_the_second_time(page):
|
||||
"""
|
||||
Review item D3: once G2025 is present, the primary row is POS 2 / 95
|
||||
and every other row is already unchecked at 0.00, the claim follows
|
||||
rule #22 — the second apply is none(), not a change and not a review.
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
first = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert first.changed is True and first.review == "", first
|
||||
# Unchecked rows and fee-only rows are counted apart (final review,
|
||||
# 2026-10-05; this used to expect "3 other CPT(s) unchecked/fee
|
||||
# zeroed", which suggested an uncheck on the row that only had its fee
|
||||
# zeroed).
|
||||
assert medicare_telehealth.summary_comment(first) == (
|
||||
"Medicare Telehealth (POS 02 / modifier 95 on 99213; G2025 added; "
|
||||
"2 other CPT(s) unchecked; 1 other CPT fee(s) zeroed)"), \
|
||||
medicare_telehealth.summary_comment(first)
|
||||
second = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert second == ConditionResult.none("medicare_telehealth"), second
|
||||
|
||||
|
||||
@case
|
||||
def an_already_correct_primary_row_still_runs_the_g2025_step(page):
|
||||
"""The primary row is already POS 02 / 95: it is not a change, but
|
||||
G2025 is still added and the other rows still zeroed."""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
row0 = page.locator(sel.SEL_CPT_ROWS).nth(0)
|
||||
row0.locator(sel.SEL_CPT_POS).first.fill("02")
|
||||
row0.locator(sel.SEL_CPT_MOD1).first.fill("95")
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert result.changed is True and result.review == "", result
|
||||
assert not [c for c in result.changes if "before" in c], result.changes
|
||||
assert [c for c in result.changes if c.get("added")], result.changes
|
||||
assert medicare_telehealth.summary_comment(result).startswith(
|
||||
"Medicare Telehealth (G2025 added; "), \
|
||||
medicare_telehealth.summary_comment(result)
|
||||
|
||||
|
||||
def _seed_g2025(page, mod1):
|
||||
"""Put a G2025 row with the given M1 on hcfa_claim.html directly — the
|
||||
same seeding as the already-present case above."""
|
||||
page.evaluate(
|
||||
"""(mod1) => {
|
||||
const tbody = document.querySelector(
|
||||
'#claimCPTCodesTable8888888888888 tbody');
|
||||
const addRowTr = tbody.querySelector('tr:last-child');
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = 'CPTROW_3';
|
||||
tr.innerHTML =
|
||||
'<td><input data-fieldname="claimCPTCode" value="G2025" readonly></td>'
|
||||
+ '<td><input data-fieldname="ClaimCPTMOD1" value="' + mod1 + '"></td>'
|
||||
+ '<td><input data-fieldname="ClaimCPTBilledFee" value="25.00"></td>';
|
||||
tbody.insertBefore(tr, addRowTr);
|
||||
}""", mod1)
|
||||
|
||||
|
||||
@case
|
||||
def an_already_present_g2025_with_the_right_modifier_reads_ok(page):
|
||||
"""Review fix round 1: a pre-existing G2025's M1 is read back too."""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
_seed_g2025(page, "95")
|
||||
g = claim_form.apply_medicare_telehealth_g2025(page, "95")
|
||||
assert g["g2025_added"] is False and g["g2025_confirmed"] is True, g
|
||||
assert g["g2025_present_modifier"] == "95", g
|
||||
assert g["g2025_present_modifier_ok"] is True, g
|
||||
assert g["g2025_modifier_ok"] is None, g
|
||||
|
||||
|
||||
@case
|
||||
def an_already_present_g2025_with_a_blank_modifier_is_sent_to_review(page):
|
||||
"""
|
||||
Review fix round 1 (2026-10-05): G2025 already on the claim with a
|
||||
blank M1. Before, its M1 was never read, and once nothing else was left
|
||||
to change the log said "already correct … G2025 present" and the claim
|
||||
passed. Now: the other rows are still cleaned up (G2025 IS on the
|
||||
claim), but the result carries a review, a describe() line and a
|
||||
summary comment naming the wrong M1 — and the M1 itself is left as is.
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
_seed_g2025(page, "")
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert result.changed is True, result
|
||||
assert result.review == "G2025 present but M1 is blank, not 95 — check it", \
|
||||
result.review
|
||||
zeroed = {c["code"] for c in result.changes if "fee" in c}
|
||||
assert zeroed == {"99213", "J7298", "G8510"}, result.changes
|
||||
assert [c for c in result.changes if c.get("mismatch")], result.changes
|
||||
|
||||
text = medicare_telehealth.describe(result)
|
||||
assert " G2025 already present but M1 is blank, not 95 — check it" \
|
||||
in text, text
|
||||
assert "G2025 present but M1 blank, not 95 — check it" in \
|
||||
medicare_telehealth.summary_comment(result), \
|
||||
medicare_telehealth.summary_comment(result)
|
||||
g2025_row = page.locator(sel.SEL_CPT_ROWS).last
|
||||
assert g2025_row.locator(sel.SEL_CPT_MOD1).first.input_value() == "", \
|
||||
"a pre-existing G2025's modifier is read, never rewritten"
|
||||
|
||||
|
||||
@case
|
||||
def a_wrong_g2025_modifier_with_nothing_else_to_change_is_declined_not_none(page):
|
||||
"""
|
||||
The exact case the reviewer named: the first apply adds G2025 (95) and
|
||||
cleans up; then the G2025 M1 is blanked. The second apply has nothing
|
||||
to change — but the claim does NOT follow rule #22, so it is declined
|
||||
(a Review row), not none() (a Pass).
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
first = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert first.changed is True and first.review == "", first
|
||||
page.locator(sel.SEL_CPT_ROWS).last.locator(
|
||||
sel.SEL_CPT_MOD1).first.fill("")
|
||||
second = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
assert second == ConditionResult.declined(
|
||||
"medicare_telehealth",
|
||||
"G2025 present but M1 is blank, not 95 — check it"), second
|
||||
|
||||
|
||||
@case
|
||||
def a_95_g2025_on_an_audio_only_visit_is_sent_to_review(page):
|
||||
"""G2025 already present with 95, but the note is audio-only (93):
|
||||
the primary row is moved to 93 and the G2025 mismatch goes to review."""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
_seed_g2025(page, "95")
|
||||
data = {"progress_notes": {"text": "THV Audio Only: Telehealth audio "
|
||||
"phone call only. No video."},
|
||||
"insurance": [{"Name": "Humana Medicare"}]}
|
||||
result = medicare_telehealth.apply(page, "9103000", data)
|
||||
assert result.changed is True, result
|
||||
assert result.review == "G2025 present but M1 is 95, not 93 — check it", \
|
||||
result.review
|
||||
|
||||
|
||||
@case
|
||||
def a_primary_write_that_does_not_take_stops_before_g2025(page):
|
||||
"""
|
||||
Review fix round 1 (2026-10-05): when the primary row's POS/M1 write
|
||||
does not take, the claim is already known to be wrong — no G2025 add
|
||||
and no other row zeroed/unchecked, just the changed primary row and a
|
||||
review. Simulated by making set_cpt_pos_modifier report ok False.
|
||||
"""
|
||||
page.goto(HCFA_FIXTURE.as_uri())
|
||||
before_rows = page.locator(sel.SEL_CPT_ROWS).count()
|
||||
before_fees = [page.locator(sel.SEL_CPT_ROWS).nth(i)
|
||||
.locator(sel.SEL_CPT_BILLED_FEE).first.input_value()
|
||||
for i in range(before_rows)]
|
||||
original = claim_form.set_cpt_pos_modifier
|
||||
claim_form.set_cpt_pos_modifier = lambda page, pos, modifier, only_first=False: [
|
||||
{"row": 0, "code": "99213", "before": {"pos": "11", "mod1": ""},
|
||||
"after": {"pos": "11", "mod1": ""}, "changed": True, "ok": False}]
|
||||
try:
|
||||
result = medicare_telehealth.apply(page, "9103000", _MEDICARE_DATA)
|
||||
finally:
|
||||
claim_form.set_cpt_pos_modifier = original
|
||||
assert result.changed is True, result
|
||||
assert result.review == (
|
||||
"POS/modifier did not take on row 1 — set it by hand; G2025 was NOT "
|
||||
"added and the other CPT rows were NOT unchecked"), result.review
|
||||
assert [c["code"] for c in result.changes] == ["99213"], result.changes
|
||||
assert not claim_form.cpt_code_present(page, "G2025"), \
|
||||
"G2025 must not be added after a primary write that did not take"
|
||||
assert page.locator(sel.SEL_CPT_ROWS).count() == before_rows
|
||||
after_fees = [page.locator(sel.SEL_CPT_ROWS).nth(i)
|
||||
.locator(sel.SEL_CPT_BILLED_FEE).first.input_value()
|
||||
for i in range(before_rows)]
|
||||
assert after_fees == before_fees, (before_fees, after_fees)
|
||||
assert "did NOT take" in medicare_telehealth.describe(result)
|
||||
|
||||
|
||||
@case
|
||||
def describe_shows_the_mode_evidence(page):
|
||||
page.goto(FIXTURE.as_uri())
|
||||
data = {"progress_notes": {"text": "THV Audio Only: Telehealth audio "
|
||||
"phone call only. No video."},
|
||||
"insurance": [{"Name": "Humana Medicare"}]}
|
||||
result = medicare_telehealth.apply(page, "9120000", data)
|
||||
text = medicare_telehealth.describe(result)
|
||||
assert 'mode audio (matched "Audio Only")' in text, text
|
||||
|
||||
|
||||
@case
|
||||
def a_live_grid_that_reads_empty_after_a_full_first_read_is_declined(page):
|
||||
"""Final review, 2026-10-05: as for telehealth — declined, not 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 = medicare_telehealth.apply(
|
||||
page, "9120000", dict(_MEDICARE_DATA, cpt=[{"code": "99213"}]))
|
||||
assert result == ConditionResult.declined("medicare_telehealth",
|
||||
CPT_GRID_NOT_READ), result
|
||||
|
||||
|
||||
@case
|
||||
def summary_comment_counts_unchecked_and_fee_only_rows_apart():
|
||||
"""
|
||||
Final review, 2026-10-05: on the UB form only fees are zeroed, and
|
||||
"N other CPT(s) unchecked/fee zeroed" suggested an uncheck that never
|
||||
happened. Each kind is counted on its own. A blank M1 read-back names
|
||||
the visit's modifier, never "?", and only the field that was fixed.
|
||||
"""
|
||||
sc = medicare_telehealth.summary_comment
|
||||
fee = lambda n: {"row": n, "code": f"9000{n}",
|
||||
"fee": {"before": "10.00", "after": "0.00"}}
|
||||
both = lambda n: dict(fee(n), unchecked=True)
|
||||
primary = {"row": 0, "code": "99213", "changed": True, "ok": False,
|
||||
"before": {"pos": "2", "mod1": ""},
|
||||
"after": {"pos": "2", "mod1": ""}, "modifier": "95"}
|
||||
added = {"row": None, "code": "G2025", "added": True, "modifier": "95"}
|
||||
r = ConditionResult("medicare_telehealth", True,
|
||||
[primary, added, fee(1), fee(2)])
|
||||
text = sc(r)
|
||||
assert text == ("Medicare Telehealth (modifier 95 on 99213 did NOT take "
|
||||
"— set by hand; G2025 added; 2 other CPT fee(s) "
|
||||
"zeroed)"), text
|
||||
assert "unchecked" not in text and "?" not in text, text
|
||||
r = ConditionResult("medicare_telehealth", True,
|
||||
[added, both(1), both(2)])
|
||||
assert sc(r) == ("Medicare Telehealth (G2025 added; 2 other CPT(s) "
|
||||
"unchecked)"), sc(r)
|
||||
|
||||
|
||||
def _run_cases() -> 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
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from ecw_bot import settings as _cfg
|
||||
# These cases exercise eCW's own click and Add flow, as a LIVE-WRITE run
|
||||
# does. VERIFY clicks no box and adds no row (eCW saves both at once,
|
||||
# 2026-10-16): that is tests/test_verify_no_live_writes.py.
|
||||
orig = _cfg.SAVE_CORRECTIONS
|
||||
_cfg.SAVE_CORRECTIONS = True
|
||||
try:
|
||||
return _run_cases()
|
||||
finally:
|
||||
_cfg.SAVE_CORRECTIONS = orig
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user