Files
cyclone/backend/tests/test_e2e_999_to_claim_drawer.py
T
Nora 9e16b8d9bd feat(sp28): wire apply_claim_ack_links into handle_999 / handle_277ca / handle_ta1
Phase 4 of SP28 (Ack↔Claim Auto-Link). Refactors the per-AK2
helpers in cyclone.claim_acks to return ClaimAckLinkRow dataclasses
instead of mutating the session directly — the orchestrator (the
999 / 277CA / TA1 handlers + the matching parse-* API endpoints)
now persists each row via cycl_store.add_claim_ack so the
publish-from-store contract owns the live-tail event emission.

* handle_999 / handle_277ca / handle_ta1 — build batch envelope
  index outside the work session (SQLite + concurrent sessions
  causes 'database is locked'), call apply_X to get
  ClaimAckLinkRow dataclasses, snapshot the rows before committing
  the work session, then call cycl_store.add_claim_ack per row
  in fresh sessions.
* /api/parse-999 / /api/parse-277ca / /api/parse-ta1 — mirror the
  handler chain with event_bus passed through so live-tail
  subscribers on the claim and ack sides see the new rows the
  moment they land. Adds a 'claim_ack_links_count' field to each
  ack response (spec §4).
* lookup_claims_for_ack_set_response — now accepts either a
  callable OR a plain dict as batch_envelope_index (the store
  returns a dict; tests pass callables).
* test_apply_claim_ack_links.py — 15 tests updated to assert on
  the dataclass shape and exercise the full helper→add_claim_ack
  cycle (so idempotency is verified at the store layer).
* test_e2e_999_to_claim_drawer.py — 2 new tests covering the
  D10 two-pass join end-to-end via FastAPI TestClient (Pass 1
  via ST02 + Pass 2 via PCN fallback).
2026-07-02 11:45:19 -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="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"