From cbffdbd2af14377a6ddfdc2b1835470fedccc5c2 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 19 Jun 2026 19:26:58 -0600 Subject: [PATCH] feat(backend): add GET /api/{remittances,providers,activity} --- backend/src/cyclone/api.py | 71 ++++++++++++++++++++++++++++++++++ backend/tests/test_api_gets.py | 59 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/backend/src/cyclone/api.py b/backend/src/cyclone/api.py index eeb5ccc..f214d30 100644 --- a/backend/src/cyclone/api.py +++ b/backend/src/cyclone/api.py @@ -450,4 +450,75 @@ def list_claims( } +@app.get("/api/remittances") +def list_remittances( + batch_id: str | None = Query(None), + payer: str | None = Query(None), + claim_id: str | None = Query(None), + date_from: str | None = Query(None), + date_to: str | None = Query(None), + sort: str | None = Query(None), + order: str = Query("desc"), + limit: int = Query(100, ge=1, le=1000), + offset: int = Query(0, ge=0), +) -> dict[str, Any]: + common = dict( + batch_id=batch_id, + payer=payer, + claim_id=claim_id, + date_from=date_from, + date_to=date_to, + ) + items = list(store.iter_remittances( + sort=sort, order=order, limit=limit, offset=offset, **common, + )) + total = len(list(store.iter_remittances(**common))) + return { + "items": items, + "total": total, + "returned": len(items), + "has_more": total > offset + len(items), + } + + +@app.get("/api/providers") +def list_providers( + npi: str | None = Query(None), + state: str | None = Query(None), + limit: int = Query(100, ge=1, le=1000), + offset: int = Query(0, ge=0), +) -> dict[str, Any]: + items = store.distinct_providers() + if npi is not None: + items = [p for p in items if p["npi"] == npi] + if state is not None: + items = [p for p in items if p.get("state") == state] + paged = items[offset:offset + limit] + return { + "items": paged, + "total": len(items), + "returned": len(paged), + "has_more": len(items) > offset + len(paged), + } + + +@app.get("/api/activity") +def list_activity( + kind: str | None = Query(None), + since: str | None = Query(None), + limit: int = Query(200, ge=1, le=500), +) -> dict[str, Any]: + events = store.recent_activity(limit=limit) + if kind is not None: + events = [e for e in events if e["kind"] == kind] + if since is not None: + events = [e for e in events if e["timestamp"] >= since] + return { + "items": events, + "total": len(events), + "returned": len(events), + "has_more": False, + } + + __all__ = ["app"] diff --git a/backend/tests/test_api_gets.py b/backend/tests/test_api_gets.py index c18253c..9c87e44 100644 --- a/backend/tests/test_api_gets.py +++ b/backend/tests/test_api_gets.py @@ -125,3 +125,62 @@ def test_claims_pagination_limit_offset(seeded_store): body1 = resp1.json() assert body1["returned"] == 1 assert body1["has_more"] is True + + +# --------------------------------------------------------------------------- # +# /api/remittances +# --------------------------------------------------------------------------- # + + +def test_remittances_returns_empty_list_when_no_parses(client: TestClient): + resp = client.get("/api/remittances") + assert resp.status_code == 200 + body = resp.json() + assert body["items"] == [] + + +def test_remittances_filters_by_batch_id(seeded_store): + """No 835 fixture parsed yet — but the endpoint should return 200 with empty.""" + resp = seeded_store.get("/api/remittances?batch_id=nope") + assert resp.status_code == 200 + assert resp.json()["items"] == [] + + +# --------------------------------------------------------------------------- # +# /api/providers +# --------------------------------------------------------------------------- # + + +def test_providers_empty_when_no_parses(client: TestClient): + resp = client.get("/api/providers") + assert resp.status_code == 200 + body = resp.json() + assert body["items"] == [] + + +def test_providers_returns_one_per_distinct_npi(seeded_store): + resp = seeded_store.get("/api/providers") + assert resp.status_code == 200 + body = resp.json() + assert len(body["items"]) == 1 + assert body["items"][0]["npi"] == "1881068062" + + +# --------------------------------------------------------------------------- # +# /api/activity +# --------------------------------------------------------------------------- # + + +def test_activity_empty_when_no_parses(client: TestClient): + resp = client.get("/api/activity") + assert resp.status_code == 200 + body = resp.json() + assert body["items"] == [] + + +def test_activity_returns_one_event_per_claim(seeded_store): + resp = seeded_store.get("/api/activity") + assert resp.status_code == 200 + body = resp.json() + assert len(body["items"]) == 2 # co_medicaid_837p.txt has 2 claims + assert all(e["kind"] == "claim_submitted" for e in body["items"])