"""TDD: apply_999_rejections moves claims to REJECTED on 999 AK5 R/E. Plan: docs/superpowers/plans/2026-06-20-cyclone-workflow-automation.md Task 3 (T3). """ from __future__ import annotations from datetime import date, datetime, timezone import pytest from cyclone import db from cyclone.db import Batch, Claim, ClaimState from cyclone.inbox_state import apply_999_rejections from cyclone.parsers.models import BatchSummary, Envelope from cyclone.parsers.models_999 import ( AcknowledgmentHeader, FunctionalGroupAck, ParseResult999, SetAcceptReject, SetFunctionalGroupResponse, ) @pytest.fixture(autouse=True) def _setup(tmp_path, monkeypatch): monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/inbox.db") db._reset_for_tests() db.init_db() def _seed_batch() -> None: with db.SessionLocal()() as s: s.add(Batch( id="B-1", kind="837p", input_filename="x.txt", parsed_at=datetime.now(timezone.utc), totals_json={"total_claims": 1}, validation_json={"passed": True, "warnings": [], "errors": []}, raw_result_json={"_": "stub"}, )) s.commit() def _add_claim(*, claim_id: str, pcn: str, state: ClaimState = ClaimState.SUBMITTED) -> None: _seed_batch() with db.SessionLocal()() as s: s.add(Claim( id=claim_id, batch_id="B-1", patient_control_number=pcn, state=state, )) s.commit() def _make_999(*, claim_refs: list[tuple[str, str]]) -> ParseResult999: """claim_refs: list of (set_control_number, accept_code).""" set_responses = [ SetFunctionalGroupResponse( ak2=AcknowledgmentHeader( functional_id_code="837", group_control_number=ref, ), set_control_number=ref, transaction_set_identifier="837", segment_errors=[], set_accept_reject=SetAcceptReject(code=code), # type: ignore[arg-type] ) for ref, code in claim_refs ] return ParseResult999( envelope=Envelope( sender_id="RECEIVER", receiver_id="SUBMITTER", control_number="000000001", transaction_date=date(2024, 1, 1), implementation_guide="005010X231A1", ), functional_group_acks=[ FunctionalGroupAck( ak1=AcknowledgmentHeader(functional_id_code="HC", group_control_number="0001"), received_count=len(set_responses), accepted_count=0, rejected_count=len(set_responses), ack_code="R", ), ], set_responses=set_responses, summary=BatchSummary( input_file="x.txt", total_claims=len(set_responses), passed=0, failed=len(set_responses), ), ) def _lookup_factory(session): """Build a claim_lookup closure that uses the given session. The session must be the same one passed to apply_999_rejections so that the returned Claim is attached to that session's transaction. """ def _lookup(pcn: str): return session.query(Claim).filter_by(patient_control_number=pcn).first() return _lookup def test_rejection_moves_submitted_claim_to_rejected_state(): _add_claim(claim_id="CLP-1", pcn="PCN-001") with db.SessionLocal()() as s: result = apply_999_rejections( s, _make_999(claim_refs=[("PCN-001", "R")]), claim_lookup=_lookup_factory(s), ) s.commit() assert result.matched == ["CLP-1"] assert result.orphans == [] with db.SessionLocal()() as s: c = s.get(Claim, "CLP-1") assert c.state == ClaimState.REJECTED assert c.rejected_at is not None assert "999" in (c.rejection_reason or "") def test_accepted_set_leaves_claim_alone(): _add_claim(claim_id="CLP-1", pcn="PCN-002") with db.SessionLocal()() as s: result = apply_999_rejections( s, _make_999(claim_refs=[("PCN-002", "A")]), claim_lookup=_lookup_factory(s), ) s.commit() assert result.matched == [] with db.SessionLocal()() as s: c = s.get(Claim, "CLP-1") assert c.state == ClaimState.SUBMITTED assert c.rejected_at is None assert c.rejection_reason is None def test_unknown_pcn_recorded_as_orphan_not_error(): with db.SessionLocal()() as s: result = apply_999_rejections( s, _make_999(claim_refs=[("PCN-NONEXISTENT", "R")]), claim_lookup=_lookup_factory(s), ) s.commit() assert result.matched == [] assert result.orphans == ["PCN-NONEXISTENT"] def test_already_rejected_claim_is_idempotent(): _add_claim(claim_id="CLP-1", pcn="PCN-003", state=ClaimState.REJECTED) with db.SessionLocal()() as s: result = apply_999_rejections( s, _make_999(claim_refs=[("PCN-003", "R")]), claim_lookup=_lookup_factory(s), ) s.commit() assert result.matched == [] # no-op with db.SessionLocal()() as s: c = s.get(Claim, "CLP-1") assert c.state == ClaimState.REJECTED