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>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
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())
|
||||
Reference in new issue
Block a user