Files
cyclone/backend/tests/test_iter_claims_received.py
Nora add6e982a4 Populate receivedAmount from matched Remittance.total_paid
Before this change, every claim — matched or not — came back from
`/api/claims` with `receivedAmount: 0.0`. `to_ui_claim_from_orm`
hardcoded the value, so the Dashboard's 'Received' KPI and the
claim detail drawer's paid amount always read $0 regardless of
whether the claim had been paired with a paying remittance.

The fix threads the real value through the read path:

- `to_ui_claim_from_orm` accepts a new `received_total: float`
  kwarg (default 0.0 for callers that don't have a remittance in
  scope).
- `iter_claims` bulk-loads `Remittance.total_paid` for every
  matched claim id in the result set (single SQL roundtrip via
  `IN` filter — no N+1) and stamps the sum onto each claim dict.
- `manual_match` passes `float(remit.total_paid)` from the remit
  it's already holding.
- `manual_unmatch` reads `paired_remit.total_paid` before clearing
  the FK so the response still reports what was paid pre-unpair.
- `add` (write path) and `list_unmatched` (filters matched-remit
  is NULL) pass `0.0` explicitly for readability.

The dev seed CLI now also generates matched Remittance rows for
every PAID/PARTIAL claim — `matched_remittance_id` is set on the
claim, and the remittance's `total_paid` is derived from the
billed amount using the same ratios the frontend's old sample
fixtures used (60–100% for PAID, 20–50% for PARTIAL). That gives
the Dashboard a non-zero 'Received' KPI on a fresh dev DB.

New tests:
- tests/test_iter_claims_received.py — 4 cases covering matched,
  unmatched, orphan FK, and bulk-load paths. Catches regressions
  if anyone re-introduces the hardcoded 0.0 or breaks the bulk
  query.
2026-06-22 17:46:47 -06:00

134 lines
4.8 KiB
Python

"""Tests that ``CycloneStore.iter_claims`` populates ``receivedAmount``
from the matched ``Remittance.total_paid``.
Before SP_Auth follow-up: every claim came back with
``receivedAmount: 0.0`` regardless of whether it had been paired with
a paid remittance, so the Dashboard's "Received" KPI was always $0
even when claims had been paid and reconciled.
The fix: ``iter_claims`` bulk-loads ``Remittance.total_paid`` for every
matched claim id in the result set (single SQL, no N+1) and stamps
the sum onto each claim dict.
"""
from __future__ import annotations
import json
from datetime import date, datetime, timezone
from decimal import Decimal
import pytest
from cyclone import db
from cyclone.db import ActivityEvent, Batch, Claim, ClaimState, Remittance
from cyclone.store import store as global_store
@pytest.fixture(autouse=True)
def _setup(tmp_path, monkeypatch):
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
db._reset_for_tests()
db.init_db()
def _make_batch(s, batch_id: str) -> None:
s.add(Batch(
id=batch_id,
kind="837p",
input_filename="seed.edi",
parsed_at=datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc),
totals_json={"total_claims": 3},
validation_json={"passed": True, "warnings": [], "errors": []},
raw_result_json={"_": "stub"},
))
def _make_claim(s, claim_id: str, batch_id: str, *, matched_remit_id: str | None = None) -> None:
s.add(Claim(
id=claim_id,
batch_id=batch_id,
patient_control_number=claim_id,
service_date_from=date(2026, 6, 1),
service_date_to=date(2026, 6, 1),
charge_amount=Decimal("200.00"),
provider_npi="1234567890",
payer_id="SKCO0",
state=ClaimState.PAID,
matched_remittance_id=matched_remit_id,
))
def _make_remit(s, remit_id: str, batch_id: str, *, total_paid: Decimal) -> None:
s.add(Remittance(
id=remit_id,
batch_id=batch_id,
payer_claim_control_number=remit_id,
claim_id=None,
status_code="1",
total_charge=Decimal("200.00"),
total_paid=total_paid,
adjustment_amount=Decimal("0"),
received_at=datetime(2026, 6, 20, 12, 0, tzinfo=timezone.utc),
))
def test_iter_claims_populates_received_amount_from_matched_remittance():
"""A matched claim should reflect its remittance's ``total_paid``."""
with db.SessionLocal()() as s:
_make_batch(s, "b1")
_make_remit(s, "r1", "b1", total_paid=Decimal("180.00"))
_make_claim(s, "CLM-A", "b1", matched_remit_id="r1")
s.commit()
items = global_store.iter_claims(limit=10)
by_id = {c["id"]: c for c in items}
assert by_id["CLM-A"]["receivedAmount"] == pytest.approx(180.00)
def test_iter_claims_unmatched_claim_has_zero_received():
"""Claims with no matched remittance still get 0.0, not stale data."""
with db.SessionLocal()() as s:
_make_batch(s, "b1")
_make_claim(s, "CLM-U", "b1", matched_remit_id=None)
s.commit()
items = global_store.iter_claims(limit=10)
by_id = {c["id"]: c for c in items}
assert by_id["CLM-U"]["receivedAmount"] == 0.0
def test_iter_claims_handles_orphan_match_fk():
"""A claim with a stale ``matched_remittance_id`` whose remittance row
was deleted should default to 0.0 rather than blow up."""
with db.SessionLocal()() as s:
_make_batch(s, "b1")
_make_claim(s, "CLM-ORPHAN", "b1", matched_remit_id="r-deleted")
s.commit()
# No remittance row exists — the FK is dangling, which can happen
# if the remittance was deleted between match and now.
items = global_store.iter_claims(limit=10)
by_id = {c["id"]: c for c in items}
assert by_id["CLM-ORPHAN"]["receivedAmount"] == 0.0
def test_iter_claims_bulk_loads_multiple_matches_in_one_pass():
"""All matched claims in a page reflect their distinct remittance
totals — the bulk load must aggregate per remittance id."""
with db.SessionLocal()() as s:
_make_batch(s, "b1")
_make_remit(s, "r1", "b1", total_paid=Decimal("120.00"))
_make_remit(s, "r2", "b1", total_paid=Decimal("175.50"))
_make_remit(s, "r3", "b1", total_paid=Decimal("0"))
_make_claim(s, "CLM-1", "b1", matched_remit_id="r1")
_make_claim(s, "CLM-2", "b1", matched_remit_id="r2")
_make_claim(s, "CLM-3", "b1", matched_remit_id="r3")
_make_claim(s, "CLM-4", "b1", matched_remit_id=None)
s.commit()
items = global_store.iter_claims(limit=10)
by_id = {c["id"]: c for c in items}
assert by_id["CLM-1"]["receivedAmount"] == pytest.approx(120.00)
assert by_id["CLM-2"]["receivedAmount"] == pytest.approx(175.50)
assert by_id["CLM-3"]["receivedAmount"] == pytest.approx(0.0)
assert by_id["CLM-4"]["receivedAmount"] == 0.0