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:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
r"""
Stage 3 (EEWA-33, spec 2026-10-22): the Dashboard's banner for a Run that
ended badly, and Started By in `dbo.CV_ChargeSummary`.
python tests\test_run_alerts.py
* **Banner**: the latest Run ended in a Run Error, or without a record
(killed) → a red banner naming when, who, the step and a link to the
Run. Done, Stopped and Running → no banner.
* **started_by** is written only once its column exists — a table not yet
updated must never cost a Run its rows — and the Portal shows it in a
claim's side panel; a Portal reading a table without it says how to fix
that.
Claim numbers here are made up.
"""
import io
import os
import sys
import json
import pathlib
import tempfile
import contextlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT / "tests"))
from ecw_bot import settings as cfg # noqa: E402
from ecw_bot.core import results_db as rdb # noqa: E402
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
from web import runs as run_view # noqa: E402
from web.repo import SqlResultsRepo # noqa: E402
from test_web_app import (FakeRepo, FakeRunner, signed_in, _row, # noqa: E402
_quiet, DAY)
from test_results_db import _row as _sheet_row, _sheet # noqa: E402
CASES = []
class DbError(Exception):
"""A pyodbc error: SQLSTATE first, as the driver gives it."""
def case(fn):
CASES.append(fn)
return fn
def ev(t, kind, **f):
return {"t": f"2026-10-22T{t}.000", "kind": kind, **f}
START = ev("14:00:00", "run_start", source="portal", started_by="olu",
mode="VERIFY", claim_limit=2, test_sheet="", pid=1, log="x")
def _logs(*runs):
d = pathlib.Path(tempfile.mkdtemp())
for run_id, events in runs:
(d / f"events_{run_id}.jsonl").write_text(
"".join(json.dumps(e) + "\n" for e in events), encoding="utf-8")
return d
ERRORED = [START, ev("14:00:05", "queue", claims=["9001", "9002"]),
ev("14:00:06", "claim_start", n=1, total=2, claim="9001"),
ev("14:01:00", "run_error", step="claims", exit_code=5,
message="Target closed", traceback=""),
ev("14:01:01", "run_end", exit_code=5)]
DONE = [{**START, "t": "2026-10-22T15:00:00.000"},
{"t": "2026-10-22T15:01:00.000", "kind": "run_end", "exit_code": 0}]
# ── Banner ────────────────────────────────────────────────────────────────────
def _dashboard(logs, runner=None):
client, _ = signed_in(FakeRepo(), runner or FakeRunner(), logs_dir=logs)
return client.get("/").text
@case
def a_run_error_last_shows_the_banner_with_who_what_and_a_link():
page = _dashboard(_logs(("20261022_140000", ERRORED)))
assert 'id="last-run-alert"' in page, page[:500]
assert "ended in a Run Error" in page and "started by olu" in page
assert "claims (exit 5): Target closed" in page
assert "1 claim(s) not reached" in page
assert 'href="/runs/20261022_140000"' in page
@case
def a_finished_run_after_the_error_clears_the_banner():
page = _dashboard(_logs(("20261022_140000", ERRORED),
("20261022_150000", DONE)))
assert 'id="last-run-alert"' not in page
@case
def a_killed_run_shows_the_banner_a_stopped_or_running_one_does_not():
killed = ERRORED[:3]
page = _dashboard(_logs(("20261022_140000", killed)))
assert "ended without a record" in page
stopped = killed + [ev("14:02:00", "stopped", stopped_by="asha")]
assert 'id="last-run-alert"' not in _dashboard(
_logs(("20261022_140000", stopped)))
runner = FakeRunner(running=True)
st = runner._st
runner._st = lambda: {**st(),
"log_path": r"D:\x\Logs\run_20261022_140000.log"}
assert 'id="last-run-alert"' not in _dashboard(
_logs(("20261022_140000", killed)), runner)
@case
def no_runs_no_banner_and_the_latest_is_read_alone():
assert 'id="last-run-alert"' not in _dashboard(_logs())
d = _logs(("20261022_140000", ERRORED), ("20261022_150000", DONE))
assert run_view.latest_run(d)["run_id"] == "20261022_150000"
assert run_view.latest_run(_logs()) is None
# ── started_by in the results table ───────────────────────────────────────────
class ColCursor:
def __init__(self, log, has):
self.log, self.has = log, has
def execute(self, sql, params):
self.log.append(("execute", sql, list(params)))
if self.has == "raise":
raise RuntimeError("no permission")
return self
def fetchone(self):
return (100,) if self.has else (None,)
def executemany(self, sql, params):
self.log.append(("executemany", sql, list(params)))
class ColConn:
def __init__(self, has):
self.log, self.has = [], has
def cursor(self):
return ColCursor(self.log, self.has)
def commit(self):
self.log.append(("commit",))
def close(self):
self.log.append(("close",))
def _records():
return [rdb.to_record(dict(zip(_sheet(_sheet_row())[0],
_sheet(_sheet_row())[1])),
run_at=None, run_mode="VERIFY", details=None)]
def _insert(conn):
return next(e for e in conn.log if e[0] == "executemany")
@case
def started_by_is_written_when_the_column_exists():
conn = ColConn(has=True)
assert rdb.insert_rows(conn, _records(), started_by="olu") == 1
_, sql, params = _insert(conn)
assert sql.split("(")[1].split(")")[0].endswith("details_json, "
"started_by"), sql
assert params[0][-1] == "olu" and len(params[0]) == len(rdb.DB_COLUMNS) + 1
check = conn.log[0]
assert check[1] == "SELECT COL_LENGTH(?, ?)", check
assert check[2] == [rdb.TABLE, "started_by"], "parameterized"
@case
def a_table_without_the_column_still_gets_its_rows():
for has in (False, "raise"):
conn = ColConn(has=has)
assert rdb.insert_rows(conn, _records(), started_by="olu") == 1
_, sql, params = _insert(conn)
assert "started_by" not in sql and len(params[0]) == \
len(rdb.DB_COLUMNS), has
@case
def no_started_by_no_check():
conn = ColConn(has=True)
rdb.insert_rows(conn, _records())
assert [e[0] for e in conn.log] == ["executemany", "commit"], conn.log
@case
def the_flow_saves_started_by_with_the_rows():
seen = {}
saved = (rdb.save_summary, cfg.RESULTS_DB, flow.charge_summary.build)
os.environ["ECW_STARTED_BY"] = "olu"
rdb.save_summary = lambda *a, **k: seen.update(k)
flow.charge_summary.build = lambda *a: [["CLAIMS#"], ["9001"]]
cfg.RESULTS_DB = True
try:
info = {"ids": ["9001"], "file": None}
with contextlib.redirect_stdout(io.StringIO()):
flow._save_results_db(info, {"9001": {"Category": "Pass"}},
{"9001": {}})
finally:
rdb.save_summary, cfg.RESULTS_DB, flow.charge_summary.build = saved
del os.environ["ECW_STARTED_BY"]
assert seen.get("started_by") == "olu", seen
@case
def the_portal_reads_and_shows_started_by():
class Cur:
def __init__(self, log):
self.log, self.description = log, [("claim_no",)]
def execute(self, sql, params):
self.log.append(sql)
return self
def fetchall(self):
return []
class Conn:
def __init__(self):
self.log = []
def cursor(self):
return Cur(self.log)
def close(self):
pass
conn = Conn()
SqlResultsRepo("DSN=x", connect=lambda cs, timeout: conn).day_results(DAY)
assert "started_by" in conn.log[0], conn.log[0]
row = _row("9000001", "Pass", 1)
row["started_by"] = "olu"
client, _ = signed_in(FakeRepo(rows=[row]), FakeRunner())
page = client.get(f"/day/{DAY.isoformat()}").text
assert '["Started By", "olu"]' in page, "a field of the side panel"
@case
def a_table_without_the_column_says_run_the_script_again():
client, _ = signed_in(FakeRepo(fail=DbError("42S22", "Invalid column "
"name 'started_by'")),
FakeRunner())
r = _quiet(client.get, "/days")
assert r.status_code == 503 and "needs an update" in r.text, r.text
assert "create_tables.sql" in r.text
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())