"""TDD: /api/inbox/* endpoints. Plan: docs/superpowers/plans/2026-06-20-cyclone-workflow-automation.md Tasks 7-10 (T7-T10). """ from __future__ import annotations from datetime import datetime, timezone import pytest from fastapi.testclient import TestClient from cyclone import db from cyclone.api import app from cyclone.db import Batch, Claim, ClaimState, Remittance @pytest.fixture(autouse=True) def _setup(tmp_path, monkeypatch): monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/inbox.db") db._reset_for_tests() db.init_db() # Initialize app state used by inbox endpoints. app.state.dismissed_pairs = set() yield @pytest.fixture def client(): with TestClient(app) as c: yield c def _seed_batch() -> None: with db.SessionLocal()() as s: if s.get(Batch, "B-1") is not None: return s.add(Batch( id="B-1", kind="837p", input_filename="x.txt", parsed_at=datetime.now(timezone.utc), totals_json={"total_claims": 1}, validation_json={"passed": True, "warnings": [], "errors": []}, raw_result_json={"_": "stub"}, )) s.commit() def test_lanes_endpoint_returns_four_keys(client: TestClient): r = client.get("/api/inbox/lanes") assert r.status_code == 200 body = r.json() assert set(body.keys()) == {"rejected", "candidates", "unmatched", "done_today"} for v in body.values(): assert isinstance(v, list) def test_match_endpoint_links_remit_to_claim(client: TestClient): _seed_batch() with db.SessionLocal()() as s: s.add(Claim(id="C1", batch_id="B-1", patient_control_number="PCN-1", state=ClaimState.SUBMITTED)) s.add(Remittance(id="R1", batch_id="B-1", payer_claim_control_number="PCN-1", status_code="1", status_label="P", total_charge=100, total_paid=0, received_at=datetime.now(timezone.utc))) s.commit() r = client.post("/api/inbox/candidates/R1/match", json={"claim_id": "C1"}) assert r.status_code == 200, r.text with db.SessionLocal()() as s: c = s.get(Claim, "C1") assert c.matched_remittance_id == "R1" def test_match_409_if_claim_already_matched(client: TestClient): _seed_batch() with db.SessionLocal()() as s: s.add(Claim(id="C1", batch_id="B-1", patient_control_number="X", state=ClaimState.SUBMITTED, matched_remittance_id="R-OTHER")) s.add(Remittance(id="R1", batch_id="B-1", payer_claim_control_number="X", status_code="1", status_label="P", total_charge=100, total_paid=0, received_at=datetime.now(timezone.utc))) s.commit() r = client.post("/api/inbox/candidates/R1/match", json={"claim_id": "C1"}) assert r.status_code == 409 assert "current_state" in r.json()["detail"] def test_dismiss_endpoint_adds_pair_to_session_set(client: TestClient): r = client.post( "/api/inbox/candidates/dismiss", json={"pairs": [{"claim_id": "C1", "remit_id": "R1"}]}, ) assert r.status_code == 200 assert frozenset({"C1", "R1"}) in client.app.state.dismissed_pairs def test_resubmit_moves_rejected_to_submitted_and_increments_count(client: TestClient): _seed_batch() with db.SessionLocal()() as s: s.add(Claim(id="C1", batch_id="B-1", patient_control_number="X", state=ClaimState.REJECTED, rejection_reason="old", resubmit_count=2)) s.commit() r = client.post("/api/inbox/rejected/resubmit", json={"claim_ids": ["C1"]}) assert r.status_code == 200, r.text with db.SessionLocal()() as s: c = s.get(Claim, "C1") assert c.state == ClaimState.SUBMITTED assert c.rejection_reason is None assert c.resubmit_count == 3 def test_resubmit_returns_conflicts_for_non_rejected_claim(client: TestClient): """Resubmit returns 200 with a conflicts list (does NOT raise 409).""" _seed_batch() with db.SessionLocal()() as s: s.add(Claim(id="C1", batch_id="B-1", patient_control_number="X", state=ClaimState.SUBMITTED)) s.commit() r = client.post("/api/inbox/rejected/resubmit", json={"claim_ids": ["C1"]}) assert r.status_code == 200 body = r.json() assert body["resubmitted"] == [] assert any(c["claim_id"] == "C1" for c in body["conflicts"]) def test_export_csv_streams_rows_with_csv_content_type(client: TestClient): r = client.get("/api/inbox/export.csv?lane=rejected") assert r.status_code == 200 assert "text/csv" in r.headers["content-type"] body = r.text assert "id" in body.splitlines()[0]