Files
cyclone/backend/tests/test_batch_txn_set_cn.py
Nora 3a58f561d4 feat(sp37): populate batches.transaction_set_control_number on 837P ingest
The parser now captures the source 837's ST02 (transaction set control
number) on the Envelope model as 'transaction_set_control_number'.
add_record reads it off the envelope and stores it on the Batch row,
mirroring how 'envelope.control_number' (ISA13) was already handled.

Unlocks the SP37 join-key update so 999 set_control_number (AK201)
values resolve back to the right batch via Pass 1.
2026-07-07 10:22:32 -06:00

103 lines
4.0 KiB
Python

"""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"