""" 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())