231 lines
8.0 KiB
Python
231 lines
8.0 KiB
Python
"""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
|