Files
ecw-charge-validation/tools/check_claims_from_excel.py
ManikandanandClaude Opus 5.5 ba15a574ab Initial import: eCW Charge Validation bot and results Portal
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>
2026-10-23 18:38:17 +05:30

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