feat(backend): parse-835 JSON response includes reconciliation summary
This commit is contained in:
@@ -303,6 +303,40 @@ def _has_835_validation_errors(result: ParseResult835) -> bool:
|
|||||||
return result.validation is not None and not result.validation.passed
|
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")
|
@app.post("/api/parse-835")
|
||||||
async def parse_835_endpoint(
|
async def parse_835_endpoint(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -384,7 +418,9 @@ async def parse_835_endpoint(
|
|||||||
store.add(rec)
|
store.add(rec)
|
||||||
|
|
||||||
if _client_wants_json(request):
|
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.
|
# Default: NDJSON stream.
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from cyclone.api import app
|
|||||||
from cyclone.store import store as global_store
|
from cyclone.store import store as global_store
|
||||||
|
|
||||||
FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"
|
FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"
|
||||||
|
FIXTURE_835 = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@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 resp.status_code == 422, resp.text
|
||||||
assert len(global_store.list()) == 0
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user