From 7a20f732f20b68066be201173a51136dc7611d2e Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 20 Jun 2026 07:49:20 -0600 Subject: [PATCH] fix(api): include adjustments array in /api/remittances list response (SP3 P2 follow-up) The P2 subagent shipped a UI chevron+expansion in Remittances.tsx that read adjustments from the list payload, but iter_remittances() didn't include the CasAdjustment rows. Result: the expansion was always empty when a remittance was opened from the list (the detail endpoint worked). Fix: bulk-fetch all CasAdjustment rows for the remittance_ids in one query, then attach them as the 'adjustments' array. N+1-free. --- backend/src/cyclone/store.py | 25 +++++++++++++++++++++++ backend/tests/test_api_gets.py | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/backend/src/cyclone/store.py b/backend/src/cyclone/store.py index 09be4e7..276771c 100644 --- a/backend/src/cyclone/store.py +++ b/backend/src/cyclone/store.py @@ -971,6 +971,20 @@ class CycloneStore: q = q.filter(Remittance.claim_id == claim_id) rows = q.all() + # Bulk-fetch all CAS rows for these remittances in one query + # (SP3 P2 follow-up — fixes the list-view's empty adjustments + # expansion). N+1-free. + cas_by_remit: dict[str, list] = {} + if rows: + from cyclone.parsers.cas_codes import reason_label + cas_rows = ( + s.query(CasAdjustment) + .filter(CasAdjustment.remittance_id.in_([r.id for r in rows])) + .all() + ) + for c in cas_rows: + cas_by_remit.setdefault(c.remittance_id, []).append(c) + out: list[dict] = [] for r in rows: raw = r.raw_json or {} @@ -984,6 +998,16 @@ class CycloneStore: payer_name = ( r.batch.raw_result_json.get("payer", {}).get("name", "") ) + adjustments = [ + { + "group": c.group_code, + "reason": c.reason_code, + "label": reason_label(c.group_code, c.reason_code), + "amount": float(c.amount), + "quantity": float(c.quantity) if c.quantity is not None else None, + } + for c in cas_by_remit.get(r.id, []) + ] out.append({ "id": r.id, "claimId": r.claim_id or "", @@ -999,6 +1023,7 @@ class CycloneStore: "receivedDate": r.received_at.isoformat().replace("+00:00", "Z"), "batchId": r.batch_id, "parsedAt": parsed_at_iso, + "adjustments": adjustments, "_sort_receivedDate": r.received_at.isoformat().replace("+00:00", "Z"), }) diff --git a/backend/tests/test_api_gets.py b/backend/tests/test_api_gets.py index f70b884..f7f7cd2 100644 --- a/backend/tests/test_api_gets.py +++ b/backend/tests/test_api_gets.py @@ -149,6 +149,43 @@ def test_remittances_filters_by_batch_id(seeded_store): assert resp.json()["items"] == [] +# --------------------------------------------------------------------------- # +# /api/remittances list payload includes adjustments array (SP3 P2 follow-up) +# --------------------------------------------------------------------------- # + + +def test_list_remittances_includes_adjustments_array(client: TestClient) -> None: + """SP3 P2 follow-up: the list endpoint must include `adjustments` so the + UI's expandable row has data when a remittance is opened from the list + (not just from the detail endpoint).""" + _seed_835_remittance( + pcns=["PCN-LIST-1"], + cas_rows={"PCN-LIST-1": [("CO", "45", "50.00", ""), ("PR", "1", "10.00", "")]}, + ) + resp = client.get("/api/remittances", headers=JSON) + assert resp.status_code == 200 + body = resp.json() + assert len(body["items"]) == 1 + item = body["items"][0] + assert "adjustments" in item, item + assert len(item["adjustments"]) == 2 + # Verify labels come through (resolved by cas_codes.reason_label). + labels = {a["reason"]: a["label"] for a in item["adjustments"]} + assert "45" in labels and "1" in labels + assert labels["45"] # non-empty + assert labels["1"] + + +def test_list_remittances_empty_adjustments_is_empty_array(client: TestClient) -> None: + """A remittance with no CAS rows must still expose `adjustments: []`.""" + _seed_835_remittance(pcns=["PCN-LIST-EMPTY"], cas_rows=None) + resp = client.get("/api/remittances", headers=JSON) + assert resp.status_code == 200 + body = resp.json() + assert len(body["items"]) == 1 + assert body["items"][0]["adjustments"] == [] + + # --------------------------------------------------------------------------- # # /api/providers # --------------------------------------------------------------------------- #