"""SP37 Task 3: batch_envelope_index populates from BOTH columns. The dict returned by ``batch_envelope_index`` should resolve lookups by EITHER ``Batch.raw_result_json.envelope.control_number`` (ISA13, preserved) OR ``Batch.transaction_set_control_number`` (ST02, new in SP37 Task 2). One dict, both keys — a single ``.get(set_control_number)`` call hits whichever matches, so the D10 Pass 1 join in :func:`cyclone.claim_acks.lookup_claims_for_ack_set_response` resolves 999 AK201 (which echoes the source 837's ST02) back to the right batch even when ST02 != ISA13. """ from __future__ import annotations from datetime import datetime, timezone import pytest from cyclone import db as db_mod from cyclone.db import Batch from cyclone.store.claim_acks import batch_envelope_index @pytest.fixture(autouse=True) def _db(tmp_path, monkeypatch): """Per-test DB; conftest's autouse already wires one too, but we pin the URL again so this module is self-contained if anyone ever lifts it out of the conftest tree.""" monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db") db_mod._reset_for_tests() db_mod.init_db() yield def _make_batch( s, *, id: str, icn: str, stcn: str | None = None, raw_json: dict | None = None, ) -> Batch: """Insert one Batch row whose envelope + ST02 mirror what ``store.write.add_record`` writes for an 837P batch.""" row = Batch( id=id, kind="837p", input_filename=id + ".x12", parsed_at=datetime.now(timezone.utc), raw_result_json=raw_json or {"envelope": {"control_number": icn}}, transaction_set_control_number=stcn, ) s.add(row) s.commit() s.refresh(row) return row def test_index_resolves_by_envelope_control_number(): """Pre-SP37 path: ISA13 (envelope.control_number) still resolves.""" with db_mod.SessionLocal()() as s: _make_batch(s, id="b1", icn="ISA000001") idx = batch_envelope_index() assert idx.get("ISA000001") == "b1" def test_index_resolves_by_transaction_set_control_number(): """SP37 path: ST02 (transaction_set_control_number) resolves too. The whole point of Task 3 — Gainwell batches have ST02 != ISA13, so 999 AK201 echoes ST02 and must hit this branch. """ with db_mod.SessionLocal()() as s: _make_batch( s, id="b2", icn="ISA000002", stcn="ST000002", raw_json={"envelope": {"control_number": "ISA000002"}}, ) idx = batch_envelope_index() assert idx.get("ST000002") == "b2" # Backward compat: ISA still resolves. assert idx.get("ISA000002") == "b2" def test_index_handles_row_with_no_transaction_set_control_number(): """Pre-migration rows (stcn NULL) should still resolve by ISA. Spec says: 'Pre-migration rows (stcn NULL) should still resolve by ISA.' Production row ``b1`` (the only one with claims) has ST02 NULL because the migration's backfill only ran over rows whose raw_result_json envelope had the ST02 key — and that row was written before the parser populated it. """ with db_mod.SessionLocal()() as s: _make_batch(s, id="b3", icn="ISA000003") # no stcn idx = batch_envelope_index() assert idx.get("ISA000003") == "b3" assert idx.get("ST000003") is None def test_index_ignores_non_837_batches(): """835 batches don't contribute to the 999 join index. The D10 two-pass join is specifically for 837 → 999 linkage; 835 remittances live on the response side of the loop and have no ST02 join key. Filtering by ``kind == "837p"`` keeps the index scoped correctly and avoids an 835 row shadowing an 837 ST02 by accident. """ with db_mod.SessionLocal()() as s: # Add an 837 row first so we can prove the 835 doesn't leak in. _make_batch(s, id="b-837", icn="ISA837", stcn="ST837") # And an 835 row whose ISA13 / ST02 would otherwise pollute. s.add(Batch( id="b-835", kind="835", input_filename="b-835.x12", parsed_at=datetime.now(timezone.utc), raw_result_json={"envelope": {"control_number": "ISA999"}}, transaction_set_control_number="ST999", )) s.commit() idx = batch_envelope_index() # 837 row resolves both ways. assert idx.get("ISA837") == "b-837" assert idx.get("ST837") == "b-837" # 835 row contributes nothing. assert "ISA999" not in idx assert "ST999" not in idx