"""Tests for the FastAPI surface in ``cyclone.api`` for the 270/271 eligibility endpoints (SP3 P4 T23 + T24). """ from __future__ import annotations from datetime import date from pathlib import Path import pytest from fastapi.testclient import TestClient from cyclone.api import app from cyclone.parsers.parse_270 import parse as parse_270_text MINIMAL_271 = Path(__file__).parent / "fixtures" / "minimal_271.txt" @pytest.fixture def client() -> TestClient: return TestClient(app) def _valid_request_body() -> dict: return { "subscriber": { "first_name": "JOHN", "last_name": "DOE", "member_id": "MEMBER123", "dob": "1980-01-01", }, "provider": { "npi": "1234567890", "name": "PROVIDER NAME", }, "payer": { "id": "SKCO0", "name": "PAYER NAME", }, "service_type_code": "1", } # --------------------------------------------------------------------------- # # T23: POST /api/eligibility/request # --------------------------------------------------------------------------- # def test_eligibility_request_returns_raw_270_text(client: TestClient): """A minimal body returns the X12 270 text + the parsed structure.""" body = _valid_request_body() resp = client.post("/api/eligibility/request", json=body) assert resp.status_code == 200, resp.text payload = resp.json() assert "raw_270_text" in payload assert "parsed" in payload raw = payload["raw_270_text"] assert isinstance(raw, str) and raw # Canonical X12 envelope markers. assert raw.startswith("ISA*") assert raw.rstrip("\n").endswith("~IEA*1*000000001~") # All the required segments are present. assert "HL*1**20*1~" in raw assert "NM1*PR*2*PAYER NAME" in raw assert "HL*2*1*21*1~" in raw assert "NM1*1P*2*PROVIDER NAME" in raw assert "NM1*IL*1*DOE*JOHN" in raw assert "EQ*1~" in raw # The parsed payload mirrors the input. parsed = payload["parsed"] assert parsed["subscriber"]["first_name"] == "JOHN" assert parsed["subscriber"]["last_name"] == "DOE" assert parsed["subscriber"]["member_id"] == "MEMBER123" assert parsed["inquiries"][0]["service_type_code"] == "1" def test_eligibility_request_parsed_matches_serialized(client: TestClient): """The endpoint's ``parsed`` field round-trips through a server-side re-parse of ``raw_270_text``. """ body = _valid_request_body() resp = client.post("/api/eligibility/request", json=body) assert resp.status_code == 200, resp.text payload = resp.json() raw = payload["raw_270_text"] re_parsed = parse_270_text(raw, input_file="round_trip.txt") # The re-parsed structure must equal the endpoint's parsed payload # for the fields the operator can observe. parsed = payload["parsed"] assert re_parsed.subscriber.first_name == parsed["subscriber"]["first_name"] assert re_parsed.subscriber.last_name == parsed["subscriber"]["last_name"] assert re_parsed.subscriber.member_id == parsed["subscriber"]["member_id"] assert re_parsed.information_source.name == parsed["information_source"]["name"] assert re_parsed.information_receiver.npi == parsed["information_receiver"]["npi"] assert re_parsed.inquiries[0].service_type_code == parsed["inquiries"][0]["service_type_code"] def test_eligibility_request_missing_service_type_returns_400(client: TestClient): """Missing ``service_type_code`` returns 400, never 500.""" body = _valid_request_body() del body["service_type_code"] resp = client.post("/api/eligibility/request", json=body) assert resp.status_code == 400, resp.text detail = resp.json()["detail"] assert "service_type_code" in str(detail) def test_eligibility_request_missing_subscriber_member_id_returns_400(client: TestClient): """Missing ``subscriber.member_id`` returns 400.""" body = _valid_request_body() del body["subscriber"]["member_id"] resp = client.post("/api/eligibility/request", json=body) assert resp.status_code == 400, resp.text # --------------------------------------------------------------------------- # # T24: POST /api/eligibility/parse-271 # --------------------------------------------------------------------------- # def test_parse_271_endpoint_happy_path(client: TestClient): """Uploading the minimal 271 returns 200 + a non-empty ``coverage_benefits`` array. """ text = MINIMAL_271.read_text() resp = client.post( "/api/eligibility/parse-271", files={"file": ("minimal_271.txt", text, "text/plain")}, ) assert resp.status_code == 200, resp.text body = resp.json() assert "coverage_benefits" in body assert "subscriber" in body assert "summary" in body assert isinstance(body["coverage_benefits"], list) assert len(body["coverage_benefits"]) == 2 # The first EB resolves to "Medical Care". cb0 = body["coverage_benefits"][0] assert cb0["service_type_code"] == "1" assert cb0["service_type_description"] == "Medical Care" assert cb0["in_plan_network"] is True # The second EB is pharmacy. cb1 = body["coverage_benefits"][1] assert cb1["service_type_code"] == "88" assert cb1["service_type_description"] == "Pharmacy" assert cb1["in_plan_network"] is False # Subscriber extracted. assert body["subscriber"]["first_name"] == "JOHN" assert body["subscriber"]["last_name"] == "DOE" assert body["subscriber"]["member_id"] == "MEMBER123" def test_parse_271_endpoint_invalid_edi_raises_400(client: TestClient): """Garbage input must surface as 400, never 500.""" resp = client.post( "/api/eligibility/parse-271", files={"file": ("garbage.txt", "not edi", "text/plain")}, ) assert resp.status_code == 400, resp.text body = resp.json() assert "error" in body def test_parse_271_endpoint_empty_file_returns_400(client: TestClient): """An empty upload returns 400 (matches the 999 / 835 contract).""" resp = client.post( "/api/eligibility/parse-271", files={"file": ("empty.txt", "", "text/plain")}, ) assert resp.status_code == 400, resp.text