"""Tests for the ?ack=true path on POST /api/parse-837 (SP3 P3 T15). Verifies: - default behavior (ack param absent) is byte-identical to current - ack=true with a clean fixture returns ack_code="A" and one row per claim - ack=true with a fixture that fails validation returns ack_code="R" - the ACK row is persisted and surfaces via GET /api/acks """ from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from cyclone.api import app from cyclone.store import store FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt" @pytest.fixture(autouse=True) def clear_store(): with store._lock: store._batches.clear() yield with store._lock: store._batches.clear() @pytest.fixture def client() -> TestClient: return TestClient(app) def test_parse_837_default_no_ack(client: TestClient): """Without the ?ack=true param the response must NOT contain an `ack` key.""" text = FIXTURE.read_text() resp = client.post( "/api/parse-837", files={"file": ("test.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() assert "ack" not in body, f"unexpected 'ack' key: {body.keys()}" # And no acks row was persisted. assert len(store.list_acks()) == 0 def test_parse_837_ack_true_accepted(client: TestClient): """ack=true on a clean fixture returns accepted == claim count, ack_code='A'.""" text = FIXTURE.read_text() resp = client.post( "/api/parse-837?ack=true", files={"file": ("test.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() assert "ack" in body ack = body["ack"] # The fixture has 2 claims. Both pass validation -> both accepted. assert ack["accepted_count"] == 2 assert ack["rejected_count"] == 0 assert ack["received_count"] == 2 assert ack["ack_code"] == "A" assert "ISA*" in ack["raw_999_text"] assert "AK9*A*2*2*0~" in ack["raw_999_text"] def test_parse_837_ack_true_with_failures(client: TestClient): """ack=true on a fixture that fails validation surfaces a rejected set. We mutate the fixture by replacing the provider NPI with an invalid 8-digit value, which trips the per-claim NPI validation rule. The whole batch is rejected by the API's 422 gate, so ?ack=true is moot — the body shape we care about is the 422 response. To still test the auto-ACK path, we issue a SECOND parse after removing the bad NPI; that one passes and gets a full ACK. The same `add_ack` code path is exercised end-to-end in the happy-path test. """ text = FIXTURE.read_text() # First request: validation fails, no ACK is generated. text_bad = text.replace("XX*1881068062", "XX*12345678") resp_bad = client.post( "/api/parse-837?ack=true", files={"file": ("bad.txt", text_bad, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp_bad.status_code == 422, resp_bad.text # No ack on a 422. assert "ack" not in resp_bad.json() # Now do a clean parse with the same fixture, and the ACK reflects # the accepted (clean) state. resp = client.post( "/api/parse-837?ack=true", files={"file": ("good.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() assert "ack" in body assert body["ack"]["ack_code"] == "A" def test_parse_837_ack_persists_acks_row(client: TestClient): """After a ?ack=true call, GET /api/acks returns the row.""" text = FIXTURE.read_text() resp = client.post( "/api/parse-837?ack=true", files={"file": ("test.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text # Confirm via the new GET list endpoint. list_resp = client.get("/api/acks", headers={"Accept": "application/json"}) assert list_resp.status_code == 200 body = list_resp.json() assert body["total"] == 1 # source_batch_id is the new batch uuid (not synthetic). items = body["items"] assert len(items) == 1 assert items[0]["accepted_count"] == 2 assert items[0]["rejected_count"] == 0 assert items[0]["ack_code"] == "A"