"""Tests for ``GET /api/dashboard/summary`` (SP10 — Dashboard aggregates).""" 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 JSON = {"Accept": "application/json"} # --------------------------------------------------------------------------- # # Empty state # --------------------------------------------------------------------------- # def test_dashboard_summary_empty_when_no_parses(client: TestClient): resp = client.get("/api/dashboard/summary", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body["kpis"] == { "claimCount": 0, "billedTotal": 0.0, "receivedTotal": 0.0, "outstandingAr": 0.0, "deniedCount": 0, "pendingCount": 0, "denialRate": 0.0, } # Default window is 6 months; with no claims every bucket is empty # but still present so the sparkline renders zeros consistently. assert len(body["monthly"]) == 6 for bucket in body["monthly"]: assert bucket["count"] == 0 assert bucket["billed"] == 0.0 assert bucket["received"] == 0.0 assert bucket["denied"] == 0 assert bucket["ar"] == 0.0 assert bucket["denialRate"] == 0.0 assert body["topProviders"] == [] assert body["recentDenials"] == [] assert body["providerCount"] == 0 # asOf is a UTC ISO string. assert body["asOf"].endswith("Z") # --------------------------------------------------------------------------- # # Populated state — uses the 2-claim co_medicaid fixture # --------------------------------------------------------------------------- # def test_dashboard_summary_after_one_parse(seeded_store): resp = seeded_store.get("/api/dashboard/summary", headers=JSON) assert resp.status_code == 200 body = resp.json() # The fixture has 2 claims, both submitted, billed $85.40 + $155.76. kpis = body["kpis"] assert kpis["claimCount"] == 2 assert kpis["billedTotal"] == pytest.approx(85.40 + 155.76) assert kpis["receivedTotal"] == 0.0 assert kpis["outstandingAr"] == pytest.approx(85.40 + 155.76) assert kpis["deniedCount"] == 0 assert kpis["pendingCount"] == 2 assert kpis["denialRate"] == 0.0 # Default monthly window is 6 months; all claims are in the current # month (parsed today) so the other 5 buckets are zeros. assert len(body["monthly"]) == 6 current = body["monthly"][-1] assert current["count"] == 2 assert current["billed"] == pytest.approx(85.40 + 155.76) assert current["denied"] == 0 # The earlier buckets should be empty (count=0, billed=0). for bucket in body["monthly"][:-1]: assert bucket["count"] == 0 assert bucket["billed"] == 0.0 # One distinct provider (TOC, Inc. — NPI 1881068062). assert body["providerCount"] == 1 assert len(body["topProviders"]) == 1 assert body["topProviders"][0]["npi"] == "1881068062" assert body["topProviders"][0]["claimCount"] == 2 # No denials in this fixture. assert body["recentDenials"] == [] # --------------------------------------------------------------------------- # # months param # --------------------------------------------------------------------------- # def test_dashboard_summary_months_param(client: TestClient): resp = client.get("/api/dashboard/summary?months=3", headers=JSON) assert resp.status_code == 200 assert len(resp.json()["monthly"]) == 3 def test_dashboard_summary_months_param_min_max(client: TestClient): # Floor is 1. r = client.get("/api/dashboard/summary?months=1", headers=JSON) assert r.status_code == 200 assert len(r.json()["monthly"]) == 1 # Ceiling is 24. r = client.get("/api/dashboard/summary?months=24", headers=JSON) assert r.status_code == 200 assert len(r.json()["monthly"]) == 24 # Out of range → 422. r = client.get("/api/dashboard/summary?months=0", headers=JSON) assert r.status_code == 422 r = client.get("/api/dashboard/summary?months=25", headers=JSON) assert r.status_code == 422 # --------------------------------------------------------------------------- # # top_providers param # --------------------------------------------------------------------------- # def test_dashboard_summary_top_providers_param(seeded_store): resp = seeded_store.get( "/api/dashboard/summary?top_providers=1", headers=JSON ) assert resp.status_code == 200 body = resp.json() assert len(body["topProviders"]) == 1 # providerCount is independent of topProviders. assert body["providerCount"] == 1 # --------------------------------------------------------------------------- # # Recent denials only contain denied claims # --------------------------------------------------------------------------- # def test_dashboard_summary_recent_denials_empty_when_no_denied(seeded_store): """Fixture has 2 submitted claims, 0 denied. recentDenials: [].""" body = seeded_store.get("/api/dashboard/summary", headers=JSON).json() assert body["kpis"]["deniedCount"] == 0 assert body["recentDenials"] == [] # --------------------------------------------------------------------------- # # Monthly buckets walk oldest → newest and the running AR is cumulative # --------------------------------------------------------------------------- # def test_dashboard_summary_monthly_chronological_and_labels(seeded_store): body = seeded_store.get("/api/dashboard/summary", headers=JSON).json() months = body["monthly"] assert len(months) == 6 # Labels are short English month names (e.g. "Jan", "Feb"). for bucket in months: assert len(bucket["label"]) == 3 assert bucket["label"][0].isupper() # `month` strings sort in the same order the buckets appear. keys = [b["month"] for b in months] assert keys == sorted(keys) # Running AR never goes negative (clamped at 0). assert all(b["ar"] >= 0 for b in months) # Today's bucket is the last one and has the running AR equal to the # outstandingAr KPI (no received amounts yet in this fixture). assert months[-1]["ar"] == pytest.approx(body["kpis"]["outstandingAr"]) # --------------------------------------------------------------------------- # # Store-level aggregation (called by the endpoint) # --------------------------------------------------------------------------- # def test_store_get_dashboard_summary_directly(): """Call the Store method without going through the API.""" from cyclone.store import store summary = store.get_dashboard_summary(months=6, top_providers=4, denials=5) # All claims come from the seeded fixture if any; we just assert the # shape here so the endpoint and Store stay in sync. assert set(summary.keys()) == { "kpis", "monthly", "topProviders", "recentDenials", "providerCount", "asOf", } assert set(summary["kpis"].keys()) == { "claimCount", "billedTotal", "receivedTotal", "outstandingAr", "deniedCount", "pendingCount", "denialRate", } assert set(summary["monthly"][0].keys()) == { "month", "label", "count", "billed", "received", "denied", "ar", "denialRate", } # Default window is 6 months even when called with explicit defaults. assert len(summary["monthly"]) == 6