From 39379d2d5e41a8f0c60258e43a05639da587665b Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 19 Jun 2026 19:25:54 -0600 Subject: [PATCH] feat(backend): add GET /api/batches and GET /api/batches/{id} --- backend/src/cyclone/api.py | 48 ++++++++++++++++++++ backend/tests/test_api_gets.py | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 backend/tests/test_api_gets.py diff --git a/backend/src/cyclone/api.py b/backend/src/cyclone/api.py index c2e59e4..9bcd874 100644 --- a/backend/src/cyclone/api.py +++ b/backend/src/cyclone/api.py @@ -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") +# --------------------------------------------------------------------------- # +# 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"] diff --git a/backend/tests/test_api_gets.py b/backend/tests/test_api_gets.py new file mode 100644 index 0000000..32a2a09 --- /dev/null +++ b/backend/tests/test_api_gets.py @@ -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