"""Tests for :mod:`cyclone.claim_acks` (SP28 auto-linker). Two flavours: * Pure-unit tests (no DB) for the walk-through logic of ``apply_999_acceptances`` etc. via stubs. * Integration tests against the test DB (autouse conftest) that exercise ``lookup_claims_for_ack_set_response``'s two-pass D10 join, per-AK2 granularity, idempotent re-ingest, and the manual link. The 8 named tests from spec §6 are here as ``test_999_*`` / ``test_277ca_*`` / ``test_ta1_*`` / ``test_reingest_*`` / ``test_manual_link_*`` / ``test_unlink_does_not_revert_claim_state``. Additional tests cover the D10 two-pass join (Step 2.5 of the plan) and the store facade wiring. """ from __future__ import annotations from datetime import date, datetime, timezone from decimal import Decimal import pytest from cyclone import claim_acks as ca from cyclone import db from cyclone.claim_acks import ( ClaimAckLinkResult, apply_277ca_acks, apply_999_acceptances, apply_ta1_envelope_link, link_manual, lookup_claims_for_ack_set_response, ) from cyclone.db import ( Batch, Claim, ClaimState, ClaimAck, ) from cyclone.parsers.models import ( BatchSummary, ClaimHeader, Envelope, ParseResult, ) from cyclone.parsers.models_277ca import ( AcknowledgmentHeader, ClaimStatus, ParseResult277CA, ) from cyclone.parsers.models_999 import ( AcknowledgmentHeader as AckHeader999, ParseResult999, SetAcceptReject, SetFunctionalGroupResponse, ) from cyclone.parsers.models_ta1 import ParseResultTa1, Ta1Ack from cyclone.store import store # --------------------------------------------------------------------------- # Fixture seeds # --------------------------------------------------------------------------- def _seed_batch( session, *, batch_id: str = "B1", envelope_control: str = "991102989", sender_id: str = "SENDER", receiver_id: str = "RECEIVER", ): """Insert one Batch row whose raw_result_json envelope carries ``envelope_control`` so D10 Pass 1 can resolve it via ``batch_envelope_index``.""" 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": sender_id, "receiver_id": receiver_id, "control_number": envelope_control, "transaction_date": "2026-07-02", }, }, ) session.add(b) session.commit() session.refresh(b) return b def _seed_claim( session, *, claim_id: str, batch_id: str, patient_control_number: str | None = None, state: ClaimState = ClaimState.SUBMITTED, ): c = Claim( id=claim_id, batch_id=batch_id, patient_control_number=patient_control_number or claim_id, charge_amount=Decimal("100.00"), state=state, ) session.add(c) session.commit() session.refresh(c) return c def _seed_ack_link_index(*, envelope_control_to_batch: dict[str, str]): """Build a closure compatible with ``lookup_claims_for_ack_set_response``.""" def _lookup(scn: str) -> str | None: return envelope_control_to_batch.get(scn) return _lookup def _parse_999_two_ak2s() -> ParseResult999: """Two-AK2 999 — one accepted, one rejected.""" sr_accepted = SetFunctionalGroupResponse( ak2=AckHeader999(functional_id_code="837", group_control_number="991102989"), set_control_number="991102989", transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject(code="A"), ) sr_rejected = SetFunctionalGroupResponse( ak2=AckHeader999(functional_id_code="837", group_control_number="991102990"), set_control_number="991102990", transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject(code="R"), ) return ParseResult999( envelope=Envelope( sender_id="PAYER", receiver_id="SUBMITTER", control_number="000000001", transaction_date=date(2026, 7, 2), ), functional_group_acks=[], set_responses=[sr_accepted, sr_rejected], summary=BatchSummary( input_file="two_ak2.txt", control_number="000000001", transaction_date=date(2026, 7, 2), total_claims=2, passed=1, failed=1, ), ) def _parse_277ca_one_accepted() -> ParseResult277CA: return ParseResult277CA( envelope=Envelope( sender_id="PAYER", receiver_id="SUBMITTER", control_number="000000077", transaction_date=date(2026, 7, 2), ), bht=AcknowledgmentHeader( hierarchical_structure_code="0085", transaction_set_purpose_code="08", reference_identification="REFNUM001", ), summary=BatchSummary( input_file="277_one_accepted.txt", control_number="000000077", transaction_date=date(2026, 7, 2), total_claims=1, passed=1, failed=0, ), claim_statuses=[ ClaimStatus( status_code="A3:19:PR", classification="accepted", payer_claim_control_number="CLAIM001", ), ], ) def _parse_277ca_one_rejected() -> ParseResult277CA: return ParseResult277CA( envelope=Envelope( sender_id="PAYER", receiver_id="SUBMITTER", control_number="000000078", transaction_date=date(2026, 7, 2), ), bht=AcknowledgmentHeader( hierarchical_structure_code="0085", transaction_set_purpose_code="08", reference_identification="REFNUM002", ), summary=BatchSummary( input_file="277_one_rejected.txt", control_number="000000078", transaction_date=date(2026, 7, 2), total_claims=1, passed=0, failed=1, ), claim_statuses=[ ClaimStatus( status_code="A6:19:PR", classification="rejected", payer_claim_control_number="CLAIM002", ), ], ) def _parse_ta1() -> ParseResultTa1: return ParseResultTa1( envelope=Envelope( sender_id="RECEIVER", receiver_id="SENDER", control_number="000000001", transaction_date=date(2026, 7, 2), ), ta1=Ta1Ack( control_number="000000001", interchange_date=date(2026, 7, 2), interchange_time="1200", ack_code="A", note_code="000", ack_generated_date=date(2026, 7, 2), ), source_batch_id="TA1-000000001", ) # --------------------------------------------------------------------------- # Spec §6 tests # --------------------------------------------------------------------------- def test_999_auto_creates_per_ak2_link_rows(): """Step 2.5 (named after spec §6). Two AK2s in one 999 — one accepted, one rejected. They reference two distinct batches (one claim per batch), so per-AK2 granularity produces two link rows with two different set_accept_reject codes. """ with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-999A", envelope_control="991102989") _seed_claim(s, claim_id="CLM-A", batch_id="B-999A", patient_control_number="t991102989o1cA") _seed_batch(s, batch_id="B-999B", envelope_control="991102990") _seed_claim(s, claim_id="CLM-B", batch_id="B-999B", patient_control_number="t991102990o1cB") # Index keyed by set_control_number -> batch_id (D10 batch # envelope index). idx = _seed_ack_link_index(envelope_control_to_batch={ "991102989": "B-999A", "991102990": "B-999B", }) parsed = _parse_999_two_ak2s() out = apply_999_acceptances(s, parsed, ack_id=42, batch_envelope_index=idx) s.commit() assert sorted(out.linked) == sorted([("CLM-A", 0), ("CLM-B", 1)]) assert out.orphans == [] with db.SessionLocal()() as s: rows = ( s.query(ClaimAck) .filter(ClaimAck.ack_kind == "999", ClaimAck.ack_id == 42) .order_by(ClaimAck.id.asc()) .all() ) assert len(rows) == 2 codes = {r.set_accept_reject_code for r in rows} assert codes == {"A", "R"} assert all(r.linked_by == "auto" for r in rows) def test_999_orphan_does_not_create_link(): """An AK2 whose set_control_number doesn't resolve stays orphan.""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-999-ORPHAN", envelope_control="0001") parsed = _parse_999_two_ak2s() idx = _seed_ack_link_index(envelope_control_to_batch={}) out = apply_999_acceptances(s, parsed, ack_id=99, batch_envelope_index=idx) s.commit() assert out.linked == [] assert sorted(out.orphans) == ["991102989", "991102990"] with db.SessionLocal()() as s: n = s.query(ClaimAck).filter(ClaimAck.ack_kind == "999").count() assert n == 0 def test_277ca_accepted_creates_link_with_no_state_mutation(): """Accepted 277CA → link row, no payer_rejected stamp.""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-277A", envelope_control="991102989") claim = _seed_claim(s, claim_id="CLM-Z", batch_id="B-277A", patient_control_number="CLAIM001") idx = _seed_ack_link_index(envelope_control_to_batch={}) parsed = _parse_277ca_one_accepted() def _pcn_lookup(pcn: str): return (s.query(Claim) .filter_by(patient_control_number=pcn) .first()) out = apply_277ca_acks(s, parsed, ack_id=7, batch_envelope_index=idx, pc_claim_lookup=_pcn_lookup) s.commit() assert out.linked == [("CLM-Z", None)] with db.SessionLocal()() as s: c = s.get(Claim, "CLM-Z") assert c.payer_rejected_at is None link = s.query(ClaimAck).filter_by(claim_id="CLM-Z").one() assert link.set_accept_reject_code.startswith("A3") assert link.linked_by == "auto" def test_277ca_rejected_creates_link_and_mutates_state(): """Rejected 277CA → link row + payer_rejected_at stamp (mirrors existing apply_277ca_rejections test).""" from cyclone.inbox_state_277ca import apply_277ca_rejections with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-277R", envelope_control="991102989") _seed_claim(s, claim_id="CLM-Y", batch_id="B-277R", patient_control_number="CLAIM002") idx = _seed_ack_link_index(envelope_control_to_batch={}) parsed = _parse_277ca_one_rejected() # First, run the existing rejection mutator (it owns # payer_rejected_at + state). Then run the auto-linker. apply_277ca_rejections( s, parsed, claim_lookup=lambda pcn: (s.query(Claim) .filter_by(patient_control_number=pcn) .first()), two77ca_id=7, ) def _pcn_lookup(pcn: str): return (s.query(Claim) .filter_by(patient_control_number=pcn) .first()) out = apply_277ca_acks(s, parsed, ack_id=7, batch_envelope_index=idx, pc_claim_lookup=_pcn_lookup) s.commit() assert out.linked == [("CLM-Y", None)] with db.SessionLocal()() as s: c = s.get(Claim, "CLM-Y") assert c.payer_rejected_at is not None assert c.payer_rejected_status_code and c.payer_rejected_status_code.startswith("A6") link = s.query(ClaimAck).filter_by(claim_id="CLM-Y").one() assert link.set_accept_reject_code.startswith("A6") def test_ta1_links_to_most_recent_matching_batch(): """TA1's batch_lookup closure picks the most-recent batch whose envelope.sender_id/receiver_id matches the TA1 envelope.""" ta1 = _parse_ta1() # TA1 carries the swapped ISA sender/receiver (the receiving side # is sending the ACK back to the original submitter). with db.SessionLocal()() as s: # Two older batches whose envelope sender/receiver do NOT # match the TA1. _seed_batch(s, batch_id="T1-OLD", envelope_control="0001", sender_id="SENDER", receiver_id="RECEIVER") _seed_batch(s, batch_id="T2-NEW", envelope_control="0002", sender_id="SENDER", receiver_id="RECEIVER") # The matching batch has the swapped sender/receiver; ordered # by parsed_at DESC and prefixed T3-MATCH so the most-recent # one wins. _seed_batch(s, batch_id="T3-MATCH", envelope_control="0003", sender_id="RECEIVER", receiver_id="SENDER") def _batch_lookup(sender_id, receiver_id): rows = ( s.query(Batch) .filter(Batch.kind == "837") .order_by(Batch.parsed_at.desc()) .all() ) for row in rows: env = (row.raw_result_json or {}).get("envelope") or {} if ( env.get("sender_id") == sender_id and env.get("receiver_id") == receiver_id ): return row return None out = apply_ta1_envelope_link(s, ta1, ack_id=11, batch_lookup=_batch_lookup) s.commit() assert out.linked == [(None, None)] with db.SessionLocal()() as s: link = ( s.query(ClaimAck) .filter_by(ack_kind="ta1", ack_id=11) .one() ) assert link.claim_id is None assert link.batch_id == "T3-MATCH" assert link.set_accept_reject_code == "A" def test_reingest_same_999_is_idempotent(): """Submitting the same 999 twice produces exactly one ClaimAck row per AK2 (the partial unique index enforces this).""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-IDEMP", envelope_control="991102989") _seed_claim(s, claim_id="CLM-I", batch_id="B-IDEMP", patient_control_number="t991102989o1cI") idx = _seed_ack_link_index(envelope_control_to_batch={ "991102989": "B-IDEMP", "991102990": "B-IDEMP", }) parsed = _parse_999_two_ak2s() out1 = apply_999_acceptances(s, parsed, ack_id=101, batch_envelope_index=idx) s.commit() with db.SessionLocal()() as s: out2 = apply_999_acceptances(s, parsed, ack_id=101, batch_envelope_index=idx) s.commit() # Each call returns the freshly linked rows. Second call finds # existing rows so linked is empty. assert len(out1.linked) == 2 assert out2.linked == [] # idempotent — no duplicate rows with db.SessionLocal()() as s: n = s.query(ClaimAck).filter_by(ack_id=101).count() assert n == 2 def test_manual_link_endpoint_idempotent(): """link_manual returns the existing row when called twice.""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-MAN", envelope_control="991102989") _seed_claim(s, claim_id="CLM-M", batch_id="B-MAN") row1, created1 = link_manual(s, claim_id="CLM-M", ack_kind="999", ack_id=200) s.commit() with db.SessionLocal()() as s: row2, created2 = link_manual(s, claim_id="CLM-M", ack_kind="999", ack_id=200) s.commit() assert created1 is True assert created2 is False assert row1.id == row2.id def test_manual_link_any_user_succeeds(): """Manual link does NOT raise (any-logged-in user posture per D5/D9).""" # The helper itself is identity-independent; the auth posture is # enforced at the API layer. Here we assert the helper runs to # completion (no permission check) when called by any caller. with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-AUTH", envelope_control="991102989") _seed_claim(s, claim_id="CLM-N", batch_id="B-AUTH") row, created = link_manual(s, claim_id="CLM-N", ack_kind="999", ack_id=300) s.commit() assert created is True assert row.linked_by == "manual" def test_manual_link_rejects_terminal_claim(): """A claim in state=REVERSED should NOT be link-able (the API layer maps that to 409; the helper itself lets the API pick the claim up and surfaces the state). Here we only assert that ``link_manual`` does NOT raise — the API is responsible for the 409 decision via :class:`cyclone.db.ClaimState`.""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-TERM", envelope_control="991102989") claim = _seed_claim( s, claim_id="CLM-R", batch_id="B-TERM", state=ClaimState.REVERSED, ) # Helper itself does no state check — the API maps state to # 409. We just exercise the helper so it's clear the door is # open; the API test asserts the 409. row, created = link_manual(s, claim_id="CLM-R", ack_kind="999", ack_id=400) s.commit() assert created is True assert row.claim_id == "CLM-R" def test_unlink_does_not_revert_claim_state(): """After unlink via remove_claim_ack, the claim retains its state — unlinking only removes the link row, not the state mutation from the original 999/277CA.""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-UL", envelope_control="991102989") _seed_claim(s, claim_id="CLM-UL", batch_id="B-UL", state=ClaimState.REJECTED) # Only the first AK2 resolves (single batch, single claim); # the second becomes an orphan. idx = _seed_ack_link_index(envelope_control_to_batch={ "991102989": "B-UL", }) # Submit a 999 with one AK2 so we get one link row. sr = SetFunctionalGroupResponse( ak2=AckHeader999(functional_id_code="837", group_control_number="991102989"), set_control_number="991102989", transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject(code="R"), ) parsed = ParseResult999( envelope=Envelope( sender_id="P", receiver_id="S", control_number="0001", transaction_date=date(2026, 7, 2), ), functional_group_acks=[], set_responses=[sr], summary=BatchSummary( input_file="one_ak2.txt", control_number="0001", transaction_date=date(2026, 7, 2), total_claims=1, passed=0, failed=1, ), ) apply_999_acceptances(s, parsed, ack_id=500, batch_envelope_index=idx) s.commit() with db.SessionLocal()() as s: links = ( s.query(ClaimAck) .filter_by(claim_id="CLM-UL") .all() ) assert len(links) == 1 s.delete(links[0]) s.commit() with db.SessionLocal()() as s: claim = s.get(Claim, "CLM-UL") assert claim.state == ClaimState.REJECTED assert ( s.query(ClaimAck).filter_by(claim_id="CLM-UL").first() is None ) # --------------------------------------------------------------------------- # Step 2.5 — D10 two-pass join + multi-claim batch coverage # --------------------------------------------------------------------------- def test_lookup_claims_two_pass_join(): """D10: Pass 1 (batch.envelope.control_number) + Pass 2 (PCN).""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-TP-A", envelope_control="991102989") _seed_claim(s, claim_id="C-TP-A", batch_id="B-TP-A", patient_control_number="t991102989o1cA") # No matching batch — Pass 2 alone resolves it. _seed_batch(s, batch_id="B-TP-B", envelope_control="OTHER") _seed_claim(s, claim_id="C-TP-B", batch_id="B-TP-B", patient_control_number="991102987") # Pass 1: "991102989" -> B-TP-A -> [C-TP-A] idx = _seed_ack_link_index(envelope_control_to_batch={ "991102989": "B-TP-A", "991102990": "B-TP-A", }) # PCN-only claim (no batch match). Pass 2 must find it. def _pcn_lookup(pcn: str): return s.query(Claim).filter_by(patient_control_number=pcn).first() pass1 = lookup_claims_for_ack_set_response( s, "991102989", batch_envelope_index=idx, pc_claim_lookup=_pcn_lookup, ) pass2 = lookup_claims_for_ack_set_response( s, "991102987", batch_envelope_index=idx, pc_claim_lookup=_pcn_lookup, ) miss = lookup_claims_for_ack_set_response( s, "0001", batch_envelope_index=idx, pc_claim_lookup=_pcn_lookup, ) assert [c.id for c in pass1] == ["C-TP-A"] assert [c.id for c in pass2] == ["C-TP-B"] assert miss == [] def test_lookup_claims_pass1_wins_over_pass2(): """If a PCN matches a claim in a DIFFERENT batch than the Pass 1 batch, only the Pass 1 result is returned (no double-fire).""" with db.SessionLocal()() as s: # Claim A lives in B1 (whose ST02 == '991102989'). Pass 1 hits. _seed_batch(s, batch_id="B-PP-1", envelope_control="991102989") _seed_claim(s, claim_id="C-PP-A", batch_id="B-PP-1", patient_control_number="991102989") # Claim B lives in B2 (ST02 == 'OTHER') but has the same PCN # by coincidence. Pass 1 misses for '991102989' on B2 so Pass 2 # SHOULD match it — but only when Pass 1 returns nothing. _seed_batch(s, batch_id="B-PP-2", envelope_control="OTHER") _seed_claim(s, claim_id="C-PP-B", batch_id="B-PP-2", patient_control_number="991102989") idx = _seed_ack_link_index(envelope_control_to_batch={ "991102989": "B-PP-1", }) rows = lookup_claims_for_ack_set_response( s, "991102989", batch_envelope_index=idx, pc_claim_lookup=lambda pcn: s.query(Claim) .filter_by(patient_control_number=pcn) .first(), ) # Pass 1 must win and skip Pass 2. assert [c.id for c in rows] == ["C-PP-A"] def test_999_linker_emits_one_row_per_claim_in_multi_claim_batch(): """One 999 AK2 + N claims in one batch → N ClaimAck rows for that single AK2 (one-ack-to-many).""" with db.SessionLocal()() as s: _seed_batch(s, batch_id="B-MC", envelope_control="991102989") _seed_claim(s, claim_id="C-MC-1", batch_id="B-MC", patient_control_number="t991102989o1c1") _seed_claim(s, claim_id="C-MC-2", batch_id="B-MC", patient_control_number="t991102989o1c2") idx = _seed_ack_link_index(envelope_control_to_batch={ "991102989": "B-MC", }) # 999 with one AK2 whose set_control_number matches the batch. sr = SetFunctionalGroupResponse( ak2=AckHeader999(functional_id_code="837", group_control_number="991102989"), set_control_number="991102989", transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject(code="A"), ) parsed = ParseResult999( envelope=Envelope( sender_id="P", receiver_id="S", control_number="0001", transaction_date=date(2026, 7, 2), ), functional_group_acks=[], set_responses=[sr], summary=BatchSummary( input_file="one_ak2.txt", control_number="0001", transaction_date=date(2026, 7, 2), total_claims=1, passed=1, failed=0, ), ) out = apply_999_acceptances(s, parsed, ack_id=42, batch_envelope_index=idx) s.commit() assert sorted(out.linked) == sorted([("C-MC-1", 0), ("C-MC-2", 0)]) with db.SessionLocal()() as s: n = s.query(ClaimAck).filter_by(ack_kind="999").count() assert n == 2 # --------------------------------------------------------------------------- # Step 3.4 — facade wiring test (placeholder; full Phase 3 adds the # store methods). The presence-only check verifies the public surface # later exposes the same names — kept here so the plan-step stays # trackable. # --------------------------------------------------------------------------- def test_store_facade_exposes_claim_ack_methods(): """Step 3.4: facade must expose add_claim_ack + 4 read methods.""" expected = [ "add_claim_ack", "list_acks_for_claim", "list_claims_for_ack", "find_ack_orphans", "remove_claim_ack", "batch_envelope_index", ] for name in expected: assert hasattr(store, name), f"missing CycloneStore.{name}" # --------------------------------------------------------------------------- # Step 2.1 — pure walk-through unit test (no DB) for the orphan tracking # --------------------------------------------------------------------------- def test_999_linker_walks_set_responses(): """Stub-based: one AK2 resolves, one orphan, no DB.""" # Mock claim_lookup: "AAA" -> Claim-like, otherwise None. class _StubClaim: def __init__(self, cid): self.id = cid from cyclone.parsers.models_999 import ( ParseResult999, SetAcceptReject, SetFunctionalGroupResponse, AcknowledgmentHeader as AckHdr, ) parse_result = ParseResult999.model_construct() parse_result.envelope = Envelope.model_construct( sender_id="", receiver_id="", control_number="", transaction_date=date(2026, 7, 2), ) parse_result.functional_group_acks = [] parse_result.summary = BatchSummary( input_file="stub.txt", control_number="", transaction_date=date(2026, 7, 2), total_claims=2, passed=1, failed=1, ) parse_result.set_responses = [ SetFunctionalGroupResponse.model_construct( ak2=AckHdr(functional_id_code="837", group_control_number="AAA"), set_control_number="AAA", transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject.model_construct(code="A"), ), SetFunctionalGroupResponse.model_construct( ak2=AckHdr(functional_id_code="837", group_control_number="BBB"), set_control_number="BBB", transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject.model_construct(code="R"), ), ] seen: list[str] = [] def _pcn_lookup(pcn): seen.append(pcn) return _StubClaim(f"CLM-{pcn}") if pcn == "AAA" else None class _StubQuery: def filter(self, *args, **kwargs): return self def first(self): return None # no existing link rows in stub class _StubSession: # Pretend to be a Session; capture adds and answer the dedup # query with "no existing rows" so the stub stays pure-unit. def __init__(self): self.added: list[ClaimAck] = [] def add(self, obj): # noqa: D401 self.added.append(obj) def flush(self): # noqa: D401 pass def query(self, *args, **kwargs): return _StubQuery() s = _StubSession() out = apply_999_acceptances( s, parse_result, ack_id=1, batch_envelope_index=lambda scn: None, # Pass 1 misses → Pass 2 fires pc_claim_lookup=_pcn_lookup, ) assert seen == ["AAA", "BBB"] assert [(cid, idx) for (cid, idx) in out.linked] == [("CLM-AAA", 0)] assert out.orphans == ["BBB"] assert len(s.added) == 1 assert s.added[0].claim_id == "CLM-AAA" assert s.added[0].ack_kind == "999" assert s.added[0].set_accept_reject_code == "A"