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>
229 lines
8.6 KiB
Python
229 lines
8.6 KiB
Python
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))
|