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,865 @@
|
||||
r"""
|
||||
core/charge_summary.py — the 24-column Charge Summary sheet written at the
|
||||
end of every claims run.
|
||||
|
||||
python tests\test_charge_summary.py
|
||||
|
||||
No browser, no eCW: a fictional report is written to a temp folder.
|
||||
|
||||
Review items D1 and D10 (2026-10-05): a TEST_CLAIM_IDS_FILE run writes
|
||||
Charge Summary_TEST_<date>.xlsx, and a same-day file held open (as Excel
|
||||
holds it) no longer costs the run its sheet — it lands under a `__HHMMSS`
|
||||
name instead.
|
||||
|
||||
Review items D2/St1 (unit D, 2026-10-05): a third Category, "Review" /
|
||||
"Needs manual review", for every claim the bot did not really check. The
|
||||
Pass/Fail cases below now pass a fully-read UB claim (`_ub()`: CPT rows, a
|
||||
progress note that was read, conditions that ran) — before, a bare
|
||||
`{"_conditions": {"fired": []}}` was enough for Pass, and that bare dict
|
||||
(no note, no rows) is exactly what now reads Review.
|
||||
"""
|
||||
|
||||
import io
|
||||
import re
|
||||
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 charge_summary as cs, spreadsheet # noqa: E402
|
||||
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
|
||||
|
||||
CASES = []
|
||||
|
||||
REPORT_HEADER = ["", "CLAIMTYPE", "CLAIMS#", "SERVICE DATE", "PVDR", "PATIENT",
|
||||
"PAYER", "STATUS", "CHARGES", "PMTS/ADJS", "Adjustment",
|
||||
"WithHeld", "Balance", "Visit Type", "Account Number",
|
||||
"Provider Name", "Guarantor Name"]
|
||||
|
||||
|
||||
def _row(claim, payer="Medicaid", pmts="35.00", adj="5.00", ctype="U"):
|
||||
return ["", ctype, claim, "09/01/2026", "AB", "Test, Patient", payer,
|
||||
"Medcor Ready", "275.00", pmts, adj, "0.00", "240.00",
|
||||
"Acute Care Walk In", "123456", "Doe, Jane", "Test, Patient"]
|
||||
|
||||
|
||||
REPORT = [REPORT_HEADER, _row("1000001"), _row("1000002", payer="DentaQuest",
|
||||
ctype="D"), _row("1000001"), _row("1000003", payer="Unmapped Payer X")]
|
||||
MAPPING = {"Medicaid": "Medicaid", "DentaQuest": "Dentaquest"}
|
||||
|
||||
|
||||
def case(fn):
|
||||
CASES.append(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def _build(outcomes=None):
|
||||
return cs.build_rows(REPORT, 0, 2, outcomes or {}, "10/01/2026",
|
||||
"eCW Bot", MAPPING)
|
||||
|
||||
|
||||
def _col(name):
|
||||
return cs.HEADERS.index(name)
|
||||
|
||||
|
||||
def _ub(fired=(), saved=False, **extra):
|
||||
"""A UB/HCFA claim the bot fully read: CPT rows, a progress note it
|
||||
actually read, and the conditions run with `fired` changing something.
|
||||
`extra` overrides or adds keys (`_conditions` included)."""
|
||||
data = {"claim_no": "1",
|
||||
"cpt": [{"code": "99213", "checked": True, "fee": "100.00"}],
|
||||
"progress_notes": {"opened": "modal", "text": "Office visit."},
|
||||
"_conditions": {"fired": list(fired), "saved": saved,
|
||||
"failed": [], "review": [], "comments": {}}}
|
||||
data.update(extra)
|
||||
return data
|
||||
|
||||
|
||||
# ── claim_outcome ────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def not_found_or_not_read_leaves_the_tracking_columns_blank():
|
||||
assert cs.claim_outcome(False, {}) == {}
|
||||
assert cs.claim_outcome(True, {}) == {}
|
||||
|
||||
|
||||
@case
|
||||
def clean_comment_is_the_teams_exact_wording():
|
||||
# Operator, 2026-10-06 — copied from the team's sheet, character for
|
||||
# character ("Pending with Errors", not the earlier "Medcor Ready").
|
||||
assert cs.CLEAN == ("Clean claims (claims that have no errors, placed "
|
||||
"in Pending with Errors)"), cs.CLEAN
|
||||
|
||||
|
||||
@case
|
||||
def nothing_fired_is_a_clean_pass():
|
||||
o = cs.claim_outcome(True, _ub())
|
||||
assert o == {"Comments": cs.CLEAN, "Current Status": "Ready to Submit",
|
||||
"Category": "Pass"}, o
|
||||
|
||||
|
||||
@case
|
||||
def a_fired_condition_is_a_fail_with_its_comment():
|
||||
o = cs.claim_outcome(True, _ub(["depression_dx_pointer", "dummy_codes"]))
|
||||
assert o["Category"] == "Fail", o
|
||||
assert "ICD pointer issue" in o["Comments"], o
|
||||
assert "DCONS/Dscprit" in o["Comments"], o
|
||||
assert o["Current Status"] == "Corrections pending (not saved)", o
|
||||
|
||||
|
||||
@case
|
||||
def a_saved_correction_reads_ready_to_submit():
|
||||
o = cs.claim_outcome(True, _ub(["telehealth"], saved=True))
|
||||
assert o["Current Status"] == "Ready to Submit", o
|
||||
|
||||
|
||||
@case
|
||||
def dental_high_risk_and_expired_sliding_fee_are_findings():
|
||||
o = cs.claim_outcome(True, {
|
||||
"high_risk": {"high_risk": True},
|
||||
"sliding_fee": {"verdict": "expired", "reason": "DOS after range"}})
|
||||
assert o["Category"] == "Fail", o
|
||||
assert cs.HIGH_RISK in o["Comments"], o
|
||||
assert "Sliding fee EXPIRED" in o["Comments"], o
|
||||
|
||||
|
||||
@case
|
||||
def dental_clean_with_active_sliding_fee_is_a_pass():
|
||||
o = cs.claim_outcome(True, {"high_risk": {"high_risk": False},
|
||||
"sliding_fee": {"verdict": "active"}})
|
||||
assert o["Category"] == "Pass" and o["Comments"] == cs.CLEAN, o
|
||||
|
||||
|
||||
@case
|
||||
def dental_sliding_fee_none_is_noted_but_still_a_pass():
|
||||
o = cs.claim_outcome(True, {"high_risk": {"high_risk": False},
|
||||
"sliding_fee": {"verdict": "none",
|
||||
"reason": "no dates"}})
|
||||
assert o["Category"] == "Pass", o
|
||||
assert "Sliding fee NONE" in o["Comments"], o
|
||||
|
||||
|
||||
# ── claim_outcome: Review (review items D2/St1, unit D) ─────────────────────
|
||||
|
||||
REVIEW = {"Current Status": "Needs manual review", "Category": "Review"}
|
||||
|
||||
|
||||
def _review(data):
|
||||
o = cs.claim_outcome(True, data)
|
||||
assert {k: o.get(k) for k in REVIEW} == REVIEW, o
|
||||
return o["Comments"]
|
||||
|
||||
|
||||
@case
|
||||
def a_form_that_could_not_be_read_is_review_not_blank():
|
||||
c = _review({"claim_no": "1", "_error": "Timeout 30000ms exceeded."})
|
||||
assert c == "Bot could not read the claim (Timeout 30000ms exceeded.)", c
|
||||
|
||||
|
||||
@case
|
||||
def a_form_that_never_finished_loading_is_review_with_its_reason():
|
||||
c = _review(_ub(_not_evaluated="HCFA form: no CPT row after 20s",
|
||||
_conditions=None, cpt=[]))
|
||||
assert c.startswith("HCFA form: no CPT row after 20s"), c
|
||||
# The flow skipped the conditions ON PURPOSE — not APPLY_CORRECTIONS=0.
|
||||
assert "APPLY_CORRECTIONS" not in c, c
|
||||
|
||||
|
||||
@case
|
||||
def apply_corrections_off_is_review():
|
||||
data = _ub()
|
||||
del data["_conditions"]
|
||||
assert _review(data) == "Conditions not run (APPLY_CORRECTIONS=0)"
|
||||
|
||||
|
||||
@case
|
||||
def a_progress_note_never_read_is_review_in_every_state():
|
||||
for note, state in ((None, "not opened"), ({"opened": "nothing"}, "nothing"),
|
||||
({"opened": "absent"}, "absent"),
|
||||
({"opened": "disabled"}, "disabled")):
|
||||
data = _ub()
|
||||
if note is None:
|
||||
del data["progress_notes"]
|
||||
else:
|
||||
data["progress_notes"] = note
|
||||
c = _review(data)
|
||||
assert c == (f"Progress note not read ({state}) — telehealth not "
|
||||
f"checked"), (note, c)
|
||||
|
||||
|
||||
@case
|
||||
def a_progress_note_that_raised_is_review_with_its_first_line():
|
||||
c = _review(_ub(progress_notes={"opened": "iframe",
|
||||
"error": "Target closed\nCall log: ..."}))
|
||||
assert c == ("Progress note not read (error: Target closed) — telehealth "
|
||||
"not checked"), c
|
||||
|
||||
|
||||
@case
|
||||
def zero_cpt_rows_read_is_review():
|
||||
assert _review(_ub(cpt=[])) == "No CPT rows were read"
|
||||
|
||||
|
||||
@case
|
||||
def a_condition_that_raised_is_review():
|
||||
c = _review(_ub(_conditions={"fired": [], "saved": False, "comments": {},
|
||||
"review": [],
|
||||
"failed": [{"name": "telehealth",
|
||||
"stage": "apply",
|
||||
"error": "Timeout"}]}))
|
||||
assert c == "telehealth failed: Timeout", c
|
||||
|
||||
|
||||
@case
|
||||
def a_condition_review_reason_is_review():
|
||||
c = _review(_ub(_conditions={
|
||||
"fired": [], "saved": False, "comments": {}, "failed": [],
|
||||
"review": [{"name": "dummy_codes",
|
||||
"reason": "CPT/HCPCS tab could not be opened — not "
|
||||
"checked"}]}))
|
||||
assert c == ("dummy_codes: CPT/HCPCS tab could not be opened — not "
|
||||
"checked"), c
|
||||
|
||||
|
||||
@case
|
||||
def dental_unreadable_billing_notes_is_review():
|
||||
c = _review({"claim_no": "1", "high_risk": {"high_risk": None,
|
||||
"match": ""}})
|
||||
assert c == "High risk: Billing Notes could not be read", c
|
||||
|
||||
|
||||
@case
|
||||
def dental_sliding_fee_unknown_is_review_not_a_pass():
|
||||
c = _review({"high_risk": {"high_risk": False},
|
||||
"sliding_fee": {"verdict": "unknown",
|
||||
"reason": "no Patient Info"}})
|
||||
assert c == "Sliding fee UNKNOWN (no Patient Info)", c
|
||||
|
||||
|
||||
@case
|
||||
def a_dental_claim_is_not_asked_for_conditions_notes_or_cpt_rows():
|
||||
"""Dental never runs the conditions or opens a note — by design."""
|
||||
o = cs.claim_outcome(True, {"claim_no": "1",
|
||||
"high_risk": {"high_risk": False}})
|
||||
assert o["Category"] == "Pass" and o["Comments"] == cs.CLEAN, o
|
||||
|
||||
|
||||
@case
|
||||
def a_dental_claim_that_ran_the_conditions_carries_both_in_one_row():
|
||||
"""
|
||||
Operator, 2026-10-12: a Dental claim runs the conditions too — its row
|
||||
has the condition comments AND the Dental findings, and the same
|
||||
claim-level checks as UB/HCFA (it did read a note and CPT rows).
|
||||
"""
|
||||
data = _ub(["dummy_codes"], screen="dental",
|
||||
high_risk={"high_risk": True, "match": ""},
|
||||
sliding_fee={"verdict": "active"})
|
||||
o = cs.claim_outcome(True, data)
|
||||
assert o["Category"] == "Fail", o
|
||||
assert "DCONS/Dscprit" in o["Comments"], o
|
||||
assert cs.HIGH_RISK in o["Comments"], o
|
||||
assert o["Current Status"] == "Corrections pending (not saved)", o
|
||||
assert cs.is_dental(data)
|
||||
|
||||
|
||||
@case
|
||||
def a_dental_read_is_asked_for_its_note_and_rows_like_any_claim():
|
||||
data = _ub(screen="dental", high_risk={"high_risk": False, "match": ""},
|
||||
progress_notes={"opened": "disabled"})
|
||||
c = _review(data)
|
||||
assert "Progress note not read (disabled)" in c, c
|
||||
no_cond = {k: v for k, v in data.items() if k != "_conditions"}
|
||||
no_cond["progress_notes"] = {"opened": "modal", "text": "x"}
|
||||
assert "Conditions not run" in _review(no_cond), _review(no_cond)
|
||||
|
||||
|
||||
@case
|
||||
def a_dental_claim_that_never_finished_loading_is_review():
|
||||
c = _review({"high_risk": {"high_risk": True},
|
||||
"_not_evaluated": "Dental form: no service line after 20s"})
|
||||
# The finding first, then the reason.
|
||||
assert c == (f"{cs.HIGH_RISK}; Dental form: no service line after 20s"), c
|
||||
|
||||
|
||||
@case
|
||||
def review_wins_over_fail_and_carries_both():
|
||||
"""A fired condition on a claim whose note was never read."""
|
||||
c = _review(_ub(["dummy_codes"], progress_notes={"opened": "disabled"}))
|
||||
assert c == (f"{cs.COMMENTS['dummy_codes']}; Progress note not read "
|
||||
f"(disabled) — telehealth not checked"), c
|
||||
|
||||
|
||||
@case
|
||||
def every_reason_is_listed_in_order():
|
||||
data = _ub(["telehealth"], cpt=[], progress_notes={"opened": "nothing"})
|
||||
data["_conditions"]["failed"] = [{"name": "j2930_j2001_update",
|
||||
"stage": "detect", "error": "boom"}]
|
||||
data["_conditions"]["review"] = [{"name": "vaccine_admin_codes",
|
||||
"reason": "payer unmapped"}]
|
||||
c = _review(data)
|
||||
assert c.split("; ") == [
|
||||
cs.COMMENTS["telehealth"],
|
||||
"Progress note not read (nothing) — telehealth not checked",
|
||||
"No CPT rows were read",
|
||||
"j2930_j2001_update failed: boom",
|
||||
"vaccine_admin_codes: payer unmapped"], c
|
||||
|
||||
|
||||
@case
|
||||
def a_fail_uses_the_conditions_own_comment_when_it_has_one():
|
||||
data = _ub(["telehealth", "dummy_codes"])
|
||||
data["_conditions"]["comments"] = {"telehealth": "Telehealth (POS 02 set)"}
|
||||
o = cs.claim_outcome(True, data)
|
||||
assert o["Category"] == "Fail", o
|
||||
assert o["Comments"] == ("Telehealth (POS 02 set); "
|
||||
+ cs.COMMENTS["dummy_codes"]), o
|
||||
|
||||
|
||||
@case
|
||||
def a_partial_note_with_no_sections_is_review():
|
||||
"""
|
||||
Unit D review, finding 1 (2026-10-05): the header arrived and nothing
|
||||
under it — no Procedure Codes, so telehealth saw nothing (the D8 false
|
||||
Clean). This test pinned Pass before; it is Review now.
|
||||
"""
|
||||
c = _review(_ub(progress_notes={"opened": "modal", "text": "x",
|
||||
"partial": True, "partial_sections": 0}))
|
||||
assert c == "Progress note partial (0 sections) — telehealth not checked", c
|
||||
|
||||
|
||||
@case
|
||||
def a_partial_note_with_some_sections_is_review_too():
|
||||
"""Sections never settled: the Procedure Codes may still be missing."""
|
||||
c = _review(_ub(progress_notes={"opened": "modal", "text": "x",
|
||||
"partial": True, "partial_sections": 3}))
|
||||
assert c == ("Progress note may be partial (3 section(s)) — telehealth "
|
||||
"may not be fully checked"), c
|
||||
|
||||
|
||||
@case
|
||||
def a_partial_note_with_an_unknown_section_count_has_no_question_mark():
|
||||
"""
|
||||
Unit F review, finding 4 (2026-10-05): these reasons are also the
|
||||
on-screen "Needs review" lines, and D9 keeps "?" off the client's box.
|
||||
"""
|
||||
c = _review(_ub(progress_notes={"opened": "modal", "text": "x",
|
||||
"partial": True}))
|
||||
assert c == ("Progress note may be partial (section count unknown) — "
|
||||
"telehealth may not be fully checked"), c
|
||||
assert "?" not in c, c
|
||||
|
||||
|
||||
@case
|
||||
def a_partial_note_keeps_the_fired_conditions_comment_first():
|
||||
c = _review(_ub(["telehealth"],
|
||||
progress_notes={"opened": "tab", "text": "x",
|
||||
"partial": True, "partial_sections": 0}))
|
||||
assert c == (f"{cs.COMMENTS['telehealth']}; Progress note partial "
|
||||
f"(0 sections) — telehealth not checked"), c
|
||||
|
||||
|
||||
@case
|
||||
def first_line_is_the_one_shared_helper():
|
||||
"""
|
||||
Unit D review, finding 4: core/text.first_line replaced three copies
|
||||
(conditions/base, core/charge_summary, the flow's inline one).
|
||||
"""
|
||||
from ecw_bot.core.text import first_line
|
||||
from ecw_bot.conditions import base
|
||||
assert first_line(TimeoutError("Timeout 30000ms\n call log:\n x")) == \
|
||||
"Timeout 30000ms"
|
||||
assert first_line(ValueError()) == "ValueError" # empty message
|
||||
assert first_line(" boom \nmore") == "boom"
|
||||
assert first_line("") == ""
|
||||
assert base.first_line is first_line and cs.first_line is first_line
|
||||
assert flow.first_line is first_line
|
||||
# Final review, 2026-10-05: the three private copies that were left
|
||||
# (claim_form, spreadsheet, vaccine_admin_codes) are gone too. Two of
|
||||
# them returned "" for a message that starts with a newline.
|
||||
from ecw_bot.pages import claim_form, progress_note
|
||||
from ecw_bot.core import spreadsheet
|
||||
from ecw_bot.conditions import vaccine_admin_codes
|
||||
for mod in (claim_form, progress_note, spreadsheet, vaccine_admin_codes):
|
||||
assert mod.first_line is first_line, mod.__name__
|
||||
assert not hasattr(mod, "_first_line"), mod.__name__
|
||||
assert first_line(RuntimeError("\nreal first line\nmore")) == \
|
||||
"real first line"
|
||||
|
||||
|
||||
# ── Final review, 2026-10-05 ─────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def a_saved_claim_that_is_review_says_it_was_saved():
|
||||
"""
|
||||
"Needs manual review" alone hid a save that had happened: the reviewer
|
||||
could treat the claim as untouched and redo or undo live edits.
|
||||
"""
|
||||
o = cs.claim_outcome(True, _ub(["zero_fee_uncheck"], saved=True,
|
||||
progress_notes={"opened": "disabled"}))
|
||||
assert o["Category"] == "Review", o
|
||||
assert o["Current Status"] == "Needs manual review", o
|
||||
assert o["Comments"].startswith(f"{cs.SAVED_NOTE}; "
|
||||
f"{cs.COMMENTS['zero_fee_uncheck']}"), o
|
||||
unsaved = cs.claim_outcome(True, _ub(["zero_fee_uncheck"],
|
||||
progress_notes={"opened": "disabled"}))
|
||||
assert cs.SAVED_NOTE not in unsaved["Comments"], unsaved
|
||||
|
||||
|
||||
@case
|
||||
def a_search_that_raised_is_review_not_a_blank_row():
|
||||
data = {"claim_no": "1", "_search_error": "Lookup never became visible"}
|
||||
o = cs.claim_outcome(False, data)
|
||||
assert o == {"Comments": "Bot could not search the claim (Lookup never "
|
||||
"became visible)",
|
||||
"Current Status": "Needs manual review",
|
||||
"Category": "Review"}, o
|
||||
# A claim really not in the queue stays blank.
|
||||
assert cs.claim_outcome(False, {}) == {}
|
||||
|
||||
|
||||
@case
|
||||
def an_error_after_the_conditions_ran_is_worded_by_stage():
|
||||
data = {"claim_no": "1", "_error": "describe blew up",
|
||||
"_conditions": {"fired": ["dummy_codes"], "saved": False,
|
||||
"failed": [], "review": [], "comments": {}}}
|
||||
o = cs.claim_outcome(True, data)
|
||||
assert o["Category"] == "Review", o
|
||||
assert o["Comments"] == (f"{cs.COMMENTS['dummy_codes']}; Bot failed after "
|
||||
f"the conditions ran (describe blew up)"), o
|
||||
plain = cs.claim_outcome(True, {"claim_no": "1", "_error": "x"})
|
||||
assert plain["Comments"] == "Bot could not read the claim (x)", plain
|
||||
|
||||
|
||||
@case
|
||||
def cpt_rows_with_no_icd_rows_is_review():
|
||||
"""depression_dx_pointer reads the ICD list; an empty one is unchecked."""
|
||||
o = cs.claim_outcome(True, _ub(icd=[]))
|
||||
assert o["Category"] == "Review", o
|
||||
assert o["Comments"] == "No ICD rows were read", o
|
||||
assert cs.claim_outcome(True, _ub(icd=[{"code": "R69"}]))["Category"] \
|
||||
== "Pass"
|
||||
# Zero CPT rows already says it; no second ICD line on top.
|
||||
reasons = cs.review_reasons(_ub(cpt=[], icd=[]))
|
||||
assert reasons == ["No CPT rows were read"], reasons
|
||||
|
||||
|
||||
@case
|
||||
def a_note_skipped_on_a_form_that_never_loaded_is_named():
|
||||
reasons = cs.review_reasons(_ub(progress_notes={"opened": "skipped"}))
|
||||
assert reasons == ["Progress note not read (skipped) — telehealth not "
|
||||
"checked"], reasons
|
||||
|
||||
|
||||
@case
|
||||
def the_telehealth_fallback_comments_claim_nothing_that_may_not_have_happened():
|
||||
"""
|
||||
Used only when a summary_comment hook raises: they must not say "G2025
|
||||
added" (it can fail), "93 or 95" (Medicaid is GT) or "unchecked".
|
||||
"""
|
||||
for name in ("telehealth", "medicare_telehealth"):
|
||||
text = cs.COMMENTS[name]
|
||||
for false_claim in ("G2025 added", "93 or 95", "unchecked"):
|
||||
assert false_claim not in text, (name, text)
|
||||
assert "see the log" in text, text
|
||||
|
||||
|
||||
@case
|
||||
def a_settled_note_is_still_a_pass():
|
||||
"""Control: `partial` absent (the note settled) — Pass as before."""
|
||||
o = cs.claim_outcome(True, _ub(progress_notes={"opened": "modal",
|
||||
"text": "x"}))
|
||||
assert o["Category"] == "Pass", o
|
||||
|
||||
|
||||
# ── build_rows ───────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def header_is_the_formats_24_columns_in_order():
|
||||
rows = _build()
|
||||
assert rows[0] == cs.HEADERS and len(rows[0]) == 24
|
||||
assert "CLAIMTYPE" not in rows[0]
|
||||
|
||||
|
||||
@case
|
||||
def one_row_per_claim_numbered_in_report_order():
|
||||
rows = _build()
|
||||
assert [r[_col("CLAIMS#")] for r in rows[1:]] == ["1000001", "1000002",
|
||||
"1000003"]
|
||||
assert [r[_col("S.No")] for r in rows[1:]] == [1, 2, 3]
|
||||
|
||||
|
||||
@case
|
||||
def report_columns_are_copied_and_group_and_refunds_are_derived():
|
||||
r = _build()[1]
|
||||
assert r[_col("PAYER")] == "Medicaid" and r[_col("CHARGES")] == "275.00"
|
||||
assert r[_col("Guarantor Name")] == "Test, Patient"
|
||||
assert r[_col("Group Name")] == "Medicaid"
|
||||
assert r[_col("Pmts/Refunds")] == "30.00", r # 35.00 - 5.00
|
||||
|
||||
|
||||
@case
|
||||
def unmapped_payer_gets_a_blank_group():
|
||||
assert _build()[3][_col("Group Name")] == ""
|
||||
|
||||
|
||||
@case
|
||||
def tracking_columns_fill_only_for_claims_the_run_read():
|
||||
outcomes = {"1000001": cs.claim_outcome(True, _ub())}
|
||||
rows = _build(outcomes)
|
||||
read, unread = rows[1], rows[2]
|
||||
assert read[_col("Category")] == "Pass"
|
||||
assert read[_col("Completed On")] == "10/01/2026"
|
||||
assert read[_col("Recevied date")] == "10/01/2026"
|
||||
assert read[_col("Assigned To ")] == "eCW Bot"
|
||||
for h in ("Recevied date", "Assigned To ", "Completed On", "Comments",
|
||||
"Current Status", "Category"):
|
||||
assert unread[_col(h)] == "", (h, unread)
|
||||
|
||||
|
||||
@case
|
||||
def a_claim_id_only_test_sheet_still_builds():
|
||||
rows = cs.build_rows([["Claim No"], ["9000002"], ["9000001"]], 0, 0,
|
||||
{}, "10/01/2026", "eCW Bot", MAPPING)
|
||||
assert [r[_col("CLAIMS#")] for r in rows[1:]] == ["9000002", "9000001"]
|
||||
assert rows[1][_col("PAYER")] == "" and rows[1][_col("Pmts/Refunds")] == ""
|
||||
|
||||
|
||||
# ── write ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@case
|
||||
def write_saves_a_readable_sheet_and_keeps_a_same_day_file_aside():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = pathlib.Path(tmp)
|
||||
report = tmp / "report.xlsx"
|
||||
spreadsheet.write_xlsx(report, REPORT)
|
||||
out = cs.write(report, {}, tmp, "10-01-2026", "eCW Bot")
|
||||
assert out.name == "Charge Summary_10-01-2026.xlsx"
|
||||
back = spreadsheet.read_rows(out)
|
||||
# read_rows strips cells, so the format's "Assigned To " reads back
|
||||
# without its trailing space.
|
||||
assert back[0] == [h.strip() for h in cs.HEADERS], back[0]
|
||||
assert len(back) == 4, back
|
||||
cs.write(report, {}, tmp, "10-01-2026", "eCW Bot")
|
||||
kept = list(tmp.glob("Charge Summary_10-01-2026__*.xlsx"))
|
||||
assert len(kept) == 1, kept
|
||||
|
||||
|
||||
@case
|
||||
def file_name_is_live_or_TEST():
|
||||
assert cs.file_name("10-01-2026") == "Charge Summary_10-01-2026.xlsx"
|
||||
assert cs.file_name("10-01-2026", test_mode=True) == \
|
||||
"Charge Summary_TEST_10-01-2026.xlsx"
|
||||
|
||||
|
||||
@case
|
||||
def test_mode_writes_the_TEST_file_and_leaves_the_live_one_alone():
|
||||
"""D1: a test run must never push the live same-day summary aside."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = pathlib.Path(tmp)
|
||||
report = tmp / "report.xlsx"
|
||||
spreadsheet.write_xlsx(report, REPORT)
|
||||
live = cs.write(report, {}, tmp, "10-01-2026", "eCW Bot")
|
||||
live_bytes = live.read_bytes()
|
||||
out = cs.write(report, {}, tmp, "10-01-2026", "eCW Bot",
|
||||
test_mode=True)
|
||||
assert out == tmp / "Charge Summary_TEST_10-01-2026.xlsx", out
|
||||
assert len(spreadsheet.read_rows(out)) == 4
|
||||
assert live.read_bytes() == live_bytes, "the live summary changed"
|
||||
assert not list(tmp.glob("Charge Summary_10-01-2026__*.xlsx")), \
|
||||
"the live summary was pushed aside by a test run"
|
||||
|
||||
|
||||
@case
|
||||
def a_same_day_summary_open_in_excel_does_not_lose_this_runs_sheet():
|
||||
"""
|
||||
D10: the earlier summary is held open (on Windows a Python handle
|
||||
refuses the rename with WinError 32, as Excel does). The new sheet goes
|
||||
to a `__HHMMSS` name, the open file is untouched, nothing raises, and
|
||||
the path returned is the new file — the one the flow logs.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = pathlib.Path(tmp)
|
||||
report = tmp / "report.xlsx"
|
||||
spreadsheet.write_xlsx(report, REPORT)
|
||||
first = cs.write(report, {}, tmp, "10-01-2026", "eCW Bot")
|
||||
first_bytes = first.read_bytes()
|
||||
outcomes = {"1000001": cs.claim_outcome(True, _ub())}
|
||||
with open(first, "rb"):
|
||||
out = cs.write(report, outcomes, tmp, "10-01-2026", "eCW Bot")
|
||||
assert out != first, "wrote over the open file's name"
|
||||
assert re.fullmatch(r"Charge Summary_10-01-2026__\d{6}\.xlsx",
|
||||
out.name), out.name
|
||||
assert first.read_bytes() == first_bytes, "the open file was changed"
|
||||
back = spreadsheet.read_rows(out)
|
||||
assert len(back) == 4, back
|
||||
assert back[1][_col("Category")] == "Pass", back[1]
|
||||
|
||||
|
||||
FALLBACK_NAME = "Charge Summary_10-01-2026__101010.xlsx"
|
||||
|
||||
|
||||
def _flow_write(test_file, returns=FALLBACK_NAME):
|
||||
"""
|
||||
_write_charge_summary with charge_summary.write replaced by a recorder
|
||||
that returns `returns`, and msgbox.info_box by one that records instead
|
||||
of popping a real Windows box. Returns (args, kwargs it was called with,
|
||||
printed text, [(title, body)] of boxes shown).
|
||||
"""
|
||||
calls = []
|
||||
shown = []
|
||||
|
||||
def _record(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
# By default, as if the usual name was open in Excel: the log must
|
||||
# name THIS, and the operator must be told.
|
||||
return pathlib.Path(returns)
|
||||
|
||||
orig = (cs.write, cfg.WRITE_CHARGE_SUMMARY, cfg.TEST_CLAIM_IDS_FILE,
|
||||
flow.msgbox.info_box)
|
||||
printed = io.StringIO()
|
||||
try:
|
||||
cs.write = _record
|
||||
cfg.WRITE_CHARGE_SUMMARY = True
|
||||
cfg.TEST_CLAIM_IDS_FILE = test_file
|
||||
flow.msgbox.info_box = lambda t, b: shown.append((t, b))
|
||||
with contextlib.redirect_stdout(printed):
|
||||
flow._write_charge_summary({"file": pathlib.Path("r.xlsx")},
|
||||
{"1": {"Category": "Pass"}})
|
||||
finally:
|
||||
(cs.write, cfg.WRITE_CHARGE_SUMMARY, cfg.TEST_CLAIM_IDS_FILE,
|
||||
flow.msgbox.info_box) = orig
|
||||
assert len(calls) == 1, calls
|
||||
return calls[0][0], calls[0][1], printed.getvalue(), shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_passes_test_mode_when_the_ids_came_from_a_test_sheet():
|
||||
_, kwargs, _, _ = _flow_write("C:/somewhere/Standard Claims.xlsx")
|
||||
assert kwargs.get("test_mode") is True, kwargs
|
||||
|
||||
|
||||
@case
|
||||
def flow_passes_test_mode_off_for_a_live_run():
|
||||
_, kwargs, _, _ = _flow_write("")
|
||||
assert kwargs.get("test_mode") is False, kwargs
|
||||
|
||||
|
||||
@case
|
||||
def flow_logs_the_file_actually_written():
|
||||
_, _, printed, _ = _flow_write("")
|
||||
line = "[SUMMARY] 1 claim(s) filled in -> " + FALLBACK_NAME
|
||||
assert line in printed, printed
|
||||
|
||||
|
||||
@case
|
||||
def flow_shows_a_box_when_the_summary_went_to_another_name():
|
||||
"""
|
||||
Fix round 1 (review item D10, 2026-10-05): the person with the usual
|
||||
file open in Excel is at the desk, so the fallback name goes in a box,
|
||||
not only the log — else they present the stale sheet they have open.
|
||||
"""
|
||||
_, _, _, shown = _flow_write("")
|
||||
assert len(shown) == 1, shown
|
||||
title, body = shown[0]
|
||||
assert title == "eCW Bot — Charge summary saved under another name", title
|
||||
usual = cs.file_name(cfg.TODAY)
|
||||
assert f"could not be saved as {usual}" in body, body
|
||||
assert FALLBACK_NAME in body, body
|
||||
# Fix round 2: the box must not promise the earlier file was untouched —
|
||||
# a failed move-back can leave a half-file under the usual name.
|
||||
assert "left as it was" not in body, body
|
||||
|
||||
|
||||
@case
|
||||
def flow_shows_no_box_when_the_summary_has_its_usual_name():
|
||||
_, _, _, shown = _flow_write("", returns=cs.file_name(cfg.TODAY))
|
||||
assert shown == [], shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_shows_no_box_for_a_test_run_under_the_TEST_name():
|
||||
_, _, _, shown = _flow_write("C:/somewhere/Standard Claims.xlsx",
|
||||
returns=cs.file_name(cfg.TODAY, True))
|
||||
assert shown == [], shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_write_never_raises_on_a_bad_report():
|
||||
shown = []
|
||||
old = flow.msgbox.info_box
|
||||
flow.msgbox.info_box = lambda t, b: shown.append(t)
|
||||
try:
|
||||
flow._write_charge_summary({"file": pathlib.Path("no_such_file.xlsx")},
|
||||
{})
|
||||
finally:
|
||||
flow.msgbox.info_box = old
|
||||
assert shown, "a failed write should show a box, not raise"
|
||||
|
||||
|
||||
# ── per-claim writes (2026-10-12: the summary grows claim by claim) ─────────
|
||||
|
||||
@case
|
||||
def the_report_is_read_once_while_it_is_unchanged():
|
||||
"""Built after every claim, twice (Excel + DB): one read, not 2 x N."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
report = pathlib.Path(tmp) / "report.xlsx"
|
||||
spreadsheet.write_xlsx(report, REPORT)
|
||||
reads = []
|
||||
real = spreadsheet.read_rows
|
||||
spreadsheet.read_rows = lambda p: reads.append(p) or real(p)
|
||||
try:
|
||||
first = cs.build(report, {}, "eCW Bot")
|
||||
cs.build(report, {"1000001": cs.claim_outcome(True, _ub())},
|
||||
"eCW Bot")
|
||||
assert len(reads) == 1, reads
|
||||
spreadsheet.write_xlsx(report, REPORT[:2])
|
||||
again = cs.build(report, {}, "eCW Bot")
|
||||
finally:
|
||||
spreadsheet.read_rows = real
|
||||
assert len(reads) == 2, "a changed report must be read again"
|
||||
assert len(first) == 4 and len(again) == 2, (first, again)
|
||||
|
||||
@case
|
||||
def write_into_rewrites_this_runs_own_file_with_no_backup():
|
||||
"""
|
||||
After the first claim, the sheet goes back into the file that claim
|
||||
wrote — no `__HHMMSS` copy per claim; only the first write keeps an
|
||||
earlier same-day file aside.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = pathlib.Path(tmp)
|
||||
report = tmp / "report.xlsx"
|
||||
spreadsheet.write_xlsx(report, REPORT)
|
||||
first = cs.write(report, {"1000001": cs.claim_outcome(True, _ub())},
|
||||
tmp, "10-01-2026", "eCW Bot")
|
||||
out = cs.write(report, {"1000001": cs.claim_outcome(True, _ub()),
|
||||
"1000002": cs.claim_outcome(True, _ub())},
|
||||
tmp, "10-01-2026", "eCW Bot", into=first)
|
||||
assert out == first, out
|
||||
assert not list(tmp.glob("Charge Summary_10-01-2026__*.xlsx")), \
|
||||
"a backup was made for this run's own file"
|
||||
back = spreadsheet.read_rows(out)
|
||||
assert back[1][_col("Category")] == "Pass", back[1]
|
||||
assert back[2][_col("Category")] == "Pass", back[2]
|
||||
|
||||
|
||||
@case
|
||||
def write_into_a_locked_file_goes_to_a_new_name():
|
||||
"""The operator opened the in-progress summary in Excel mid-run."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = pathlib.Path(tmp)
|
||||
report = tmp / "report.xlsx"
|
||||
spreadsheet.write_xlsx(report, REPORT)
|
||||
first = cs.write(report, {}, tmp, "10-01-2026", "eCW Bot")
|
||||
first_bytes = first.read_bytes()
|
||||
real = spreadsheet.write_xlsx
|
||||
|
||||
def locked(path, rows):
|
||||
if pathlib.Path(path) == first:
|
||||
raise PermissionError(13, "Permission denied")
|
||||
return real(path, rows)
|
||||
|
||||
spreadsheet.write_xlsx = locked
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()) as printed:
|
||||
out = cs.write(report, {"1000001": cs.claim_outcome(True,
|
||||
_ub())},
|
||||
tmp, "10-01-2026", "eCW Bot", into=first)
|
||||
finally:
|
||||
spreadsheet.write_xlsx = real
|
||||
assert re.fullmatch(r"Charge Summary_10-01-2026__\d{6}\.xlsx",
|
||||
out.name), out.name
|
||||
assert first.read_bytes() == first_bytes, "the locked file changed"
|
||||
assert spreadsheet.read_rows(out)[1][_col("Category")] == "Pass"
|
||||
assert "open or locked" in printed.getvalue(), printed.getvalue()
|
||||
|
||||
|
||||
def _flow_write_twice(first_returns, second_returns):
|
||||
"""_write_charge_summary called for two claims of one run."""
|
||||
calls, shown = [], []
|
||||
returns = [pathlib.Path(first_returns), pathlib.Path(second_returns)]
|
||||
|
||||
def _record(*args, **kwargs):
|
||||
calls.append(kwargs)
|
||||
return returns[len(calls) - 1]
|
||||
|
||||
orig = (cs.write, cfg.WRITE_CHARGE_SUMMARY, cfg.TEST_CLAIM_IDS_FILE,
|
||||
flow.msgbox.info_box)
|
||||
info = {"file": pathlib.Path("r.xlsx")}
|
||||
try:
|
||||
cs.write = _record
|
||||
cfg.WRITE_CHARGE_SUMMARY = True
|
||||
cfg.TEST_CLAIM_IDS_FILE = ""
|
||||
flow.msgbox.info_box = lambda t, b: shown.append((t, b))
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
flow._write_charge_summary(info, {"1": {"Category": "Pass"}})
|
||||
flow._write_charge_summary(info, {"1": {"Category": "Pass"},
|
||||
"2": {"Category": "Fail"}})
|
||||
finally:
|
||||
(cs.write, cfg.WRITE_CHARGE_SUMMARY, cfg.TEST_CLAIM_IDS_FILE,
|
||||
flow.msgbox.info_box) = orig
|
||||
return calls, shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_second_claim_writes_into_the_file_the_first_one_wrote():
|
||||
usual = cs.file_name(cfg.TODAY)
|
||||
calls, shown = _flow_write_twice(usual, usual)
|
||||
assert calls[0].get("into") is None, calls
|
||||
assert calls[1].get("into") == pathlib.Path(usual), calls
|
||||
assert shown == [], shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_shows_the_another_name_box_once_not_per_claim():
|
||||
calls, shown = _flow_write_twice(FALLBACK_NAME, FALLBACK_NAME)
|
||||
assert calls[1].get("into") == pathlib.Path(FALLBACK_NAME), calls
|
||||
assert len(shown) == 1, shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_shows_the_box_again_when_the_run_moves_to_a_new_name():
|
||||
"""Mid-run, the operator opened this run's file: the sheet moves on."""
|
||||
usual = cs.file_name(cfg.TODAY)
|
||||
_, shown = _flow_write_twice(usual, FALLBACK_NAME)
|
||||
assert len(shown) == 1 and FALLBACK_NAME in shown[0][1], shown
|
||||
|
||||
|
||||
@case
|
||||
def flow_write_error_box_shows_once_per_run():
|
||||
shown = []
|
||||
old = flow.msgbox.info_box
|
||||
flow.msgbox.info_box = lambda t, b: shown.append(t)
|
||||
info = {"file": pathlib.Path("no_such_file.xlsx")}
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
flow._write_charge_summary(info, {})
|
||||
flow._write_charge_summary(info, {})
|
||||
finally:
|
||||
flow.msgbox.info_box = old
|
||||
assert len(shown) == 1, shown
|
||||
|
||||
|
||||
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