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>
190 lines
7.1 KiB
Python
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())
|