"""SP14 — Payer-Rejected acknowledge endpoint tests. The endpoint marks payer-rejected claims as acknowledged so the working-surface lane query filters them out. The original payer_rejected_* fields stay intact (SP11 audit). """ from __future__ import annotations import pytest from cyclone import db from cyclone.db import Claim, ClaimState from cyclone.audit_log import verify_chain @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_claim(claim_id: str, *, payer_rejected: bool = True) -> None: """Insert a minimal claim with optional payer_rejected markers.""" from datetime import datetime, timezone from cyclone.db import Batch with db.SessionLocal()() as session: batch = Batch( id=f"b-{claim_id}", kind="837p", input_filename="t.x12", parsed_at=datetime.now(timezone.utc), ) session.add(batch) session.flush() claim = Claim( id=claim_id, batch_id=batch.id, patient_control_number=claim_id, state=ClaimState.SUBMITTED, charge_amount=100, ) if payer_rejected: now = datetime.now(timezone.utc) claim.payer_rejected_at = now claim.payer_rejected_reason = "invalid diagnosis code" claim.payer_rejected_status_code = "A7" session.add(claim) session.commit() def test_acknowledge_marks_unacknowledged_claim(): _make_claim("C1", payer_rejected=True) from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": ["C1"], "actor": "operator"}, ) assert r.status_code == 200, r.text body = r.json() assert body["ok"] is True assert body["transitioned"] == 1 assert body["already_acked"] == 0 assert body["not_found"] == 0 assert body["not_rejected"] == 0 with db.SessionLocal()() as session: c = session.get(Claim, "C1") assert c.payer_rejected_acknowledged_at is not None assert c.payer_rejected_acknowledged_actor == "operator" # Original fields stay intact for audit. assert c.payer_rejected_at is not None assert c.payer_rejected_status_code == "A7" assert c.payer_rejected_reason == "invalid diagnosis code" def test_acknowledge_is_idempotent(): _make_claim("C1", payer_rejected=True) from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r1 = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": ["C1"]}, ) assert r1.json()["transitioned"] == 1 r2 = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": ["C1"]}, ) assert r2.json()["transitioned"] == 0 assert r2.json()["already_acked"] == 1 def test_acknowledge_skips_non_payer_rejected_claims(): _make_claim("C1", payer_rejected=False) from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": ["C1"]}, ) assert r.status_code == 200 body = r.json() assert body["transitioned"] == 0 assert body["not_rejected"] == 1 def test_acknowledge_counts_missing_ids(): from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": ["nope"]}, ) assert r.status_code == 200 body = r.json() assert body["transitioned"] == 0 assert body["not_found"] == 1 def test_acknowledge_empty_claim_ids_400(): from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": []}, ) assert r.status_code == 400 def test_acknowledge_writes_audit_event(): _make_claim("C1", payer_rejected=True) from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r = client.post( "/api/inbox/payer-rejected/acknowledge", json={"claim_ids": ["C1"], "actor": "alice"}, ) assert r.status_code == 200 # SP11: the chain is intact and includes the new event. with db.SessionLocal()() as session: result = verify_chain(session) assert result.ok, f"chain broken: {result.reason} at {result.first_bad_id}" # And the event itself is queryable. import json as _json from cyclone.db import AuditLog with db.SessionLocal()() as session: events = ( session.query(AuditLog) .filter(AuditLog.event_type == "claim.payer_rejected_acknowledged") .all() ) assert len(events) == 1 e = events[0] assert e.entity_type == "claim" assert e.entity_id == "C1" assert e.actor == "alice" payload = _json.loads(e.payload_json) if e.payload_json else {} assert payload["payer_rejected_status_code"] == "A7"