128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""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"
|