"""SP37 Task 2: add_record populates Batch.transaction_set_control_number. The column should mirror the source 837 envelope's ``transaction_set_control_number`` (ST02) so the join-key update in Task 3 can resolve 999 ``set_control_number`` (AK201) values back to the right batch. The 835 path leaves the column NULL because the field is an 837P-specific join key (835 remittances don't need to resolve back to a source 837 — they're the response side of the loop). """ from __future__ import annotations from datetime import date, datetime, timezone from pathlib import Path from cyclone import db as db_mod from cyclone.db import Batch from cyclone.parsers.parse_837 import parse as parse_837_text from cyclone.parsers.payer import PayerConfig from cyclone.store import store as cycl_store from cyclone.store.records import BatchRecord837, BatchRecord835 FIXTURE_837 = Path(__file__).parent / "fixtures" / "minimal_837p.txt" FIXTURE_835 = Path(__file__).parent / "fixtures" / "minimal_835.txt" def test_add_record_populates_transaction_set_control_number_for_837(): """parse_837 → add_record writes the parsed ST02 onto the Batch row. The fixture's ST02 (``991102977``) must round-trip into the new ``batches.transaction_set_control_number`` column. This is the join key Task 3's 999-ingest Pass 1 update relies on. """ text = FIXTURE_837.read_text() parsed = parse_837_text(text, PayerConfig.co_medicaid()) record = BatchRecord837( id="b-txn-cn-1", input_filename="minimal_837p.txt", parsed_at=datetime.now(timezone.utc), result=parsed, ) cycl_store.add(record) with db_mod.SessionLocal()() as s: row = s.get(Batch, "b-txn-cn-1") assert row is not None assert row.transaction_set_control_number == parsed.envelope.transaction_set_control_number # Sanity: the fixture's ST02 must be the parsed-and-stored value. assert row.transaction_set_control_number == "991102977" def test_envelope_model_exposes_transaction_set_control_number(): """The Envelope Pydantic model carries the new field with a None default. Guards against a regression where someone removes the field from the model — the rest of the chain (parser + ORM + write path) silently degrades to None if the model loses the attribute. """ from cyclone.parsers.models import Envelope env = Envelope( sender_id="S", receiver_id="R", control_number="000000001", transaction_date=date(2026, 1, 1), ) assert env.transaction_set_control_number is None env2 = env.model_copy(update={"transaction_set_control_number": "0001"}) assert env2.transaction_set_control_number == "0001" def test_add_record_leaves_835_column_null(): """835 batches don't carry an ST02 join key; column stays NULL. The shared ``Envelope`` class is used by both 837P and 835 parsers, but only ``parse_837`` populates ``transaction_set_control_number``. For 835 records the field is None and ``add_record`` writes NULL to the column — verified end-to-end via a real 835 ingest. """ text = FIXTURE_835.read_text() from cyclone.parsers.parse_835 import parse as parse_835_text parsed835 = parse_835_text(text, payer_config=None) # type: ignore[arg-type] # Sanity: the 835 envelope also has the field (shared model class), # but the parser doesn't populate it — so it must be None. assert parsed835.envelope.transaction_set_control_number is None record = BatchRecord835( id="b-txn-cn-835-1", input_filename="minimal_835.txt", parsed_at=datetime.now(timezone.utc), result=parsed835, ) cycl_store.add(record) with db_mod.SessionLocal()() as s: row = s.get(Batch, "b-txn-cn-835-1") assert row is not None assert row.transaction_set_control_number is None # And the kind/kind round-trip is correct (sanity check that # we did exercise the 835 path, not the 837 path). assert row.kind == "835"