Files
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

190 lines
7.1 KiB
Python

"""
Claim-id extraction from the exported report.
python tests\\test_claim_file.py
Built xlsx files, not fixtures checked into the repo: the real report is patient
data and must not be committed, but the *shape* of it can be reproduced exactly.
Each case below is written as a genuine xlsx — zip, worksheet XML, inline
`t="str"` cells — matching what eCW actually produced on 2026-09-07:
<row r="1"><c r="A1" t="str"><v></v></c>
<c r="C1" t="str"><v>CLAIMS#</v></c> ...
The cases that matter, and why each one is here:
* the real header is **`CLAIMS#`**, not `CLAIM #` as the on-screen grid calls
it — reading the column name off the screen would have found nothing;
* the claim id is in column **C** with a blank column A before it, so
anything that reads positionally from the left is wrong;
* a **sparse row** (eCW omitting an empty cell entirely) must not shift the
columns after it — the one failure mode of parsing cells in order;
* shared strings, which this build does not use but another may;
* an HTML table served with an .xls name, which other eCW builds send.
"""
import io
import sys
import zipfile
import pathlib
import tempfile
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
from ecw_bot import claim_file # noqa: E402
from ecw_bot.core import spreadsheet # noqa: E402
TMP = pathlib.Path(tempfile.mkdtemp())
NS = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
def xlsx(rows, shared=None, name="t.xlsx") -> pathlib.Path:
"""
Write a real xlsx. `rows` is a list of {column letter: cell} dicts.
A cell is either a plain string (inline `t="str"`, what eCW sends) or an
int, meaning an index into `shared` (`t="s"`).
"""
body = []
for r, cells in enumerate(rows, start=1):
parts = []
for letter, value in cells.items():
if isinstance(value, int):
parts.append(f'<c r="{letter}{r}" t="s"><v>{value}</v></c>')
else:
parts.append(f'<c r="{letter}{r}" t="str"><v>{value}</v></c>')
body.append(f'<row r="{r}">{"".join(parts)}</row>')
sheet = (f'<?xml version="1.0"?><worksheet {NS}><sheetData>'
f'{"".join(body)}</sheetData></worksheet>')
path = TMP / name
with zipfile.ZipFile(path, "w") as z:
z.writestr("xl/worksheets/sheet1.xml", sheet)
if shared is not None:
items = "".join(f"<si><t>{s}</t></si>" for s in shared)
z.writestr("xl/sharedStrings.xml",
f'<?xml version="1.0"?><sst {NS}>{items}</sst>')
return path
def html(name="t.xls") -> pathlib.Path:
path = TMP / name
path.write_text(
"<html><body><table>"
"<tr><th></th><th>CLAIMTYPE</th><th>CLAIMS#</th><th>PATIENT</th></tr>"
"<tr><td></td><td>U</td><td>9104000</td><td>Sample, Pat, Q</td></tr>"
"<tr><td></td><td>U</td><td>9101000</td><td>Example, Robin</td></tr>"
"</table></body></html>", encoding="utf-8")
return path
# Column A is blank in eCW's report; the claim id is in C.
REAL = [
{"A": "", "B": "CLAIMTYPE", "C": "CLAIMS#", "D": "SERVICE DATE"},
{"A": "", "B": "U", "C": "9104000", "D": "06/29/2026"},
{"A": "", "B": "U", "C": "9101000", "D": "07/01/2026"},
{"A": "", "B": "U", "C": "9105000", "D": "07/02/2026"},
]
def case_real_shape():
info = claim_file.claim_ids(xlsx(REAL, name="real.xlsx"))
assert info["header"] == "CLAIMS#", info["header"]
assert info["column"] == 3, info["column"]
assert info["ids"] == ["9104000", "9101000", "9105000"], info["ids"]
def case_sparse_row():
"""eCW omitting an empty cell must not shift the columns after it."""
rows = list(REAL) + [{"B": "U", "C": "9102000"}] # no A at all
info = claim_file.claim_ids(xlsx(rows, name="sparse.xlsx"))
assert info["ids"][-1] == "9102000", info["ids"][-1]
assert info["column"] == 3, info["column"]
def case_duplicates_and_blanks():
rows = list(REAL) + [
{"A": "", "B": "U", "C": "9104000"}, # duplicate of row 2
{"A": "", "B": "U", "C": ""}, # blank claim id
]
info = claim_file.claim_ids(xlsx(rows, name="dupes.xlsx"))
assert info["duplicates"] == ["9104000"], info["duplicates"]
assert info["blank_rows"] == 1, info["blank_rows"]
assert len(info["ids"]) == 3, info["ids"]
# Reported, not folded into the total — a count alone hides both.
text = claim_file.summary(info)
assert "1 duplicate(s) skipped" in text, text
assert "1 blank row(s) skipped" in text, text
def case_shared_strings():
"""This build inlines every string, but another may use the table."""
shared = ["", "CLAIMTYPE", "CLAIMS#", "U"]
rows = [{"A": 0, "B": 1, "C": 2},
{"A": 0, "B": 3, "C": "9104000"},
{"A": 0, "B": 3, "C": "9101000"}]
info = claim_file.claim_ids(xlsx(rows, shared=shared, name="shared.xlsx"))
assert info["header"] == "CLAIMS#", info["header"]
assert info["ids"] == ["9104000", "9101000"], info["ids"]
def case_html_export():
"""Other eCW builds serve an HTML table with an .xls name."""
info = claim_file.claim_ids(html())
assert info["header"] == "CLAIMS#", info["header"]
assert info["ids"] == ["9104000", "9101000"], info["ids"]
def case_no_header_falls_back_to_shape():
rows = [{"A": "", "B": "U", "C": str(9104000 + i)} for i in range(8)]
info = claim_file.claim_ids(xlsx(rows, name="noheader.xlsx"))
assert len(info["ids"]) == 8, info["ids"]
assert info["column"] == 3, info["column"]
def case_no_claim_column_is_an_error():
rows = [{"A": "PATIENT", "B": "PAYER"},
{"A": "Sample, Pat", "B": "Medicaid"}]
try:
claim_file.claim_ids(xlsx(rows, name="nocol.xlsx"))
except ValueError as exc:
assert "No claim-id column" in str(exc), exc
return
raise AssertionError("expected a ValueError naming the missing column")
def case_summary_does_not_dump_everything():
"""541 ids in a MessageBox would be a wall of digits. Ends only."""
rows = [{"A": "", "C": "CLAIMS#"}] + \
[{"A": "", "C": str(5500000 + i)} for i in range(541)]
info = claim_file.claim_ids(xlsx(rows, name="big.xlsx"))
assert len(info["ids"]) == 541
text = claim_file.summary(info)
assert text.count("55000") < 30, "summary is dumping the whole list"
assert "541" in text, text
assert info["ids"][0] in text and info["ids"][-1] in text, text
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())