"""Successful parses must persist to the store; failed parses must not.""" 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 as global_store FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt" FIXTURE_835 = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt" @pytest.fixture(autouse=True) def clear_store(): """Reset the module-level store before and after each test.""" with global_store._lock: global_store._batches.clear() yield with global_store._lock: global_store._batches.clear() @pytest.fixture def client() -> TestClient: return TestClient(app) def test_successful_parse_837_creates_batch(client: TestClient): assert len(global_store.list()) == 0 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 assert len(global_store.list()) == 1 rec = global_store.list()[0] assert rec.kind == "837p" assert rec.input_filename == "test.txt" def test_failed_parse_837_does_not_create_batch(client: TestClient): """An empty / garbage file should NOT create a batch.""" assert len(global_store.list()) == 0 resp = client.post( "/api/parse-837", files={"file": ("garbage.txt", "not-edi", "text/plain")}, headers={"Accept": "application/json"}, ) # Either a 4xx from the parser, or a 200 with 0 claims — the contract # is that NO batch is added either way. assert resp.status_code in (200, 400, 422) assert len(global_store.list()) == 0 def test_validation_failed_837_does_not_create_batch(client: TestClient): """A parse that produced claims with validation errors (422) must not be stored. The spec only mandates "do not store on CycloneParseError or unhandled exception"; we choose the stricter interpretation that 422 validation failures are also excluded so the data on disk is always clean. """ # Construct a minimal but technically malformed 837P that the parser # will accept but validation will reject. We inject a wrong NPI # length to trigger the per-claim NPI validation rule. text = FIXTURE.read_text() # Replace the provider NPI (XX*1881068062) with an invalid 8-digit NPI # (the rule is NPI must be 10 digits). This keeps the parse step # green while failing claim validation. text = text.replace("XX*1881068062", "XX*12345678") resp = client.post( "/api/parse-837", files={"file": ("bad.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 422, resp.text assert len(global_store.list()) == 0 def test_parse_835_response_includes_reconciliation_summary( client: TestClient, tmp_path: Path ): """A successful 835 parse returns matched/unmatched counts in JSON.""" fixture = FIXTURE_835.read_text() p = tmp_path / "era.txt" p.write_text(fixture) with open(p, "rb") as f: r = client.post( "/api/parse-835", files={"file": ("era.txt", f, "text/plain")}, headers={"Accept": "application/json"}, ) assert r.status_code == 200 body = r.json() assert "reconciliation" in body, f"missing reconciliation: {list(body.keys())}" rec = body["reconciliation"] assert "matched" in rec assert "unmatched_claims" in rec assert "unmatched_remittances" in rec assert "skipped" in rec