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,140 @@
|
||||
r"""
|
||||
The Charge Summary grows claim by claim (operator, 2026-10-12): each claim's
|
||||
row goes into the Excel summary and the results database as soon as that
|
||||
claim is done, not once at the end of the run.
|
||||
|
||||
python tests\test_summary_per_claim.py
|
||||
|
||||
Drives the real `_process_claims` with the search, the form read and both
|
||||
writers stubbed — no browser, no file, no database.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import pathlib
|
||||
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 charge_summary as cs # noqa: E402
|
||||
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def _ub(fired=()):
|
||||
return {"cpt": [{"code": "99213", "checked": True, "fee": "100.00"}],
|
||||
"icd": [{"code": "Z00.00"}],
|
||||
"progress_notes": {"opened": "modal", "text": "Office visit."},
|
||||
"_conditions": {"fired": list(fired), "saved": False,
|
||||
"failed": [], "review": [], "comments": {}}}
|
||||
|
||||
|
||||
def _run(ids, found=None, forms=None):
|
||||
"""
|
||||
`_process_claims` over `ids`. `found` = ids the search finds (default
|
||||
all), `forms` = claim id -> what the form read returns. Returns the
|
||||
event list: ("search", id), ("excel", sorted outcome ids),
|
||||
("db", outcomes, details).
|
||||
"""
|
||||
found = set(ids if found is None else found)
|
||||
forms = forms or {}
|
||||
events = []
|
||||
|
||||
def search(page, cid):
|
||||
events.append(("search", cid))
|
||||
return {"id": cid, "count": 1 if cid in found else 0,
|
||||
"found": cid in found, "seconds": 1.0}
|
||||
|
||||
orig = (cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT,
|
||||
flow.claim_results.search_claim, flow._read_claim_form,
|
||||
flow.claim_filters.clear_claim_no, flow._write_charge_summary,
|
||||
flow._save_results_db)
|
||||
cfg.SHOW_EACH_CLAIM = False
|
||||
cfg.SHOW_CLAIM_IDS = False
|
||||
cfg.CLAIM_PROCESS_LIMIT = 0
|
||||
flow.claim_results.search_claim = search
|
||||
flow._read_claim_form = lambda page, cid, head: dict(
|
||||
forms.get(cid) or _ub(), claim_no=cid)
|
||||
flow.claim_filters.clear_claim_no = lambda page: None
|
||||
flow._write_charge_summary = lambda info, outcomes: events.append(
|
||||
("excel", sorted(outcomes)))
|
||||
flow._save_results_db = lambda info, outcomes, details: events.append(
|
||||
("db", dict(outcomes), dict(details)))
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
flow._process_claims(None, {"ids": list(ids), "file": None})
|
||||
finally:
|
||||
(cfg.SHOW_EACH_CLAIM, cfg.SHOW_CLAIM_IDS, cfg.CLAIM_PROCESS_LIMIT,
|
||||
flow.claim_results.search_claim, flow._read_claim_form,
|
||||
flow.claim_filters.clear_claim_no, flow._write_charge_summary,
|
||||
flow._save_results_db) = orig
|
||||
return events
|
||||
|
||||
|
||||
@case
|
||||
def each_claim_is_written_to_the_excel_summary_before_the_next_starts():
|
||||
events = _run(["111", "222"])
|
||||
order = [e[:2] if e[0] != "db" else ("db",) for e in events]
|
||||
assert order == [("search", "111"), ("excel", ["111"]), ("db",),
|
||||
("search", "222"), ("excel", ["111", "222"]), ("db",)], \
|
||||
order
|
||||
|
||||
|
||||
@case
|
||||
def each_claim_saves_only_its_own_row_to_the_database():
|
||||
events = _run(["111", "222"],
|
||||
forms={"222": _ub(["depression_dx_pointer"])})
|
||||
db = [e for e in events if e[0] == "db"]
|
||||
assert [sorted(e[1]) for e in db] == [["111"], ["222"]], db
|
||||
assert db[0][1]["111"]["Comments"] == cs.CLEAN, db[0]
|
||||
assert db[1][1]["222"]["Category"] == "Fail", db[1]
|
||||
assert "ICD pointer issue" in db[1][1]["222"]["Comments"], db[1]
|
||||
assert sorted(db[1][2]) == ["222"], db[1]
|
||||
# The details are what the conditions reported, never the claim's own
|
||||
# fields (review, 2026-10-12).
|
||||
detail = db[1][2]["222"]
|
||||
assert detail["fired"] == ["depression_dx_pointer"], detail
|
||||
assert "cpt" not in detail and "progress_notes" not in detail, detail
|
||||
|
||||
|
||||
@case
|
||||
def a_claim_not_found_updates_the_excel_but_saves_nothing_to_the_database():
|
||||
events = _run(["111", "222"], found=["222"])
|
||||
assert ("excel", ["111"]) in events, events
|
||||
db = [e for e in events if e[0] == "db"]
|
||||
assert [sorted(e[1]) for e in db] == [["222"]], db
|
||||
|
||||
|
||||
@case
|
||||
def nothing_is_written_again_after_the_last_claim():
|
||||
events = _run(["111"])
|
||||
assert [e[0] for e in events] == ["search", "excel", "db"], events
|
||||
|
||||
|
||||
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