"""End-to-end ingest tests for SP28: 999 file → claim_acks rows. Step 4.5 of the plan. Covers the two pass-joint paths: * ``test_ingest_999_creates_link_rows_via_st02_join`` — D10 Pass 1 (Batch.envelope.control_number == ST02). The fixture's ST02 (``991102989``) matches a pre-seeded Batch's envelope so Pass 1 resolves the claim by ``Claim.batch_id``. PCN mismatch on purpose. * ``test_ingest_999_creates_link_rows_via_pcn_fallback`` — D10 Pass 2 (Claim.patient_control_number). No Batch matches, so Pass 2 fires and resolves by PCN. Both tests use the FastAPI test client to exercise the full parse-999 → apply_999_acceptances → add_claim_ack chain end-to-end, then verify the row exists in the DB and the response shape includes the new ``claim_ack_links_count`` field. """ from __future__ import annotations from datetime import datetime, timezone from decimal import Decimal from pathlib import Path import pytest from fastapi.testclient import TestClient from cyclone import db from cyclone.api import app from cyclone.db import Batch, Claim, ClaimAck, ClaimState from cyclone.store import store GAINWELL_999 = ( Path(__file__).parent / "fixtures" / "minimal_999_ik5_gainwell.txt" ) ACCEPTED_999 = Path(__file__).parent / "fixtures" / "minimal_999.txt" @pytest.fixture(autouse=True) def clear_store(): """Reset the module-level store before and after each test. The conftest's autouse fixtures set up the tmp DB; here we just make sure the in-memory store shim is clean too (no-op for the DB-backed store but matches the rest of the test suite's style). """ with store._lock: store._batches.clear() yield with store._lock: store._batches.clear() @pytest.fixture def client() -> TestClient: return TestClient(app) def _seed_batch_envelope(*, batch_id: str, envelope_control: str): """Insert one Batch row whose raw_result_json envelope carries ``envelope_control`` so D10 Pass 1 can resolve it via ``batch_envelope_index``.""" with db.SessionLocal()() as s: b = Batch( id=batch_id, kind="837", input_filename=f"{batch_id}.txt", parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc), raw_result_json={ "envelope": { "sender_id": "SUBMITTER", "receiver_id": "RECEIVER", "control_number": envelope_control, "transaction_date": "2026-07-02", }, }, ) s.add(b) s.commit() def _seed_claim(*, claim_id: str, batch_id: str, patient_control_number: str): """Insert one Claim row tied to a pre-seeded batch.""" with db.SessionLocal()() as s: c = Claim( id=claim_id, batch_id=batch_id, patient_control_number=patient_control_number, charge_amount=Decimal("100.00"), state=ClaimState.SUBMITTED, ) s.add(c) s.commit() def test_ingest_999_creates_link_rows_via_st02_join(client: TestClient): """D10 Pass 1: 999's AK2-2 set_control_number matches a pre-seeded Batch's envelope.control_number, so the auto-linker resolves the claim by Claim.batch_id (NOT by PCN). PCN is intentionally non-matching to prove Pass 1 won — if Pass 2 had fired instead, the PCN lookup would have returned no claim and the row would have been an orphan. """ # Seed a Batch whose envelope control number == the Gainwell # 999's AK2-2 set_control_number ("991102989"). _seed_batch_envelope(batch_id="B-GAINWELL", envelope_control="991102989") # Seed a Claim in that batch, but with a PCN that does NOT match # "991102989" — Pass 2 must not fire. _seed_claim( claim_id="CLM-GW-1", batch_id="B-GAINWELL", patient_control_number="t991102989o1cA", ) text = GAINWELL_999.read_text() resp = client.post( "/api/parse-999", files={"file": ("minimal_999_ik5_gainwell.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() # New response field from spec §4. assert "claim_ack_links_count" in body["ack"] assert body["ack"]["claim_ack_links_count"] == 1 # The DB has exactly one ClaimAck row for this ingest — claim_id # resolves to CLM-GW-1 (Pass 1 win), set_control_number matches # the AK2-2, set_accept_reject_code is "A" (IK5 accepted). with db.SessionLocal()() as s: rows = ( s.query(ClaimAck) .filter(ClaimAck.ack_kind == "999") .all() ) assert len(rows) == 1 assert rows[0].claim_id == "CLM-GW-1" assert rows[0].set_control_number == "991102989" assert rows[0].set_accept_reject_code == "A" assert rows[0].ak2_index == 0 assert rows[0].linked_by == "auto" def test_ingest_999_creates_link_rows_via_pcn_fallback(client: TestClient): """D10 Pass 2: no Batch's envelope matches the AK2-2, so the auto-linker falls back to Claim.patient_control_number.""" # Seed a Batch whose envelope control number does NOT match. _seed_batch_envelope(batch_id="B-OTHER", envelope_control="999999999") # Seed a Claim in some other batch whose PCN equals the 999's # set_control_number ("0001" for the minimal accepted fixture). _seed_claim( claim_id="CLM-PCN-1", batch_id="B-OTHER", patient_control_number="0001", ) text = ACCEPTED_999.read_text() resp = client.post( "/api/parse-999", files={"file": ("minimal_999.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() assert body["ack"]["claim_ack_links_count"] == 1 with db.SessionLocal()() as s: rows = ( s.query(ClaimAck) .filter(ClaimAck.ack_kind == "999") .all() ) assert len(rows) == 1 assert rows[0].claim_id == "CLM-PCN-1" assert rows[0].set_control_number == "0001"