wip: dashboard wiring (real backend) + Docker compose for production

This commit is contained in:
Nora
2026-06-22 14:07:31 -06:00
parent 4a382c0b16
commit 0677e4fd65
18 changed files with 1879 additions and 155 deletions
+230
View File
@@ -0,0 +1,230 @@
"""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