"""Tests for ``GET /api/remittances/summary`` (server-aggregated KPI backend for the Remittances page tiles). Anomaly recap (see ``docs/superpowers/specs/2026-06-29-cyclone-... -design.md`` if it lands later): * The Remittances page's "Total paid" + "Adjustments" KPI tiles previously summed ``items.reduce(...)`` over the *current page* (25 rows) plus whatever live-tail events had arrived in the session. * For an empty-looking page with most-paid=0 rows, the UI rendered e.g. ``$16,934`` paid / ``$147`` adjustments — neither the page total ($1,334 / $143) nor the true DB total ($227,181 / $13,792). Same silent-incompleteness pattern the count bug retired in ``d81b6ed``: the page looked complete but the numbers were wrong. The fix adds a server-aggregated summary endpoint — ``count + paid + adjustments`` summed over the full persisted row set with the same filter pipeline as ``/api/remittances``. ``Remittances.tsx`` swaps ``items.reduce(...)`` for the new hook so the tiles reflect the true DB population regardless of dataset size. The covered cases match ``test_list_endpoint_counts.py`` / ``test_acks_aggregates.py`` for shape consistency. """ from __future__ import annotations from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest from fastapi.testclient import TestClient from cyclone import db from cyclone.api import app from cyclone.db import Batch, Remittance from cyclone.store import store as global_store @pytest.fixture def client() -> TestClient: return TestClient(app) def _seed_remit( s, remit_id: str, batch_id: str, *, received_at: datetime, total_paid: Decimal = Decimal("100.00"), adjustment_amount: Decimal = Decimal("0"), claim_id: str | None = None, payer_name: str = "Colorado Medicaid", status_code: str = "1", ) -> None: # The remittance's batch row carries ``raw_result_json`` with the # payer name because ``iter_remittances`` reads payer name from # there. We stage a fresh ``Batch`` row per call so unique # ``payer_name`` values for tests like the per-payer filter case # don't collide — the existing pattern from # ``test_list_endpoint_counts.py:88``. s.add(Batch( id=batch_id, kind="835", input_filename=f"{remit_id}.edi", parsed_at=received_at, totals_json={"total_claims": 1}, validation_json={"passed": True, "warnings": [], "errors": []}, raw_result_json={"payer": {"name": payer_name}}, )) s.add(Remittance( id=remit_id, batch_id=batch_id, payer_claim_control_number=remit_id, claim_id=claim_id, status_code=status_code, total_charge=Decimal("100.00"), total_paid=total_paid, adjustment_amount=adjustment_amount, received_at=received_at, )) # --------------------------------------------------------------------------- # # Module-level: summarize_remittances # --------------------------------------------------------------------------- # def test_summarize_remittances_zero_when_empty(): assert global_store.summarize_remittances() == { "count": 0, "total_paid": 0.0, "total_adjustments": 0.0, } def test_summarize_remittances_unfiltered_returns_full_population(): """Regression: with 5 remits seeded, the OLD ``items.reduce(...)`` page-local sum was per-page (25 rows); the new helper must aggregate over all 5.""" base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) with db.SessionLocal()() as s: # 5 remits, paid=10/20/30/40/50 → sum=150, adj all 0. for i, amt in enumerate([10, 20, 30, 40, 50]): _seed_remit( s, f"RMT-SUM-{i:04d}", f"b-sum-pop-{i}", received_at=base + timedelta(minutes=i), total_paid=Decimal(amt), adjustment_amount=Decimal("0"), ) s.commit() assert global_store.summarize_remittances() == { "count": 5, "total_paid": 150.0, "total_adjustments": 0.0, } def test_summarize_remittances_sums_adjustments_across_rows(): """Adjustments accumulate independently from paid.""" base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) with db.SessionLocal()() as s: _seed_remit( s, "RMT-A", "b-sum-adj-A", received_at=base, total_paid=Decimal("25.96"), adjustment_amount=Decimal("16.87"), ) _seed_remit( s, "RMT-B", "b-sum-adj-B", received_at=base + timedelta(minutes=1), total_paid=Decimal("0"), adjustment_amount=Decimal("50.19"), ) _seed_remit( s, "RMT-C", "b-sum-adj-C", received_at=base + timedelta(minutes=2), total_paid=Decimal("0"), adjustment_amount=Decimal("9.95"), ) s.commit() assert global_store.summarize_remittances() == { "count": 3, "total_paid": 25.96, "total_adjustments": 77.01, # 16.87 + 50.19 + 9.95 } def test_summarize_remittances_filters_by_claim_id(): """claim_id filter narrows the summary the same way it narrows iter_remittances (DB-side WHERE remittance.claim_id = :claim_id). """ base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) with db.SessionLocal()() as s: _seed_remit( s, "RMT-X", "b-sum-cid-X", received_at=base, claim_id="CLM-1", total_paid=Decimal("10"), adjustment_amount=Decimal("0"), ) _seed_remit( s, "RMT-Y", "b-sum-cid-Y", received_at=base, claim_id="CLM-2", total_paid=Decimal("20"), adjustment_amount=Decimal("1"), ) _seed_remit( s, "RMT-Z", "b-sum-cid-Z", received_at=base, claim_id=None, total_paid=Decimal("30"), adjustment_amount=Decimal("2"), ) s.commit() assert global_store.summarize_remittances(claim_id="CLM-1") == { "count": 1, "total_paid": 10.0, "total_adjustments": 0.0, } assert global_store.summarize_remittances(claim_id="CLM-2") == { "count": 1, "total_paid": 20.0, "total_adjustments": 1.0, } assert global_store.summarize_remittances(claim_id="CLM-missing") == { "count": 0, "total_paid": 0.0, "total_adjustments": 0.0, } def test_summarize_remittances_filters_by_payer_exact_match(): """``payer`` filter is in-memory + case-sensitive exact match — the helper must apply it the same way ``iter_remittances`` does so summary and list views agree under the same chip.""" base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) with db.SessionLocal()() as s: _seed_remit( s, "RMT-CO-1", "b-sum-payer-co-1", received_at=base, payer_name="Colorado Medicaid", total_paid=Decimal("7"), adjustment_amount=Decimal("0.50"), ) _seed_remit( s, "RMT-CO-2", "b-sum-payer-co-2", received_at=base + timedelta(minutes=1), payer_name="Colorado Medicaid", total_paid=Decimal("13"), adjustment_amount=Decimal("0.25"), ) _seed_remit( s, "RMT-AZ-1", "b-sum-payer-az-1", received_at=base + timedelta(minutes=2), payer_name="Arizona Medicaid", total_paid=Decimal("99"), adjustment_amount=Decimal("0"), ) s.commit() assert global_store.summarize_remittances(payer="Colorado Medicaid") == { "count": 2, "total_paid": 20.0, "total_adjustments": 0.75, } assert global_store.summarize_remittances(payer="Arizona Medicaid") == { "count": 1, "total_paid": 99.0, "total_adjustments": 0.0, } assert global_store.summarize_remittances(payer="colorado medicaid") == { "count": 0, "total_paid": 0.0, "total_adjustments": 0.0, } # --------------------------------------------------------------------------- # # HTTP: /api/remittances/summary # --------------------------------------------------------------------------- # def test_api_remittances_summary_empty(client: TestClient): """Empty DB → zero totals, well-formed response.""" resp = client.get("/api/remittances/summary") assert resp.status_code == 200, resp.text assert resp.json() == { "count": 0, "total_paid": 0, "total_adjustments": 0, } def test_api_remittances_summary_full_population(client: TestClient): """Bug repro: seed 5 remits with known amounts, assert the endpoint reports the FULL sum, not a 25-row sample. """ base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) with db.SessionLocal()() as s: for i, amt in enumerate([100, 200, 300, 400, 500]): _seed_remit( s, f"RMT-HTTP-SUM-{i}", f"b-http-sum-{i}", received_at=base + timedelta(minutes=i), total_paid=Decimal(amt), adjustment_amount=Decimal("5"), ) s.commit() resp = client.get("/api/remittances/summary") assert resp.status_code == 200, resp.text body = resp.json() assert body == { "count": 5, "total_paid": 1500, # 100+200+300+400+500 "total_adjustments": 25, # 5*5 } def test_api_remittances_summary_respects_claim_id_filter(client: TestClient): """Filter narrows both count and sums.""" base = datetime(2026, 6, 1, 12, 0, tzinfo=timezone.utc) with db.SessionLocal()() as s: _seed_remit( s, "RMT-F-1", "b-http-cid-1", received_at=base, claim_id="CLM-F1", total_paid=Decimal("11"), adjustment_amount=Decimal("1"), ) _seed_remit( s, "RMT-F-2", "b-http-cid-2", received_at=base, claim_id="CLM-F2", total_paid=Decimal("22"), adjustment_amount=Decimal("2"), ) s.commit() resp = client.get( "/api/remittances/summary", params={"claim_id": "CLM-F1"}, ) assert resp.status_code == 200 assert resp.json() == { "count": 1, "total_paid": 11, "total_adjustments": 1, }