"""
export_medcor_ready._categorize_by_payer(): the payer -> Group Name step.
python tests\\test_categorize_by_payer.py
Builds a real xlsx (same helper shape as test_claim_file.py's `xlsx()`) so
this exercises the actual read -> classify -> write path, not a mock of it.
"""
import sys
import zipfile
import pathlib
import tempfile
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
from ecw_bot.flows import export_medcor_ready as flow # noqa: E402
from ecw_bot.core import spreadsheet # noqa: E402
from ecw_bot import settings as cfg # noqa: E402
TMP = pathlib.Path(tempfile.mkdtemp())
NS = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
# Hermetic before anything below runs: _categorize_by_payer() builds its
# output path from cfg.ROOT / "Output" / ..., and without this every test run
# would write into the REAL Output\ folder. It would also leave
# NOTIFY_UNMAPPED_PAYERS at its True default, which pops a real Windows
# message box (case_unmapped_payer_gets_blank_group_not_a_guess triggers it)
# and blocks for up to MSGBOX_TIMEOUT_SECONDS. Must run before CASES is built
# and before main() runs, so every case is affected.
cfg.ROOT = TMP
cfg.NOTIFY_UNMAPPED_PAYERS = False
def xlsx(header, data_rows, name="t.xlsx") -> pathlib.Path:
"""A minimal real xlsx: header + data rows, both plain string lists."""
rows = [header] + data_rows
body = []
for r, row in enumerate(rows, start=1):
cells = "".join(
f'{v}'
for c, v in enumerate(row))
body.append(f'{cells}
')
sheet = (f''
f'{"".join(body)}')
path = TMP / name
with zipfile.ZipFile(path, "w") as z:
z.writestr("xl/worksheets/sheet1.xml", sheet)
return path
HEADER = ["CLAIMS#", "PAYER", "CHARGES"]
def case_categorizes_known_payers():
saved = xlsx(HEADER, [
["9107000", "Medicaid", "275.00"],
["9106000", "DentaQuest", "117.00"],
], name="known.xlsx")
out = flow._categorize_by_payer(saved)
assert out is not None and out.exists(), out
rows = spreadsheet.read_rows(out)
assert rows[0] == HEADER + ["Group Name"], rows[0]
assert rows[1] == ["9107000", "Medicaid", "275.00", "Medicaid"], rows[1]
assert rows[2] == ["9106000", "DentaQuest", "117.00", "Dental - CHP+/DentaQuest"], rows[2]
def case_unmapped_payer_gets_blank_group_not_a_guess():
saved = xlsx(HEADER, [
["9108000", "Some Brand New Payer", "50.00"],
], name="unmapped.xlsx")
out = flow._categorize_by_payer(saved)
rows = spreadsheet.read_rows(out)
assert rows[1][-1] == "", rows[1] # blank, not a guessed group
def case_none_input_is_a_no_op():
assert flow._categorize_by_payer(None) is None
def case_missing_payer_column_does_not_raise():
saved = xlsx(["CLAIMS#", "CHARGES"], [["9108000", "50.00"]],
name="nopayer.xlsx")
assert flow._categorize_by_payer(saved) is None # skipped, not a crash
def case_input_file_is_never_modified():
saved = xlsx(HEADER, [["9107000", "Medicaid", "275.00"]], name="untouched.xlsx")
before_bytes = saved.read_bytes()
before_mtime = saved.stat().st_mtime
flow._categorize_by_payer(saved)
assert saved.read_bytes() == before_bytes, "Input file content changed"
assert saved.stat().st_mtime == before_mtime, "Input file was rewritten"
# The same-day output, under the temp cfg.ROOT set at the top of this file.
OUT = TMP / "Output" / f"Medcor Ready_{cfg.TODAY}.xlsx"
EARLIER = [HEADER + ["Group Name"],
["9108000", "Some Brand New Payer", "50.00", "hand-filled"]]
def case_same_day_rerun_keeps_the_earlier_file():
"""The operator's hand-filled Group Names survive a second run."""
spreadsheet.write_xlsx(OUT, EARLIER)
earlier_bytes = OUT.read_bytes()
before = set(OUT.parent.glob(f"{OUT.stem}__*.xlsx"))
saved = xlsx(HEADER, [["9107000", "Medicaid", "275.00"]], name="rerun.xlsx")
out = flow._categorize_by_payer(saved)
assert out == OUT, out
kept = set(OUT.parent.glob(f"{OUT.stem}__*.xlsx")) - before
assert len(kept) == 1, kept
assert kept.pop().read_bytes() == earlier_bytes, "the kept file changed"
def case_locked_same_day_file_does_not_lose_the_categorized_output():
"""
Review item D10: the earlier file is open in Excel, so Windows refuses
to rename it (WinError 32; a Python `open(..., "rb")` handle does the
same). That used to throw this run's categorized rows away. Now they
land under a `__HHMMSS` name, the open file is untouched, and the path
returned is the new one.
"""
spreadsheet.write_xlsx(OUT, EARLIER)
earlier_bytes = OUT.read_bytes()
saved = xlsx(HEADER, [
["9107000", "Medicaid", "275.00"],
["9106000", "DentaQuest", "117.00"],
], name="locked.xlsx")
with open(OUT, "rb"):
out = flow._categorize_by_payer(saved)
assert out is not None, "the categorized output was lost"
assert out != OUT and out.parent == OUT.parent, out
assert out.name.startswith(f"{OUT.stem}__"), out.name
rows = spreadsheet.read_rows(out)
assert rows[1] == ["9107000", "Medicaid", "275.00", "Medicaid"], rows
assert rows[2] == ["9106000", "DentaQuest", "117.00", "Dental - CHP+/DentaQuest"], rows
assert OUT.read_bytes() == earlier_bytes, "the open file was changed"
def _categorize_with_boxes(saved):
"""
_categorize_by_payer with NOTIFY_UNMAPPED_PAYERS on and the message box
replaced by a recorder (never a real box). Returns (path, [(title, body)]).
"""
shown = []
orig = (cfg.NOTIFY_UNMAPPED_PAYERS, flow.msgbox.info_box)
try:
cfg.NOTIFY_UNMAPPED_PAYERS = True
flow.msgbox.info_box = lambda t, b: shown.append((t, b))
out = flow._categorize_by_payer(saved)
finally:
cfg.NOTIFY_UNMAPPED_PAYERS, flow.msgbox.info_box = orig
return out, shown
def case_locked_same_day_file_tells_the_operator_the_new_name():
"""
Fix round 1 (review item D10, 2026-10-05): whoever has the usual file
open in Excel is at the desk, so the `__HHMMSS` name goes in a box too,
not only the log — else they keep filling in the earlier run's sheet.
"""
spreadsheet.write_xlsx(OUT, EARLIER)
saved = xlsx(HEADER, [["9107000", "Medicaid", "275.00"]],
name="locked-box.xlsx")
with open(OUT, "rb"):
out, shown = _categorize_with_boxes(saved)
assert out is not None and out != OUT, out
assert len(shown) == 1, shown
title, body = shown[0]
assert title == "eCW Bot — Medcor Ready saved under another name", title
assert f"could not be saved as {OUT.name}" in body, body
assert out.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
def case_unlocked_same_day_rerun_shows_no_new_name_box():
spreadsheet.write_xlsx(OUT, EARLIER)
saved = xlsx(HEADER, [["9107000", "Medicaid", "275.00"]],
name="unlocked-box.xlsx")
out, shown = _categorize_with_boxes(saved)
assert out == OUT, out
assert shown == [], shown
CASES = [v for k, v in sorted(globals().items()) if k.startswith("case_")]
def main() -> int:
failures = 0
for fn in CASES:
name = fn.__name__[5:].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())