r""" Ad hoc check: read claim numbers straight out of a Medcor-Ready-style Excel export and open exactly those claims, one by one, to see what our conditions do to them — without touching the live Medcor Ready queue or the export step at all. python tools\check_claims_from_excel.py python tools\check_claims_from_excel.py "Input\Some Other Claims.xlsx" Exists because the full pipeline (`ecw_bot.flows.export_medcor_ready.main`) always re-exports and walks the WHOLE current Medcor Ready queue, in whatever order eCW returns it — there is no way to target specific claim numbers through it, and CLAIM_PROCESS_LIMIT only says how many of that queue's own claims to touch, not which ones. The point of reading claim numbers from an Excel file instead of typing them is: when we already know from the Excel (its own "Comments" column says so) that a claim is eligible for a specific condition, we can hand this script that one file and watch that exact condition fire — instead of downloading the live queue and hunting through it one claim at a time. Once every condition has been proven against its own known-eligible claim(s) this way, the real workflow goes back to the full download/export pipeline. This calls the exact same per-claim primitives the real pipeline calls (`claim_filters`, `claim_results.search_claim`, and the flow module's own `_process_claims`) — so what this proves about these claims is what a real run would show for them, not a re-implementation of the logic. `SAVE_CORRECTIONS` / `APPLY_CORRECTIONS` are read from `.env` exactly like a normal run: this script does not turn saving on or off, it only skips the queue-wide filter/lookup/export/categorize steps. Leave SAVE_CORRECTIONS=0 in `.env` while testing. With no arguments, reads "Input\Medcor Ready Claims.xlsx" (the standard input sheet). If that file is password-protected, you'll be prompted for the password at the console — it is never read from a file or hardcoded here. """ import sys as _sys from pathlib import Path as _Path _sys.path.insert(0, str(_Path(__file__).resolve().parent.parent / "src")) # Windows hands these scripts a cp1252 console, which cannot encode the # non-ASCII text (em dashes and the like) some of the reused functions print. for _s in (_sys.stdout, _sys.stderr): try: _s.reconfigure(encoding="utf-8", errors="replace") except Exception: pass import getpass import io import sys import zipfile from playwright.sync_api import sync_playwright from ecw_bot import settings as cfg from ecw_bot.core import guard, runlog from ecw_bot.auth.login import login from ecw_bot.auth.dead_end import LoginTimedOut from ecw_bot.auth.restricted import Restricted from ecw_bot.pages import leftnav, claim_filters from ecw_bot.flows.export_medcor_ready import ( _process_claims, GuardRefused, CLIENT_NAME, start_mode_gate) DEFAULT_EXCEL_PATH = ( _Path(__file__).resolve().parent.parent / "Input" / "Medcor Ready Claims.xlsx") CLAIM_COLUMN = "CLAIMS#" COMMENT_COLUMN = "Comments" def _load_workbook(path: _Path): import openpyxl try: return openpyxl.load_workbook(path, data_only=True) except zipfile.BadZipFile: pass # password-protected legacy OLE container — fall through try: import msoffcrypto except ImportError: raise SystemExit( f"{path} is password-protected. Install msoffcrypto-tool " "(pip install msoffcrypto-tool) to read it.") password = getpass.getpass(f"Password for {path.name}: ") with open(path, "rb") as f: office_file = msoffcrypto.OfficeFile(f) office_file.load_key(password=password) decrypted = io.BytesIO() office_file.decrypt(decrypted) return openpyxl.load_workbook(decrypted, data_only=True) def load_claims_from_excel(path: _Path) -> list: """ Returns [(claim_id, comment), ...] read from the sheet's own "CLAIMS#" / "Comments" columns — never a guess at column position, since a Medcor Ready export's column order isn't guaranteed to be stable. """ if not path.exists(): raise SystemExit(f"Excel file not found: {path}") wb = _load_workbook(path) ws = wb.worksheets[0] rows = list(ws.iter_rows(values_only=True)) if not rows: return [] header = [str(h).strip() if h is not None else "" for h in rows[0]] if CLAIM_COLUMN not in header: raise SystemExit( f"Could not find a '{CLAIM_COLUMN}' column in {path}. " f"Found columns: {header}") claim_col = header.index(CLAIM_COLUMN) comment_col = header.index(COMMENT_COLUMN) if COMMENT_COLUMN in header else None out = [] for row in rows[1:]: raw_id = row[claim_col] if raw_id is None or str(raw_id).strip() == "": continue claim_id = str(int(raw_id)) if isinstance(raw_id, float) else str(raw_id).strip() comment = (row[comment_col] or "") if comment_col is not None else "" out.append((claim_id, comment)) return out def main(excel_path: _Path) -> int: log_path = runlog.start() problems = cfg.validate() if problems: print("[CONFIG] Not ready:") for p in problems: print(f" - {p}") return 2 claims = load_claims_from_excel(excel_path) if not claims: print(f"[CHECK] No claim numbers found in {excel_path}") return 5 claim_ids = [claim_id for claim_id, _ in claims] print(f"[CONFIG] user={cfg.ECW_USERNAME} " f"apply_corrections={cfg.APPLY_CORRECTIONS} " f"save_corrections={cfg.SAVE_CORRECTIONS}") print(f"[CONFIG] log={log_path}") print(f"[CHECK] Loaded {len(claim_ids)} claim(s) from {excel_path}:") for claim_id, comment in claims: note = f" — {comment}" if comment else "" print(f" {claim_id}{note}") if cfg.SAVE_CORRECTIONS: print("[CHECK] WARNING: SAVE_CORRECTIONS is ON — a confirmed " "condition WILL write to eCW. Set it to 0 in .env to only " "observe.") # The same [MODE] banner and LIVE-WRITE start box as the full run: this # tool logs in and reaches the flow's real _process_claims, so it can # save too (final review, 2026-10-05). Before login, so Cancel costs # nothing. if not start_mode_gate(): return 2 # No Charge Summary for an ad hoc check: it has no downloaded claim # report to build one from (the flow's writer raised KeyError on the # missing "file" and showed an error box), and a sheet under the live # summary's name would push the day's real one aside (review item D1). if cfg.WRITE_CHARGE_SUMMARY: print("[CHECK] Charge Summary not written for an ad hoc check") cfg.WRITE_CHARGE_SUMMARY = False # Same for the results database: no report means no rows to build, and # the flow's DB hook would only log a KeyError for the missing "file". if cfg.RESULTS_DB: print("[CHECK] Results database not written for an ad hoc check") cfg.RESULTS_DB = False with sync_playwright() as pw: browser = None try: guard.wait_if_cooling() refusal = guard.check() if refusal: raise GuardRefused(refusal) guard.record_attempt() browser, context, page = login(pw, CLIENT_NAME) guard.record_success() print(f"[OK] Logged in. Landed on: {page.url}") leftnav.open_claims(page) print(f"[OK] On: {page.url}") # Same order _claims_phase uses — Claim Status first, since it # drives whether the other filters can match anything. claim_filters.set_claim_status(page) claim_filters.set_date_type(page) claim_filters.set_date_range(page) claim_filters.clear_balance(page) _process_claims(page, {"ids": claim_ids}) claim_filters.clear_claim_no(page) try: input("\nBrowser open. Press Enter to close...\n") except EOFError: print("[INFO] No console attached — closing") return 0 except GuardRefused as exc: print(f"[GUARD] {exc}") return 3 except LoginTimedOut as exc: print(f"[TIMEOUT] {exc}") return 6 except Restricted as exc: print(f"[RESTRICTED] {exc}") return 4 except Exception as exc: print(f"[FAIL] {exc}") return 1 finally: if browser: browser.close() if __name__ == "__main__": _path = _Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_EXCEL_PATH sys.exit(main(_path))