Files
cyclone/backend/tests/test_e2e_999_to_claim_drawer.py
Nora fba594baf4 fix(sp28): align Batch.kind filter with prod ('837p' not '837')
The SP28 helpers (batch_envelope_index + lookup_claims_for_ack_set_response
+ two _batch_lookup closures in handle_ta1 + api.py) all filter
Batch.kind == "837", but prod Batch rows are persisted with
kind="837p" (lowercase p — see api.py:443 / store/records.py:58).
Result: the D10 batch-envelope index was empty on prod and zero 999 /
277CA / TA1 acks auto-linked to their claims via Pass 1 (ST02 via
batch.envelope.control_number). Pass 2 (PCN) was the only path firing,
which on this codebase matches 0 acks (Gainwell's 999 echoes the 837's
ST02, not its CLM01 — see spec §D10).

Fix: 4 production sites swapped to Batch.kind == "837p". 3 test
files updated to use the prod value (test_apply_claim_ack_links.py +
test_api_claim_acks.py + test_e2e_999_to_claim_drawer.py). All 25 SP28
tests + 20 handler tests pass. Expected prod effect: claim
t991102989o1c120d's 143 incoming 999 AK2 acks now auto-link via the
batch with envelope.control_number=991102989 (Pass 1), plus 727/1,398
total acks across all batches.
2026-07-02 12:22:29 -06:00

175 lines
6.0 KiB
Python

"""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="837p",
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"