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,341 @@
|
||||
r"""
|
||||
export_medcor_ready - the [MODE] banner and the LIVE-WRITE start box.
|
||||
|
||||
python tests\test_mode_banner.py
|
||||
|
||||
Review item P1 (2026-10-01): nothing at the start of a run said whether it
|
||||
could write to live eCW. The only way to tell VERIFY from LIVE-WRITE was to
|
||||
open .env, and the .env reader itself could get it wrong silently. So:
|
||||
|
||||
* `mode_banner()` prints, after the [CONFIG] lines, which mode this is, the
|
||||
claim limit, and whether the ids come from a test sheet.
|
||||
* With SAVE_CORRECTIONS=1, `main()` asks for an OK BEFORE login, with
|
||||
Cancel as the default button. Anything but an explicit OK stops the run
|
||||
with exit 2 - nothing spent: no login attempt, no 2FA mail.
|
||||
|
||||
`main()` is driven with everything past the config check stubbed: the run
|
||||
log, the browser and the login all raise if reached, so these cases prove
|
||||
where `main()` stops, never touching eCW, the Logs\ folder or the real .env.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import pathlib
|
||||
import tempfile
|
||||
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.flows import export_medcor_ready as flow # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
_CFG_KEYS = ("SAVE_CORRECTIONS", "APPLY_CORRECTIONS", "CLAIM_PROCESS_LIMIT",
|
||||
"TEST_CLAIM_IDS_FILE", "CLAIM_STATUS_LABEL", "ECW_USERNAME",
|
||||
"PROXY_IP", "PROXY_PORT", "HEADLESS", "validate",
|
||||
"resolved_otp_mode", "WRITE_CHARGE_SUMMARY", "BROWSER_CDP_URL")
|
||||
|
||||
|
||||
def _with_cfg(fn, **values):
|
||||
"""Run fn() with cfg attributes set, restoring every one after."""
|
||||
saved = {k: getattr(cfg, k) for k in _CFG_KEYS}
|
||||
saved_bad = list(cfg._BAD_VALUES)
|
||||
try:
|
||||
for k, v in values.items():
|
||||
assert k in _CFG_KEYS, k
|
||||
setattr(cfg, k, v)
|
||||
return fn()
|
||||
finally:
|
||||
for k, v in saved.items():
|
||||
setattr(cfg, k, v)
|
||||
cfg._BAD_VALUES[:] = saved_bad
|
||||
|
||||
|
||||
_VERIFY = dict(SAVE_CORRECTIONS=False, APPLY_CORRECTIONS=True,
|
||||
CLAIM_PROCESS_LIMIT=1, TEST_CLAIM_IDS_FILE="",
|
||||
CLAIM_STATUS_LABEL="Medcor Ready")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# mode_banner()
|
||||
# =============================================================================
|
||||
|
||||
@case
|
||||
def verify_mode_banner():
|
||||
lines = _with_cfg(flow.mode_banner, **_VERIFY)
|
||||
assert lines == [
|
||||
"[MODE] VERIFY — corrections are filled in on screen for review "
|
||||
"and NEVER saved (SAVE_CORRECTIONS=0)",
|
||||
"[MODE] CLAIM_PROCESS_LIMIT=1 (0 = all)",
|
||||
], lines
|
||||
|
||||
|
||||
@case
|
||||
def live_write_mode_banner():
|
||||
lines = _with_cfg(flow.mode_banner,
|
||||
**{**_VERIFY, "SAVE_CORRECTIONS": True})
|
||||
assert lines[0] == ("[MODE] LIVE-WRITE — corrections are SAVED to eCW "
|
||||
"after the per-claim OK; box ticks and added CPT "
|
||||
"rows save at once, OK or not "
|
||||
"(SAVE_CORRECTIONS=1)"), lines
|
||||
assert not any("VERIFY" in l for l in lines), lines
|
||||
|
||||
|
||||
@case
|
||||
def apply_off_says_conditions_are_not_run():
|
||||
lines = _with_cfg(flow.mode_banner,
|
||||
**{**_VERIFY, "APPLY_CORRECTIONS": False})
|
||||
assert "[MODE] conditions are NOT run (APPLY_CORRECTIONS=0)" in lines, \
|
||||
lines
|
||||
|
||||
|
||||
@case
|
||||
def apply_on_says_nothing_about_conditions():
|
||||
lines = _with_cfg(flow.mode_banner, **_VERIFY)
|
||||
assert not any("APPLY_CORRECTIONS" in l for l in lines), lines
|
||||
|
||||
|
||||
@case
|
||||
def claim_limit_zero_is_shown_as_all():
|
||||
lines = _with_cfg(flow.mode_banner,
|
||||
**{**_VERIFY, "CLAIM_PROCESS_LIMIT": 0})
|
||||
assert "[MODE] CLAIM_PROCESS_LIMIT=0 (0 = all)" in lines, lines
|
||||
|
||||
|
||||
@case
|
||||
def claim_ids_from_a_test_sheet_are_named_by_file_name():
|
||||
sheet = str(pathlib.Path("C:/somewhere/Standard Claims.xlsx"))
|
||||
lines = _with_cfg(flow.mode_banner,
|
||||
**{**_VERIFY, "TEST_CLAIM_IDS_FILE": sheet})
|
||||
assert lines[-1] == ("[MODE] TEST MODE — claim ids come from Standard "
|
||||
"Claims.xlsx, not the live Medcor Ready queue"), lines
|
||||
|
||||
|
||||
@case
|
||||
def no_test_mode_line_without_a_test_file():
|
||||
lines = _with_cfg(flow.mode_banner, **_VERIFY)
|
||||
assert not any("TEST MODE" in l for l in lines), lines
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# main() - where it stops
|
||||
# =============================================================================
|
||||
|
||||
class _ReachedLogin(Exception):
|
||||
"""The browser/login was reached."""
|
||||
|
||||
|
||||
class _BoxShown(Exception):
|
||||
"""A box that must not appear in this mode appeared."""
|
||||
|
||||
|
||||
def _reached_login(*a, **k):
|
||||
raise _ReachedLogin()
|
||||
|
||||
|
||||
def _run_main(save, box_answer=None, validate=None):
|
||||
"""
|
||||
main() with the config check passed (unless `validate` is given), the
|
||||
browser and login stubbed to raise _ReachedLogin, and the run log not
|
||||
started. `box_answer`: what the LIVE-WRITE box answers, or None to make
|
||||
any box an error. Returns (exit code or _ReachedLogin, printed text,
|
||||
the boxes shown as (title, body, kwargs)).
|
||||
"""
|
||||
boxes = []
|
||||
|
||||
def _box(title, msg, **kw):
|
||||
boxes.append((title, msg, kw))
|
||||
if box_answer is None:
|
||||
raise _BoxShown(title)
|
||||
return box_answer
|
||||
|
||||
orig = (flow.runlog.start, flow.sync_playwright, flow._login_with_retry,
|
||||
msgbox.ok_cancel, flow.run_lock.acquire, flow.run_lock.finish)
|
||||
# In a temp folder: main() writes the Run's events beside the log.
|
||||
unused_log = pathlib.Path(tempfile.mkdtemp()) / "unused.log"
|
||||
flow.runlog.start = lambda: unused_log
|
||||
flow.sync_playwright = _reached_login
|
||||
flow._login_with_retry = _reached_login
|
||||
msgbox.ok_cancel = _box
|
||||
flow.run_lock.acquire = lambda log_path, source=None: {}
|
||||
flow.run_lock.finish = lambda code: None
|
||||
out = io.StringIO()
|
||||
|
||||
def _go():
|
||||
with contextlib.redirect_stdout(out):
|
||||
try:
|
||||
return flow.main()
|
||||
except _ReachedLogin as exc:
|
||||
return exc
|
||||
|
||||
values = {**_VERIFY, "SAVE_CORRECTIONS": save,
|
||||
"ECW_USERNAME": "test-user", "PROXY_IP": "", "PROXY_PORT": "",
|
||||
# Never the operator's Chrome: with BROWSER_CDP_URL from .env
|
||||
# main() would open a real one, here with no proxy — and a
|
||||
# Portal run then attached to it (2026-10-22).
|
||||
"BROWSER_CDP_URL": "",
|
||||
"HEADLESS": False, "resolved_otp_mode": lambda: "manual",
|
||||
"validate": validate or (lambda: [])}
|
||||
try:
|
||||
result = _with_cfg(_go, **values)
|
||||
finally:
|
||||
(flow.runlog.start, flow.sync_playwright, flow._login_with_retry,
|
||||
msgbox.ok_cancel, flow.run_lock.acquire, flow.run_lock.finish) = orig
|
||||
return result, out.getvalue(), boxes
|
||||
|
||||
|
||||
@case
|
||||
def live_write_not_confirmed_stops_before_login_with_exit_2():
|
||||
result, out, boxes = _run_main(save=True, box_answer=False)
|
||||
assert result == 2, (result, out)
|
||||
assert ("[MODE] Operator did not confirm LIVE-WRITE mode — stopping "
|
||||
"before login") in out, out
|
||||
assert len(boxes) == 1, boxes
|
||||
title, body, kw = boxes[0]
|
||||
assert title == "eCW Bot — LIVE-WRITE mode", title
|
||||
# Enter must answer Cancel here too: OK starts a run that writes.
|
||||
assert kw.get("default_cancel") is True, kw
|
||||
assert "live eCW" in body, body
|
||||
# The banner is printed first, so the log says why the box appeared.
|
||||
assert out.index("[MODE] LIVE-WRITE") < out.index("did not confirm"), out
|
||||
|
||||
|
||||
@case
|
||||
def live_write_confirmed_goes_on_to_login():
|
||||
result, out, boxes = _run_main(save=True, box_answer=True)
|
||||
assert isinstance(result, _ReachedLogin), (result, out)
|
||||
assert len(boxes) == 1, boxes
|
||||
assert "did not confirm" not in out, out
|
||||
|
||||
|
||||
@case
|
||||
def verify_mode_asks_nothing_and_goes_on_to_login():
|
||||
result, out, boxes = _run_main(save=False, box_answer=None)
|
||||
assert isinstance(result, _ReachedLogin), (result, out)
|
||||
assert boxes == [], boxes
|
||||
assert "[MODE] VERIFY" in out, out
|
||||
|
||||
|
||||
@case
|
||||
def mode_banner_follows_the_config_lines():
|
||||
_, out, _ = _run_main(save=False)
|
||||
assert out.index("[CONFIG] log=") < out.index("[MODE] VERIFY"), out
|
||||
|
||||
|
||||
@case
|
||||
def unreadable_setting_exits_2_before_banner_and_login():
|
||||
"""
|
||||
P1's strict parsing, end to end: a bad yes/no value recorded by the
|
||||
settings parser is a validate() problem, so main() stops at the config
|
||||
check - before the banner, the box and the login.
|
||||
"""
|
||||
def _validate():
|
||||
cfg._BAD_VALUES.append(("SAVE_CORRECTIONS", "O",
|
||||
"a valid yes/no value"))
|
||||
return orig_validate()
|
||||
|
||||
orig_validate = cfg.validate
|
||||
result, out, boxes = _run_main(save=True, box_answer=None,
|
||||
validate=_validate)
|
||||
assert result == 2, (result, out)
|
||||
assert "SAVE_CORRECTIONS='O' is not a valid yes/no value" in out, out
|
||||
assert "[MODE]" not in out, out
|
||||
assert boxes == [], boxes
|
||||
|
||||
|
||||
# ── tools/check_claims_from_excel.py (final review, 2026-10-05) ──────────────
|
||||
# A second entry point that logs in and reaches the flow's real
|
||||
# _process_claims (so it can save). It only printed a WARNING line and went
|
||||
# straight to login with SAVE_CORRECTIONS=1; it now goes through the same
|
||||
# start_mode_gate() as main().
|
||||
|
||||
def _tool():
|
||||
import importlib.util
|
||||
path = ROOT / "tools" / "check_claims_from_excel.py"
|
||||
spec = importlib.util.spec_from_file_location("_check_tool", path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def _run_tool(save, box_answer=None):
|
||||
tool = _tool()
|
||||
boxes = []
|
||||
|
||||
def _box(title, msg, **kw):
|
||||
boxes.append((title, msg, kw))
|
||||
if box_answer is None:
|
||||
raise _BoxShown(title)
|
||||
return box_answer
|
||||
|
||||
orig = (tool.runlog.start, msgbox.ok_cancel)
|
||||
tool.runlog.start = lambda: pathlib.Path("unused.log")
|
||||
tool.sync_playwright = _reached_login
|
||||
tool.login = _reached_login
|
||||
tool.load_claims_from_excel = lambda path: [("9103000", "")]
|
||||
msgbox.ok_cancel = _box
|
||||
out = io.StringIO()
|
||||
|
||||
def _go():
|
||||
with contextlib.redirect_stdout(out):
|
||||
try:
|
||||
return tool.main(pathlib.Path("unused.xlsx"))
|
||||
except _ReachedLogin as exc:
|
||||
return exc
|
||||
|
||||
values = {**_VERIFY, "SAVE_CORRECTIONS": save,
|
||||
"ECW_USERNAME": "test-user", "WRITE_CHARGE_SUMMARY": True,
|
||||
"validate": lambda: []}
|
||||
try:
|
||||
result = _with_cfg(_go, **values)
|
||||
finally:
|
||||
tool.runlog.start, msgbox.ok_cancel = orig
|
||||
return result, out.getvalue(), boxes
|
||||
|
||||
|
||||
@case
|
||||
def the_check_tool_stops_before_login_when_live_write_is_not_confirmed():
|
||||
result, out, boxes = _run_tool(save=True, box_answer=False)
|
||||
assert result == 2, (result, out)
|
||||
assert "[MODE] LIVE-WRITE" in out, out
|
||||
assert "did not confirm LIVE-WRITE" in out, out
|
||||
assert len(boxes) == 1 and boxes[0][2].get("default_cancel") is True, \
|
||||
boxes
|
||||
|
||||
|
||||
@case
|
||||
def the_check_tool_in_verify_mode_asks_nothing_and_goes_on_to_login():
|
||||
result, out, boxes = _run_tool(save=False)
|
||||
assert isinstance(result, _ReachedLogin), (result, out)
|
||||
assert boxes == [], boxes
|
||||
assert "[MODE] VERIFY" in out, out
|
||||
|
||||
|
||||
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())
|
||||
Reference in new issue
Block a user