feat(backend): add GET /api/batches and GET /api/batches/{id}
This commit is contained in:
@@ -369,4 +369,52 @@ def _ndjson_stream_835(result: ParseResult835) -> Iterator[bytes]:
|
|||||||
yield (json.dumps({"type": "summary", "data": json.loads(result.summary.model_dump_json())}) + "\n").encode("utf-8")
|
yield (json.dumps({"type": "summary", "data": json.loads(result.summary.model_dump_json())}) + "\n").encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# GET endpoints (read views over the in-memory store)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def _batch_summary_claim_count(rec: BatchRecord) -> int:
|
||||||
|
"""Return the number of claims on a batch, handling both 837P and 835."""
|
||||||
|
if rec.kind == "837p":
|
||||||
|
return len(rec.result.claims) # type: ignore[attr-defined]
|
||||||
|
if rec.kind == "835":
|
||||||
|
return len(rec.result.claims) # type: ignore[attr-defined]
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/batches")
|
||||||
|
def list_batches(limit: int = Query(100, ge=1, le=1000)) -> dict[str, Any]:
|
||||||
|
"""Summary of all parsed batches, newest first."""
|
||||||
|
records = store.list(limit=limit)
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"id": r.id,
|
||||||
|
"kind": r.kind,
|
||||||
|
"inputFilename": r.input_filename,
|
||||||
|
"parsedAt": r.parsed_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"claimCount": _batch_summary_claim_count(r),
|
||||||
|
}
|
||||||
|
for r in records
|
||||||
|
]
|
||||||
|
all_records = store.all()
|
||||||
|
return {
|
||||||
|
"items": items,
|
||||||
|
"total": len(all_records),
|
||||||
|
"returned": len(items),
|
||||||
|
"has_more": len(all_records) > len(items),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/batches/{batch_id}")
|
||||||
|
def get_batch(batch_id: str) -> Any:
|
||||||
|
rec = store.get(batch_id)
|
||||||
|
if rec is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail={"error": "Not found", "detail": f"Batch {batch_id} not found"},
|
||||||
|
)
|
||||||
|
return json.loads(rec.result.model_dump_json())
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["app"]
|
__all__ = ["app"]
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""Tests for the 6 new GET endpoints."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from cyclone.api import app
|
||||||
|
from cyclone.store import store as global_store
|
||||||
|
|
||||||
|
FIXTURE_837 = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clear_store():
|
||||||
|
with global_store._lock:
|
||||||
|
global_store._batches.clear()
|
||||||
|
yield
|
||||||
|
with global_store._lock:
|
||||||
|
global_store._batches.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> TestClient:
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def seeded_store():
|
||||||
|
"""One parsed 837P fixture batch."""
|
||||||
|
text = FIXTURE_837.read_text()
|
||||||
|
client = TestClient(app)
|
||||||
|
client.post(
|
||||||
|
"/api/parse-837",
|
||||||
|
files={"file": ("x.txt", text, "text/plain")},
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# /api/batches
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def test_batches_returns_empty_list_when_no_parses(client: TestClient):
|
||||||
|
resp = client.get("/api/batches")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body == {"items": [], "total": 0, "returned": 0, "has_more": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_batches_returns_summary_after_parse(seeded_store):
|
||||||
|
resp = seeded_store.get("/api/batches")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["total"] == 1
|
||||||
|
assert body["returned"] == 1
|
||||||
|
assert body["items"][0]["kind"] == "837p"
|
||||||
|
assert body["items"][0]["inputFilename"] == "x.txt"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# /api/batches/{id}
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_batch_by_id_returns_full_result(seeded_store):
|
||||||
|
batches = seeded_store.get("/api/batches").json()["items"]
|
||||||
|
bid = batches[0]["id"]
|
||||||
|
resp = seeded_store.get(f"/api/batches/{bid}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert "envelope" in body
|
||||||
|
assert "claims" in body
|
||||||
|
assert len(body["claims"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_batch_404_when_missing(client: TestClient):
|
||||||
|
resp = client.get("/api/batches/does-not-exist")
|
||||||
|
assert resp.status_code == 404
|
||||||
Reference in New Issue
Block a user