886 lines
34 KiB
Python
886 lines
34 KiB
Python
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_payer_the_sheet_and_ecw_disagree_on_is_noted_and_ecws_is_used():
|
|
# Operator, 2026-10-28: the rules go by eCW's payer; the sheet's is
|
|
# compared, and a difference is a note — not a finding (Category kept).
|
|
ecw = _ub(insurance=[{"Name": "Medicare"}])
|
|
r = _build({"1000001": cs.claim_outcome(True, ecw)})[1]
|
|
assert r[_col("Category")] == "Pass", r
|
|
assert r[_col("Comments")].endswith(
|
|
"; Payer: sheet says Medicaid, eCW says Medicare - eCW used"), r
|
|
assert r[_col("PAYER")] == "Medicaid", "the sheet's column stays"
|
|
|
|
|
|
@case
|
|
def no_payer_note_when_they_agree_or_ecw_shows_none():
|
|
for ecw in ([{"Name": " medicaid "}], [], [{"Name": ""}]):
|
|
r = _build({"1000001": cs.claim_outcome(True,
|
|
_ub(insurance=ecw))})[1]
|
|
assert "Payer:" not in r[_col("Comments")], (ecw, r)
|
|
|
|
|
|
@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())
|