feat(backend): parse-835 JSON response includes reconciliation summary

This commit is contained in:
Tyler
2026-06-19 23:42:22 -06:00
parent 44fe88d694
commit 43a88a356c
2 changed files with 61 additions and 1 deletions
+37 -1
View File
@@ -303,6 +303,40 @@ def _has_835_validation_errors(result: ParseResult835) -> bool:
return result.validation is not None and not result.validation.passed
def _reconciliation_summary_for_batch(batch_id: str) -> dict:
"""Return ``{matched, unmatched_claims, unmatched_remittances, skipped}`` for a batch.
Reads from the DB after ``store.add()`` has already triggered T10
reconciliation synchronously (via ``_run_reconcile``). Counts are
observed at this moment; a subsequent manual match/unmatch will not
be reflected until the next request.
``skipped`` is reserved for future use — the T10 orchestrator tracks
skipped claims internally but does not surface a queryable count.
"""
from sqlalchemy import func, select
from cyclone import db
from cyclone.db import Match, Remittance
with db.SessionLocal()() as s:
matched = s.execute(
select(func.count(Match.id)).where(
Match.remittance_id.in_(
select(Remittance.id).where(Remittance.batch_id == batch_id)
)
)
).scalar_one()
# Pull unmatched via the store (small result set; cheap).
unmatched = store.list_unmatched(kind="both")
return {
"matched": matched,
"unmatched_claims": len(unmatched["claims"]),
"unmatched_remittances": len(unmatched["remittances"]),
"skipped": 0, # reserved — T10 does not persist a skipped count
}
@app.post("/api/parse-835")
async def parse_835_endpoint(
request: Request,
@@ -384,7 +418,9 @@ async def parse_835_endpoint(
store.add(rec)
if _client_wants_json(request):
return JSONResponse(content=json.loads(result.model_dump_json()))
body = json.loads(result.model_dump_json())
body["reconciliation"] = _reconciliation_summary_for_batch(rec.id)
return JSONResponse(content=body)
# Default: NDJSON stream.
return StreamingResponse(
+24
View File
@@ -11,6 +11,7 @@ from cyclone.api import app
from cyclone.store import store as global_store
FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"
FIXTURE_835 = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt"
@pytest.fixture(autouse=True)
@@ -79,3 +80,26 @@ def test_validation_failed_837_does_not_create_batch(client: TestClient):
)
assert resp.status_code == 422, resp.text
assert len(global_store.list()) == 0
def test_parse_835_response_includes_reconciliation_summary(
client: TestClient, tmp_path: Path
):
"""A successful 835 parse returns matched/unmatched counts in JSON."""
fixture = FIXTURE_835.read_text()
p = tmp_path / "era.txt"
p.write_text(fixture)
with open(p, "rb") as f:
r = client.post(
"/api/parse-835",
files={"file": ("era.txt", f, "text/plain")},
headers={"Accept": "application/json"},
)
assert r.status_code == 200
body = r.json()
assert "reconciliation" in body, f"missing reconciliation: {list(body.keys())}"
rec = body["reconciliation"]
assert "matched" in rec
assert "unmatched_claims" in rec
assert "unmatched_remittances" in rec
assert "skipped" in rec