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>
784 lines
32 KiB
Python
784 lines
32 KiB
Python
r"""
|
|
export_medcor_ready - the SAVE_CORRECTIONS gate and the review box under it.
|
|
|
|
python tests\test_save_corrections_gate.py
|
|
|
|
Real incident this guards against: claim 9103000 got saved for real during
|
|
a live test run (Logs\run_20260917_172622.log: "[COND 9103000] Saved:
|
|
depression_dx_pointer") because someone answered the confirmation box while
|
|
conditions were still being verified against live claims, not because the
|
|
code did anything wrong. SAVE_CORRECTIONS=0 makes that impossible while a
|
|
condition is still under test: the confirmation box must never even
|
|
appear, and save_claim() must never be called, regardless of what a human
|
|
would have answered.
|
|
|
|
No Playwright page needed - SAVE_CORRECTIONS=0 must return before the
|
|
function ever touches one.
|
|
"""
|
|
|
|
import io
|
|
import sys
|
|
import pathlib
|
|
import types
|
|
import contextlib
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
from ecw_bot import settings as cfg # noqa: E402
|
|
from ecw_bot.core import msgbox # noqa: E402
|
|
from ecw_bot.pages import claim_form # noqa: E402
|
|
from ecw_bot.conditions.base import ConditionResult # noqa: E402
|
|
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
|
|
|
|
CASES = []
|
|
|
|
|
|
def case(fn):
|
|
CASES.append(fn)
|
|
return fn
|
|
|
|
|
|
_RESULT = [ConditionResult(
|
|
condition_name="depression_dx_pointer", changed=True,
|
|
changes=[{"row": 0, "code": "G8510",
|
|
"before": {"icd": ["1", "", "", ""]},
|
|
"after": {"icd": ["2", "", "", ""]}}])]
|
|
|
|
|
|
class _Called(Exception):
|
|
"""Raised by a stub to prove it was reached, without needing a mock lib."""
|
|
|
|
|
|
def _forbidden(*a, **k):
|
|
raise _Called("must not be called while SAVE_CORRECTIONS is off")
|
|
|
|
|
|
@case
|
|
def save_corrections_off_never_shows_the_box_or_saves():
|
|
orig_save, orig_flag = cfg.SAVE_CORRECTIONS, cfg.SAVE_CORRECTIONS
|
|
orig_ok_cancel = msgbox.ok_cancel
|
|
orig_save_claim = claim_form.save_claim
|
|
cfg.SAVE_CORRECTIONS = False
|
|
msgbox.ok_cancel = _forbidden
|
|
claim_form.save_claim = _forbidden
|
|
try:
|
|
saved = flow._confirm_and_save(None, "9103000", _RESULT)
|
|
assert saved is False, saved
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS = orig_flag
|
|
msgbox.ok_cancel = orig_ok_cancel
|
|
claim_form.save_claim = orig_save_claim
|
|
|
|
|
|
@case
|
|
def save_corrections_on_and_operator_approves_still_saves():
|
|
"""
|
|
The pre-existing path, unchanged: proves the gate only ever REMOVES the
|
|
save, never adds a save that was not already going to happen.
|
|
"""
|
|
orig_flag = cfg.SAVE_CORRECTIONS
|
|
orig_ok_cancel = msgbox.ok_cancel
|
|
orig_save_claim = claim_form.save_claim
|
|
cfg.SAVE_CORRECTIONS = True
|
|
msgbox.ok_cancel = lambda *a, **k: True
|
|
claim_form.save_claim = lambda page: True
|
|
try:
|
|
saved = flow._confirm_and_save(None, "9103000", _RESULT)
|
|
assert saved is True, saved
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS = orig_flag
|
|
msgbox.ok_cancel = orig_ok_cancel
|
|
claim_form.save_claim = orig_save_claim
|
|
|
|
|
|
@case
|
|
def save_box_makes_cancel_the_default_button():
|
|
"""
|
|
Review item P1: OK on this box writes the claim to live eCW, so Enter
|
|
must answer Cancel. Proven at the call site; test_msgbox.py proves that
|
|
default_cancel=True really adds MB_DEFBUTTON2.
|
|
"""
|
|
calls = []
|
|
orig_flag = cfg.SAVE_CORRECTIONS
|
|
orig_ok_cancel = msgbox.ok_cancel
|
|
orig_save_claim = claim_form.save_claim
|
|
cfg.SAVE_CORRECTIONS = True
|
|
msgbox.ok_cancel = lambda *a, **k: calls.append(k) or False
|
|
claim_form.save_claim = _forbidden
|
|
try:
|
|
flow._confirm_and_save(None, "9103000", _RESULT)
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS = orig_flag
|
|
msgbox.ok_cancel = orig_ok_cancel
|
|
claim_form.save_claim = orig_save_claim
|
|
assert len(calls) == 1, calls
|
|
assert calls[0].get("default_cancel") is True, calls
|
|
|
|
|
|
@case
|
|
def save_corrections_on_and_operator_declines_does_not_save():
|
|
orig_flag = cfg.SAVE_CORRECTIONS
|
|
orig_ok_cancel = msgbox.ok_cancel
|
|
orig_save_claim = claim_form.save_claim
|
|
cfg.SAVE_CORRECTIONS = True
|
|
msgbox.ok_cancel = lambda *a, **k: False
|
|
claim_form.save_claim = _forbidden
|
|
try:
|
|
saved = flow._confirm_and_save(None, "9103000", _RESULT)
|
|
assert saved is False, saved
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS = orig_flag
|
|
msgbox.ok_cancel = orig_ok_cancel
|
|
claim_form.save_claim = orig_save_claim
|
|
|
|
|
|
@case
|
|
def no_results_never_saves_regardless_of_the_flag():
|
|
saved = flow._confirm_and_save(None, "9103000", [])
|
|
assert saved is False, saved
|
|
|
|
|
|
# =============================================================================
|
|
# _review_before_close - the pause the gate was missing
|
|
# =============================================================================
|
|
# SAVE_CORRECTIONS=0 filled the form in and clicked Cancel in the same breath,
|
|
# so "applied for review" was only ever reviewable in the log and the
|
|
# screenshot. These cases pin the pause to exactly the path that lacked one,
|
|
# and - the point of the whole gate - prove it still cannot write.
|
|
|
|
_SHOWN = []
|
|
|
|
|
|
def _record_info_box(title, msg):
|
|
_SHOWN.append((title, msg))
|
|
|
|
|
|
def _with(save_corrections, review_before_close, results, info_box):
|
|
"""Run _review_before_close under a given config, restoring it after."""
|
|
orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE,
|
|
msgbox.info_box, claim_form.save_claim)
|
|
_SHOWN.clear()
|
|
cfg.SAVE_CORRECTIONS = save_corrections
|
|
cfg.REVIEW_BEFORE_CLOSE = review_before_close
|
|
msgbox.info_box = info_box
|
|
# Nothing on this path may write, whatever the operator does with the box.
|
|
claim_form.save_claim = _forbidden
|
|
try:
|
|
return flow._review_before_close("9103000", results)
|
|
finally:
|
|
(cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE,
|
|
msgbox.info_box, claim_form.save_claim) = orig
|
|
|
|
|
|
@case
|
|
def review_box_appears_when_the_save_gate_is_off():
|
|
"""The gap: gate off, a condition fired, so the form must be held open."""
|
|
shown = _with(False, True, _RESULT, _record_info_box)
|
|
assert shown is True, shown
|
|
assert len(_SHOWN) == 1, _SHOWN
|
|
title, msg = _SHOWN[0]
|
|
assert "9103000" in title, title
|
|
# The operator has to be told what to look for, not just that something
|
|
# happened - so the condition's own describe() output has to be in there.
|
|
assert "G8510" in msg, msg
|
|
|
|
|
|
@case
|
|
def review_box_does_not_appear_when_the_save_gate_is_on():
|
|
"""
|
|
With the gate on, _confirm_and_save's own box is already the pause before
|
|
closing. A second one would re-ask a decision that was just made.
|
|
"""
|
|
shown = _with(True, True, _RESULT, _forbidden)
|
|
assert shown is False, shown
|
|
|
|
|
|
@case
|
|
def review_box_does_not_appear_when_no_condition_fired():
|
|
shown = _with(False, True, [], _forbidden)
|
|
assert shown is False, shown
|
|
|
|
|
|
@case
|
|
def review_box_can_be_switched_off_for_unattended_runs():
|
|
shown = _with(False, False, _RESULT, _forbidden)
|
|
assert shown is False, shown
|
|
|
|
|
|
@case
|
|
def review_box_is_shown_before_the_form_is_closed():
|
|
"""
|
|
The ordering is the whole feature: a box after close_claim() would be
|
|
describing a form that is no longer on screen. Recorded rather than
|
|
assumed, because nothing else in _read_claim_form's `finally` enforces it.
|
|
|
|
`conditions.run_all` is stubbed wholesale here, so it never actually
|
|
calls the `before_apply` callback `_read_claim_form` passes it — that
|
|
wiring (and the BEFORE box's own content) is proven separately below,
|
|
against the real `run_all`. This case only needs the stub to accept
|
|
the new keyword without blowing up.
|
|
"""
|
|
calls = []
|
|
orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS,
|
|
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.extract, claim_form.save, claim_form.close_claim,
|
|
claim_form.save_claim, flow.conditions.run_all)
|
|
cfg.SAVE_CORRECTIONS = False
|
|
cfg.REVIEW_BEFORE_CLOSE = True
|
|
cfg.APPLY_CORRECTIONS = True
|
|
cfg.OPEN_CLAIM_FORM = True
|
|
cfg.OPEN_PROGRESS_NOTES = False
|
|
msgbox.info_box = lambda t, m: calls.append("review_box")
|
|
claim_form.open_claim = lambda page, cid: True
|
|
# Stubbed too (review item D7): on page=None the real one would
|
|
# poll its full 20s before giving up.
|
|
claim_form.wait_claim_loaded = lambda page, cid: None
|
|
claim_form.open_cpt_hcpcs_tab = lambda page: None
|
|
claim_form.extract = lambda page: {"cpt": [], "icd": [], "revenue": []}
|
|
claim_form.save = lambda data: pathlib.Path("unused.json")
|
|
claim_form.close_claim = lambda page: calls.append("close_claim")
|
|
claim_form.save_claim = _forbidden
|
|
flow.conditions.run_all = (lambda page, cid, data, before_apply=None,
|
|
report=None: _RESULT)
|
|
try:
|
|
flow._read_claim_form(None, "9103000")
|
|
finally:
|
|
(cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS,
|
|
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.extract, claim_form.save, claim_form.close_claim,
|
|
claim_form.save_claim, flow.conditions.run_all) = orig
|
|
|
|
assert calls == ["review_box", "close_claim"], calls
|
|
|
|
|
|
# =============================================================================
|
|
# _log_before_apply - the BEFORE half, now a log line only (review item D9)
|
|
# =============================================================================
|
|
# _review_before_close shows what changed, once apply() has already written
|
|
# to the DOM. In front of it there used to be a second box, "N condition(s)
|
|
# matched - about to apply". Since D9 (2026-10-05) that is a log line only:
|
|
# a corrected claim gets ONE box, and these cases pin that the old box is
|
|
# gone whatever REVIEW_BEFORE_CLOSE says, while its log line stays.
|
|
# (Updated: these two cases used to assert the box appeared with REVIEW on.)
|
|
|
|
_FIRED = [types.SimpleNamespace(NAME="depression_dx_pointer"),
|
|
types.SimpleNamespace(NAME="j2930_j2001_update")]
|
|
|
|
|
|
def _log_before_apply_with(review_before_close):
|
|
orig = (cfg.REVIEW_BEFORE_CLOSE, msgbox.info_box)
|
|
cfg.REVIEW_BEFORE_CLOSE = review_before_close
|
|
msgbox.info_box = _forbidden
|
|
out = io.StringIO()
|
|
try:
|
|
with contextlib.redirect_stdout(out):
|
|
flow._log_before_apply("9103000", _FIRED)
|
|
finally:
|
|
cfg.REVIEW_BEFORE_CLOSE, msgbox.info_box = orig
|
|
return out.getvalue()
|
|
|
|
|
|
@case
|
|
def before_apply_logs_the_matched_conditions_and_shows_no_box():
|
|
out = _log_before_apply_with(True)
|
|
assert ("[COND 9103000] 2 condition(s) matched — about to apply: "
|
|
"depression_dx_pointer, j2930_j2001_update") in out, out
|
|
|
|
|
|
@case
|
|
def before_apply_still_logs_with_review_before_close_off():
|
|
out = _log_before_apply_with(False)
|
|
assert "2 condition(s) matched — about to apply" in out, out
|
|
|
|
|
|
@case
|
|
def read_claim_form_wires_before_apply_through_to_run_all():
|
|
"""
|
|
Proves the actual wiring in `_read_claim_form` — not a stubbed
|
|
`run_all`, but the real one, fed fake condition modules so this stays
|
|
independent of whatever the real registry currently contains. If
|
|
`_read_claim_form` stopped passing `before_apply=...`, or passed it
|
|
with the wrong `claim_id`, the BEFORE log line would never be written
|
|
on a real run and this would catch it.
|
|
|
|
Updated for review item D9: the BEFORE half used to be a box, first in
|
|
`calls`; it is now only the log line, so the one box is the review box.
|
|
"""
|
|
calls = []
|
|
orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS,
|
|
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.extract, claim_form.save, claim_form.close_claim,
|
|
claim_form.save_claim, flow.conditions.CONDITIONS)
|
|
|
|
fake = types.ModuleType("fake_cond")
|
|
fake.NAME = "fake_cond"
|
|
fake.detect = lambda claim_id, data: True
|
|
fake.apply = lambda page, claim_id, data: (
|
|
calls.append("apply:fake_cond") or _RESULT[0])
|
|
|
|
cfg.SAVE_CORRECTIONS = False
|
|
cfg.REVIEW_BEFORE_CLOSE = True
|
|
cfg.APPLY_CORRECTIONS = True
|
|
cfg.OPEN_CLAIM_FORM = True
|
|
cfg.OPEN_PROGRESS_NOTES = False
|
|
msgbox.info_box = lambda t, m: calls.append(f"box:{t}")
|
|
claim_form.open_claim = lambda page, cid: True
|
|
# Stubbed too (review item D7): on page=None the real one would
|
|
# poll its full 20s before giving up.
|
|
claim_form.wait_claim_loaded = lambda page, cid: None
|
|
claim_form.open_cpt_hcpcs_tab = lambda page: None
|
|
claim_form.extract = lambda page: {"cpt": [], "icd": [], "revenue": []}
|
|
claim_form.save = lambda data: pathlib.Path("unused.json")
|
|
claim_form.close_claim = lambda page: calls.append("close_claim")
|
|
claim_form.save_claim = _forbidden
|
|
flow.conditions.CONDITIONS = [fake]
|
|
out = io.StringIO()
|
|
try:
|
|
with contextlib.redirect_stdout(out):
|
|
flow._read_claim_form(None, "9103000")
|
|
finally:
|
|
(cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS,
|
|
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
msgbox.info_box, claim_form.open_claim, claim_form.open_cpt_hcpcs_tab,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.extract, claim_form.save, claim_form.close_claim,
|
|
claim_form.save_claim, flow.conditions.CONDITIONS) = orig
|
|
|
|
# BEFORE log line (no box), the fix lands, the ONE box, then close.
|
|
assert ("[COND 9103000] 1 condition(s) matched — about to apply: "
|
|
"fake_cond") in out.getvalue(), out.getvalue()
|
|
assert calls == ["apply:fake_cond",
|
|
"box:eCW Bot — Claim 9103000: review before close",
|
|
"close_claim"], calls
|
|
|
|
|
|
@case
|
|
def read_claim_form_names_the_snapshot_by_the_searched_claim_id():
|
|
"""
|
|
The HCFA screen has no UB claim-number field, so extract() returns no
|
|
claim_no there and every HCFA claim used to overwrite one shared
|
|
Output\\claims\\unknown.json — losing 9109000's progress note.
|
|
"""
|
|
saved = []
|
|
orig = (cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
claim_form.open_claim, claim_form.is_dental_claim_form,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.open_cpt_hcpcs_tab, claim_form.extract, claim_form.save,
|
|
claim_form.close_claim)
|
|
cfg.APPLY_CORRECTIONS = False
|
|
cfg.OPEN_CLAIM_FORM = True
|
|
cfg.OPEN_PROGRESS_NOTES = False
|
|
claim_form.open_claim = lambda page, cid: True
|
|
# Stubbed too (review item D7): on page=None the real one would
|
|
# poll its full 20s before giving up.
|
|
claim_form.wait_claim_loaded = lambda page, cid: None
|
|
claim_form.is_dental_claim_form = lambda page: False
|
|
claim_form.open_cpt_hcpcs_tab = lambda page: None
|
|
claim_form.extract = lambda page: {"cpt": [], "icd": [], "revenue": []}
|
|
claim_form.save = lambda data: (saved.append(data.get("claim_no"))
|
|
or pathlib.Path("unused.json"))
|
|
claim_form.close_claim = lambda page: None
|
|
try:
|
|
flow._read_claim_form(None, "9109000")
|
|
finally:
|
|
(cfg.APPLY_CORRECTIONS, cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
claim_form.open_claim, claim_form.is_dental_claim_form,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.open_cpt_hcpcs_tab, claim_form.extract, claim_form.save,
|
|
claim_form.close_claim) = orig
|
|
assert saved == ["9109000"], saved
|
|
|
|
|
|
# =============================================================================
|
|
# A tainted claim is never offered for save (review item P2, unit D)
|
|
# =============================================================================
|
|
# A condition that raised half-way through apply() leaves its edits on the
|
|
# form with no result to describe them, and Save writes the whole form. So a
|
|
# run_all report with any `failed` or `review` entry keeps the box away even
|
|
# with SAVE_CORRECTIONS=1 — the box is never shown, save_claim never called.
|
|
|
|
def _gate_on(report):
|
|
orig = (cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim)
|
|
cfg.SAVE_CORRECTIONS = True
|
|
msgbox.ok_cancel = _forbidden
|
|
claim_form.save_claim = _forbidden
|
|
out = io.StringIO()
|
|
try:
|
|
with contextlib.redirect_stdout(out):
|
|
saved = flow._confirm_and_save(None, "9103000", _RESULT, report)
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim = orig
|
|
return saved, out.getvalue()
|
|
|
|
|
|
@case
|
|
def a_failed_condition_blocks_the_save_box_even_with_save_on():
|
|
saved, out = _gate_on({"failed": [{"name": "telehealth", "stage": "apply",
|
|
"error": "Timeout 30000ms"}],
|
|
"review": []})
|
|
assert saved is False, saved
|
|
assert ("[COND 9103000] Not offering Save: telehealth failed (apply): "
|
|
"Timeout 30000ms — closing with Cancel") in out, out
|
|
|
|
|
|
@case
|
|
def a_review_entry_blocks_the_save_box_even_with_save_on():
|
|
saved, out = _gate_on({"failed": [], "review": [
|
|
{"name": "vaccine_admin_codes",
|
|
"reason": "90471 could not be added cleanly — check the CPT grid "
|
|
"by hand"}]})
|
|
assert saved is False, saved
|
|
assert "Not offering Save: vaccine_admin_codes: 90471 could not be " \
|
|
"added cleanly" in out, out
|
|
|
|
|
|
@case
|
|
def a_clean_report_still_reaches_the_save_box():
|
|
"""The control: an empty report changes nothing about the old path."""
|
|
orig = (cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim)
|
|
cfg.SAVE_CORRECTIONS = True
|
|
asked = []
|
|
msgbox.ok_cancel = lambda *a, **k: asked.append(1) or False
|
|
claim_form.save_claim = _forbidden
|
|
try:
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
flow._confirm_and_save(None, "9103000", _RESULT,
|
|
{"failed": [], "review": [],
|
|
"comments": {}})
|
|
finally:
|
|
cfg.SAVE_CORRECTIONS, msgbox.ok_cancel, claim_form.save_claim = orig
|
|
assert asked == [1], asked
|
|
|
|
|
|
def _read_with(conds, save_corrections=True, open_claim=None, note=None,
|
|
ask=None, closed=None, any_open=None, save_claim=None):
|
|
"""
|
|
The real _read_claim_form and the real run_all, fed fake condition
|
|
modules; every page step stubbed and save_claim / the save box
|
|
forbidden. Returns (data, printed text).
|
|
|
|
`note`: the progress note open_progress_notes returns (OPEN_PROGRESS_NOTES
|
|
on); None = notes off. `ask`: a stand-in for the save box (default:
|
|
forbidden). `closed`: a list close_claim appends to. `any_open`: a
|
|
stand-in for claim_form.any_claim_form_open. `save_claim`: a stand-in
|
|
for claim_form.save_claim (default: forbidden).
|
|
"""
|
|
orig = (cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS,
|
|
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
msgbox.info_box, msgbox.ok_cancel, claim_form.open_claim,
|
|
claim_form.is_dental_claim_form, claim_form.open_cpt_hcpcs_tab,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.extract, claim_form.save, claim_form.close_claim,
|
|
claim_form.save_claim, flow.conditions.CONDITIONS,
|
|
claim_form.open_progress_notes, claim_form.any_claim_form_open)
|
|
cfg.SAVE_CORRECTIONS = save_corrections
|
|
cfg.REVIEW_BEFORE_CLOSE = False
|
|
cfg.APPLY_CORRECTIONS = True
|
|
cfg.OPEN_CLAIM_FORM = True
|
|
cfg.OPEN_PROGRESS_NOTES = note is not None
|
|
msgbox.info_box = lambda t, m: None
|
|
msgbox.ok_cancel = ask or _forbidden
|
|
claim_form.open_claim = open_claim or (lambda page, cid: True)
|
|
claim_form.is_dental_claim_form = lambda page: False
|
|
claim_form.wait_claim_loaded = lambda page, cid: None
|
|
claim_form.open_cpt_hcpcs_tab = lambda page: None
|
|
claim_form.extract = lambda page: {"cpt": [{"code": "99213"}],
|
|
"icd": [{"#": "1", "code": "R69"}],
|
|
"revenue": []}
|
|
claim_form.save = lambda data: pathlib.Path("unused.json")
|
|
claim_form.close_claim = ((lambda page: closed.append(True))
|
|
if closed is not None else (lambda page: None))
|
|
claim_form.save_claim = save_claim or _forbidden
|
|
claim_form.open_progress_notes = lambda page: dict(note or {})
|
|
if any_open is not None:
|
|
claim_form.any_claim_form_open = any_open
|
|
flow.conditions.CONDITIONS = conds
|
|
out = io.StringIO()
|
|
try:
|
|
with contextlib.redirect_stdout(out):
|
|
data = flow._read_claim_form(None, "9103000")
|
|
finally:
|
|
(cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE, cfg.APPLY_CORRECTIONS,
|
|
cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES,
|
|
msgbox.info_box, msgbox.ok_cancel, claim_form.open_claim,
|
|
claim_form.is_dental_claim_form, claim_form.open_cpt_hcpcs_tab,
|
|
claim_form.wait_claim_loaded,
|
|
claim_form.extract, claim_form.save, claim_form.close_claim,
|
|
claim_form.save_claim, flow.conditions.CONDITIONS,
|
|
claim_form.open_progress_notes, claim_form.any_claim_form_open) = orig
|
|
return data, out.getvalue()
|
|
|
|
|
|
def _fake(name, apply):
|
|
mod = types.ModuleType(name)
|
|
mod.NAME = name
|
|
mod.detect = lambda claim_id, data: True
|
|
mod.apply = apply
|
|
mod.describe = lambda result: f"{name} changed"
|
|
return mod
|
|
|
|
|
|
@case
|
|
def read_claim_form_never_offers_save_after_a_condition_raised_in_apply():
|
|
"""
|
|
End to end, SAVE_CORRECTIONS=1: one condition changes something, the
|
|
next raises mid-apply(). The box would list only the first while the
|
|
form also carries whatever the second did — so it is never shown.
|
|
"""
|
|
def boom(page, claim_id, data):
|
|
raise RuntimeError("fill failed\ncall log ...")
|
|
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
bad = _fake("telehealth", boom)
|
|
orig_shot = flow.conditions.base.shot
|
|
flow.conditions.base.shot = lambda page, name: None
|
|
try:
|
|
data, out = _read_with([good, bad])
|
|
finally:
|
|
flow.conditions.base.shot = orig_shot
|
|
cond = data["_conditions"]
|
|
assert cond["fired"] == ["depression_dx_pointer"], cond
|
|
assert cond["saved"] is False, cond
|
|
assert cond["failed"] == [{"name": "telehealth", "stage": "apply",
|
|
"error": "fill failed"}], cond
|
|
assert cond["review"] == [] and cond["comments"] == {}, cond
|
|
assert "Not offering Save" in out, out
|
|
|
|
|
|
@case
|
|
def read_claim_form_stores_a_declined_conditions_review_reason():
|
|
from ecw_bot.conditions.base import CPT_TAB_NOT_CONFIRMED
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
declines = _fake("dummy_codes", lambda p, c, d: ConditionResult.declined(
|
|
"dummy_codes", CPT_TAB_NOT_CONFIRMED))
|
|
data, out = _read_with([good, declines])
|
|
assert data["_conditions"]["review"] == [
|
|
{"name": "dummy_codes", "reason": CPT_TAB_NOT_CONFIRMED}], data
|
|
assert "Not offering Save" in out, out
|
|
|
|
|
|
# =============================================================================
|
|
# Claim-level review reasons block the save box too (final review, 2026-10-05)
|
|
# =============================================================================
|
|
# The gate used to read only run_all's report. A claim the Charge Summary
|
|
# itself sends to Review — a partial note, an unread note, no CPT rows —
|
|
# could still be offered for Save: a partial note can hide "Audio Only" and
|
|
# turn 93 into 95, and one OK writes that to live eCW.
|
|
|
|
_PARTIAL_NOTE = {"opened": "frame", "sections": {"TELEHEALTH": "x"},
|
|
"partial": True, "partial_sections": 1}
|
|
_READ_NOTE = {"opened": "frame", "sections": {"Procedure Codes": "x"}}
|
|
|
|
|
|
@case
|
|
def a_partial_note_blocks_the_save_box_even_with_save_on():
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
# ok_cancel and save_claim are both forbidden in _read_with.
|
|
data, out = _read_with([good], note=_PARTIAL_NOTE)
|
|
assert data["_conditions"]["fired"] == ["depression_dx_pointer"], data
|
|
assert data["_conditions"]["saved"] is False, data
|
|
assert ("[COND 9103000] Not offering Save: Progress note may be partial "
|
|
"(1 section(s))") in out, out
|
|
|
|
|
|
@case
|
|
def an_unread_note_blocks_the_save_box_even_with_save_on():
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
data, out = _read_with([good], note={"opened": "nothing"})
|
|
assert "Not offering Save: Progress note not read (nothing)" in out, out
|
|
|
|
|
|
@case
|
|
def a_fully_read_claim_still_reaches_the_save_box():
|
|
"""The control: a claim with no review reason is offered as before."""
|
|
asked = []
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
data, out = _read_with([good], note=_READ_NOTE,
|
|
ask=lambda *a, **k: asked.append(1) or False)
|
|
assert asked == [1], out
|
|
assert "Not offering Save" not in out, out
|
|
|
|
|
|
@case
|
|
def the_conditions_account_survives_a_raise_after_run_all():
|
|
"""
|
|
Final review, 2026-10-05: `_conditions` is stored right after run_all,
|
|
so a raise inside _confirm_and_save (here a describe() that meets an
|
|
unexpected change shape) no longer loses what the conditions changed.
|
|
"""
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
|
|
def bad_describe(result):
|
|
raise KeyError("after")
|
|
|
|
orig_by_name = flow.conditions.BY_NAME
|
|
flow.conditions.BY_NAME = dict(
|
|
orig_by_name,
|
|
depression_dx_pointer=types.SimpleNamespace(describe=bad_describe))
|
|
try:
|
|
data, out = _read_with([good], save_corrections=False)
|
|
finally:
|
|
flow.conditions.BY_NAME = orig_by_name
|
|
assert data["_error"] == "'after'", data
|
|
assert data["_conditions"]["fired"] == ["depression_dx_pointer"], data
|
|
from ecw_bot.core import charge_summary as cs
|
|
o = cs.claim_outcome(True, data)
|
|
assert o["Category"] == "Review", o
|
|
assert "Bot failed after the conditions ran ('after')" in o["Comments"], o
|
|
assert "could not read" not in o["Comments"], o
|
|
|
|
|
|
@case
|
|
def an_approved_save_eCW_did_not_confirm_is_review_not_not_saved():
|
|
"""
|
|
Final review, 2026-10-05: save_claim() answers False when OK WAS
|
|
clicked and the form just did not hide in time — eCW may have written
|
|
the claim. The row used to read "Corrections pending (not saved)", a
|
|
guess stated as fact; it is now Review, saying the save is unconfirmed.
|
|
"""
|
|
good = _fake("depression_dx_pointer", lambda p, c, d: _RESULT[0])
|
|
closed = []
|
|
data, out = _read_with([good], note=_READ_NOTE,
|
|
ask=lambda *a, **k: True,
|
|
save_claim=lambda page: False, closed=closed)
|
|
cond = data["_conditions"]
|
|
assert cond["saved"] is False and cond.get("save_unconfirmed") is True, cond
|
|
assert closed == [True], "the form was not Cancelled after the failed save"
|
|
from ecw_bot.core import charge_summary as cs
|
|
o = cs.claim_outcome(True, data)
|
|
assert o["Category"] == "Review", o
|
|
assert o["Current Status"] == "Needs manual review", o
|
|
assert cs.SAVE_UNCONFIRMED in o["Comments"], o
|
|
assert "not saved" not in o["Current Status"], o
|
|
|
|
|
|
# =============================================================================
|
|
# A form left on screen by a raise is still Cancelled (final review)
|
|
# =============================================================================
|
|
|
|
def _raises_with_form_open(page, cid):
|
|
raise RuntimeError("Opened the form for claim '5000001' after clicking "
|
|
"'9103000'")
|
|
|
|
|
|
@case
|
|
def a_form_open_when_open_claim_raises_is_closed_with_cancel():
|
|
"""
|
|
open_claim's UB wrong-claim check raises AFTER the form opened. That
|
|
form used to be left open (the flow's `opened` was never set), and the
|
|
next claim's click fallback would accept it as its own form.
|
|
"""
|
|
closed = []
|
|
data, _ = _read_with([], open_claim=_raises_with_form_open,
|
|
closed=closed, any_open=lambda page: True)
|
|
assert data["_error"].startswith("Opened the form for claim"), data
|
|
assert closed == [True], "the form open on screen was not Cancelled"
|
|
|
|
|
|
@case
|
|
def nothing_is_cancelled_when_open_claim_raises_with_no_form_open():
|
|
closed = []
|
|
_read_with([], open_claim=_cannot_open, closed=closed,
|
|
any_open=lambda page: False)
|
|
assert closed == [], closed
|
|
|
|
|
|
# =============================================================================
|
|
# A form that could not be read is not "not found" (review item D2, unit D)
|
|
# =============================================================================
|
|
|
|
def _cannot_open(page, cid):
|
|
raise RuntimeError("Timeout 30000ms exceeded.\nCall log:\n - waiting")
|
|
|
|
|
|
@case
|
|
def read_claim_form_returns_the_error_instead_of_an_empty_dict():
|
|
data, out = _read_with([], open_claim=_cannot_open)
|
|
assert data == {"claim_no": "9103000",
|
|
"_error": "Timeout 30000ms exceeded."}, data
|
|
assert "[FORM 9103000] Could not read the form" in out, out
|
|
|
|
|
|
@case
|
|
def process_claims_counts_an_unreadable_form_as_not_read_and_reviews_it():
|
|
"""
|
|
`_process_claims` with search and every page step stubbed: the form
|
|
raises on open. It must count as found-but-not-read, the SHOW_EACH_CLAIM
|
|
box must not run claim_form.summary() on the error dict, and the
|
|
charge summary row must be Review, not blank.
|
|
"""
|
|
from ecw_bot.core import charge_summary as cs
|
|
|
|
written, boxes = [], []
|
|
orig = (cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT,
|
|
cfg.OPEN_CLAIM_FORM, msgbox.info_box, claim_form.open_claim,
|
|
claim_form.close_claim, claim_form.summary,
|
|
flow.claim_results.search_claim, flow.claim_filters.clear_claim_no,
|
|
flow._write_charge_summary, cfg.RESULTS_DB)
|
|
cfg.SHOW_EACH_CLAIM = True
|
|
cfg.SHOW_CLAIM_IDS = False
|
|
cfg.CLAIM_PROCESS_LIMIT = 0
|
|
cfg.OPEN_CLAIM_FORM = True
|
|
msgbox.info_box = lambda t, m: boxes.append((t, m))
|
|
claim_form.open_claim = _cannot_open
|
|
claim_form.close_claim = lambda page: None
|
|
claim_form.summary = _forbidden
|
|
flow.claim_results.search_claim = lambda page, cid: {
|
|
"id": cid, "count": 1, "found": True, "seconds": 1.0}
|
|
flow.claim_filters.clear_claim_no = lambda page: None
|
|
flow._write_charge_summary = lambda info, outcomes: written.append(outcomes)
|
|
# Never the real results database, whatever this machine's .env says.
|
|
cfg.RESULTS_DB = False
|
|
out = io.StringIO()
|
|
try:
|
|
with contextlib.redirect_stdout(out):
|
|
flow._process_claims(None, {"ids": ["9103000"], "file": None})
|
|
finally:
|
|
(cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT,
|
|
cfg.OPEN_CLAIM_FORM, msgbox.info_box, claim_form.open_claim,
|
|
claim_form.close_claim, claim_form.summary,
|
|
flow.claim_results.search_claim, flow.claim_filters.clear_claim_no,
|
|
flow._write_charge_summary, cfg.RESULTS_DB) = orig
|
|
|
|
text = out.getvalue()
|
|
assert ("1 found, 0 not found, 0 form(s) read, 1 could not be read"
|
|
in text), text
|
|
assert len(boxes) == 1 and \
|
|
"Could not read this claim: Timeout 30000ms exceeded." in boxes[0][1], \
|
|
boxes
|
|
outcome = written[0]["9103000"]
|
|
assert outcome == {
|
|
"Comments": "Bot could not read the claim (Timeout 30000ms "
|
|
"exceeded.)",
|
|
"Current Status": "Needs manual review",
|
|
"Category": "Review"}, outcome
|
|
assert outcome != cs.claim_outcome(True, {}), "must not be blank"
|
|
|
|
|
|
def main() -> int:
|
|
failures = 0
|
|
for fn in CASES:
|
|
name = fn.__name__.replace("_", " ")
|
|
try:
|
|
fn()
|
|
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
|
|
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|