"""Tests for the in-memory batch store.""" from __future__ import annotations from datetime import datetime, timezone from decimal import Decimal from cyclone.parsers.models import ( BatchSummary, BillingProvider, ClaimHeader, ClaimOutput, Envelope, Payer, ParseResult, Subscriber, ValidationIssue, ValidationReport, ) from cyclone.parsers.models_835 import ( ClaimAdjustment, ClaimPayment, FinancialInfo, ParseResult835, Payer835, Payee835, ReassociationTrace, ServicePayment, ) from cyclone.parsers.payer import PayerConfig835 from cyclone.store import BatchRecord, InMemoryStore, store # --------------------------------------------------------------------------- # Stubs (match real model field names) # --------------------------------------------------------------------------- _PARSED_AT = datetime(2026, 6, 19, 12, 0, 0, tzinfo=timezone.utc) def _make_claim(claim_id: str = "CLM-1", *, frequency_code: str = "1") -> ClaimOutput: """Minimal ClaimOutput stub. Real parser not needed here.""" return ClaimOutput( claim_id=claim_id, control_number="0001", transaction_date="2026-06-19", billing_provider=BillingProvider( name="Test", npi="1234567890", tax_id=None, address=None, ), subscriber=Subscriber( first_name="Jane", last_name="Doe", member_id="M1", dob=None, gender=None, address=None, ), payer=Payer(name="Test Payer", id="P1"), claim=ClaimHeader( claim_id=claim_id, total_charge=Decimal("0.00"), place_of_service="11", facility_code_qualifier=None, frequency_code=frequency_code, provider_signature=None, assignment=None, release_of_info=None, prior_auth=None, ), diagnoses=[], service_lines=[], validation=ValidationReport(passed=True, errors=[], warnings=[]), raw_segments=[], ) def _make_result() -> ParseResult: return ParseResult( envelope=Envelope( sender_id="SENDER", receiver_id="RECEIVER", control_number="0001", transaction_date="2026-06-19", transaction_time=None, implementation_guide=None, ), claims=[_make_claim("CLM-1")], summary=BatchSummary( input_file="test.txt", control_number="0001", transaction_date="2026-06-19", total_claims=1, passed=1, failed=0, failed_claim_ids=[], issues_by_rule={}, output_dir=None, ), ) def _make_remit( *, status_code: str = "1", service_payments: list[ServicePayment] | None = None, ) -> ClaimPayment: return ClaimPayment( payer_claim_control_number="PCN-1", status_code=status_code, status_label=None, total_charge=Decimal("100.00"), total_paid=Decimal("80.00"), patient_responsibility=Decimal("0.00"), claim_filing_indicator="MC", original_claim_id=None, frequency_code=None, facility_type=None, service_payments=service_payments or [], ) def _make_835_result(claims: list[ClaimPayment] | None = None) -> ParseResult835: return ParseResult835( envelope=Envelope( sender_id="SENDER", receiver_id="RECEIVER", control_number="0001", transaction_date="2026-06-19", transaction_time=None, implementation_guide=None, ), financial_info=FinancialInfo( handling_code="C", paid_amount=Decimal("80.00"), credit_debit_flag="C", payment_method=None, payer_tax_id=None, payment_date=None, ), trace=ReassociationTrace( trace_type_code="1", trace_number="TRN1", originating_company_id="OC1", payer_id=None, ), payer=Payer835(name="CO_TXIX", id="7912900843", address=None, contact_url=None), payee=Payee835(name="Acme Clinic", npi="1234567890", address=None), claims=claims or [], summary=BatchSummary( input_file="test.txt", control_number="0001", transaction_date="2026-06-19", total_claims=1, passed=1, failed=0, failed_claim_ids=[], issues_by_rule={}, output_dir=None, ), ) # --------------------------------------------------------------------------- # Task 1: BatchRecord + InMemoryStore # --------------------------------------------------------------------------- def test_module_singleton_store_exists(): """The module-level `store` should be importable and be an InMemoryStore.""" assert isinstance(store, InMemoryStore) def test_in_memory_store_starts_empty(): s = InMemoryStore() assert s.list() == [] def test_add_and_get_round_trip(): s = InMemoryStore() result = _make_result() rec = BatchRecord( id="abc123", kind="837p", input_filename="test.txt", parsed_at=_PARSED_AT, result=result, ) s.add(rec) assert s.get("abc123") is rec assert s.list() == [rec] def test_get_missing_returns_none(): s = InMemoryStore() assert s.get("does-not-exist") is None def test_batch_record_parsed_at_accepts_datetime(): """Spec 6.1: parsed_at is a tz-aware datetime, not a string.""" rec = BatchRecord( id="x", kind="837p", input_filename="a.txt", parsed_at=_PARSED_AT, result=_make_result(), ) assert isinstance(rec.parsed_at, datetime) assert rec.parsed_at.tzinfo is not None # --------------------------------------------------------------------------- # Task 2: Mappers # --------------------------------------------------------------------------- def test_to_ui_claim_maps_status_submitted(): from cyclone.store import to_ui_claim claim = _make_claim("CLM-1") # passed=True, frequency_code="1" out = to_ui_claim(claim, batch_id="b1", parsed_at=_PARSED_AT) assert out["id"] == "CLM-1" assert out["status"] == "submitted" assert out["providerNpi"] == "1234567890" assert out["batchId"] == "b1" assert out["parsedAt"] == "2026-06-19T12:00:00Z" def test_to_ui_claim_maps_status_pending_when_warnings_no_freq_one(): """passed + warnings + non-1 frequency_code → pending (rule 2).""" from cyclone.store import to_ui_claim claim = _make_claim("CLM-2", frequency_code="7") claim.validation = ValidationReport( passed=True, errors=[], warnings=[ValidationIssue(rule="W001", severity="warning", message="soft")], ) out = to_ui_claim(claim, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "pending" def test_to_ui_claim_submitted_takes_precedence_over_pending(): """passed + frequency_code=1 + warnings → submitted (rule 1 beats rule 2).""" from cyclone.store import to_ui_claim claim = _make_claim("CLM-3", frequency_code="1") claim.validation = ValidationReport( passed=True, errors=[], warnings=[ValidationIssue(rule="W001", severity="warning", message="soft")], ) out = to_ui_claim(claim, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "submitted" def test_to_ui_claim_maps_status_denied_when_failed(): from cyclone.store import to_ui_claim claim = _make_claim("CLM-4") claim.validation = ValidationReport( passed=False, errors=[ValidationIssue(rule="E001", severity="error", message="hard")], warnings=[], ) out = to_ui_claim(claim, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "denied" def test_to_ui_claim_maps_status_draft_when_r050_diagnosis_present(): """!passed with an R050_diagnosis_present error → draft (carve-out).""" from cyclone.store import to_ui_claim claim = _make_claim("CLM-5") claim.validation = ValidationReport( passed=False, errors=[ValidationIssue( rule="R050_diagnosis_present", severity="error", message="missing principal diagnosis", )], warnings=[], ) out = to_ui_claim(claim, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "draft" def test_to_ui_claim_maps_status_draft_when_passed_no_freq_no_warnings(): """passed + non-1 frequency + no warnings → draft.""" from cyclone.store import to_ui_claim claim = _make_claim("CLM-6", frequency_code="7") out = to_ui_claim(claim, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "draft" def test_to_ui_remittance_maps_status_received_for_paid(): from cyclone.store import to_ui_remittance cp = _make_remit(status_code="1") out = to_ui_remittance(cp, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "received" assert out["denialReason"] is None assert out["validationWarnings"] == [] def test_to_ui_remittance_denial_reason_from_cas_for_code_4(): """CLP02 == 4 → received with denialReason populated from first CAS.""" from cyclone.store import to_ui_remittance sp = ServicePayment( line_number=1, procedure_qualifier="HC", procedure_code="99213", modifiers=[], charge=Decimal("100.00"), payment=Decimal("0.00"), units=None, unit_type=None, service_date=None, adjustments=[ ClaimAdjustment( group_code="CO", reason_code="97", amount=Decimal("100.00"), quantity=None, ), ], ) cp = _make_remit(status_code="4", service_payments=[sp]) out = to_ui_remittance(cp, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "received" assert out["denialReason"] == "CO-97: $100.00" def test_to_ui_remittance_denial_reason_none_when_no_cas_for_code_4(): """CLP02 == 4 with no CAS segments → denialReason stays None.""" from cyclone.store import to_ui_remittance cp = _make_remit(status_code="4", service_payments=[]) out = to_ui_remittance(cp, batch_id="b1", parsed_at=_PARSED_AT) assert out["status"] == "received" assert out["denialReason"] is None def test_to_ui_remittance_validation_warning_for_invalid_clp02(): """CLP02 outside the allowlist → received + validationWarnings populated.""" from cyclone.store import to_ui_remittance cp = _make_remit(status_code="99") # not in {"1","2","3","4","19","20","21","22","23","25"} out = to_ui_remittance( cp, batch_id="b1", parsed_at=_PARSED_AT, payer_config=PayerConfig835.generic_835(), ) assert out["status"] == "received" assert any("99" in w and "allowlist" in w for w in out["validationWarnings"]) def test_to_ui_remittance_no_warning_for_allowed_clp02(): """CLP02 in the allowlist → empty validationWarnings.""" from cyclone.store import to_ui_remittance cp = _make_remit(status_code="1") out = to_ui_remittance( cp, batch_id="b1", parsed_at=_PARSED_AT, payer_config=PayerConfig835.generic_835(), ) assert out["validationWarnings"] == [] def test_to_ui_remittance_fills_payer_name(): """payer_name kwarg flows into the remittance's payerName field.""" from cyclone.store import to_ui_remittance cp = _make_remit(status_code="1") out = to_ui_remittance( cp, batch_id="b1", parsed_at=_PARSED_AT, payer_name="CO_TXIX", ) assert out["payerName"] == "CO_TXIX" def test_to_ui_provider_aggregates_claims(): from cyclone.store import to_ui_provider out = to_ui_provider( npi="1234567890", name="Acme Clinic", tax_id="99-9999999", address="1 Main", city="Denver", state="CO", zip="80202", phone="303-555-0100", claim_count=5, outstanding_ar=250.0, ) assert out["npi"] == "1234567890" assert out["claimCount"] == 5 def test_to_activity_event_serializes_datetime_to_iso_z(): from cyclone.store import to_activity_event out = to_activity_event( id="a1", kind="claim_submitted", message="Claim CLM-1 submitted", timestamp=_PARSED_AT, npi="1234567890", amount=100.0, ) assert out["id"] == "a1" assert out["kind"] == "claim_submitted" assert out["amount"] == 100.0 assert out["timestamp"] == "2026-06-19T12:00:00Z" # --------------------------------------------------------------------------- # Task 3: Iterators # --------------------------------------------------------------------------- def test_iter_claims_filters_by_status(): s = InMemoryStore() result = _make_result() a = _make_claim("A", frequency_code="1") # passed + freq=1 → submitted b = _make_claim("B", frequency_code="7") b.validation = ValidationReport( passed=True, errors=[], warnings=[ValidationIssue(rule="W1", severity="warning", message="x")], ) # passed + freq=7 + warnings → pending result.claims = [a, b] s.add(BatchRecord( id="1", kind="837p", input_filename="a.txt", parsed_at=_PARSED_AT, result=result, )) out = list(s.iter_claims(status="submitted")) assert all(c["status"] == "submitted" for c in out) assert any(c["id"] == "A" for c in out) out_pending = list(s.iter_claims(status="pending")) assert all(c["status"] == "pending" for c in out_pending) assert any(c["id"] == "B" for c in out_pending) def test_iter_claims_filters_by_batch_id(): s = InMemoryStore() s.add(BatchRecord( id="b1", kind="837p", input_filename="a.txt", parsed_at=_PARSED_AT, result=_make_result(), )) out = list(s.iter_claims(batch_id="b1")) assert all(c["batchId"] == "b1" for c in out) def test_iter_claims_sorts_by_billed_amount_desc(): s = InMemoryStore() result = _make_result() result.claims = [ _make_claim("cheap"), _make_claim("expensive"), ] result.claims[0].claim.total_charge = Decimal("50.00") result.claims[1].claim.total_charge = Decimal("500.00") s.add(BatchRecord( id="b1", kind="837p", input_filename="a.txt", parsed_at=_PARSED_AT, result=result, )) out = list(s.iter_claims(sort="billedAmount", order="desc")) assert out[0]["id"] == "expensive" assert out[1]["id"] == "cheap" def test_distinct_providers_aggregates_claims(): s = InMemoryStore() result = _make_result() result.claims = [_make_claim("A"), _make_claim("B")] s.add(BatchRecord( id="b1", kind="837p", input_filename="a.txt", parsed_at=_PARSED_AT, result=result, )) out = s.distinct_providers() assert len(out) == 1 assert out[0]["npi"] == "1234567890" assert out[0]["claimCount"] == 2 def test_recent_activity_returns_newest_first(): older = datetime(2026, 6, 19, 11, 0, 0, tzinfo=timezone.utc) s = InMemoryStore() s.add(BatchRecord( id="b1", kind="837p", input_filename="a.txt", parsed_at=older, result=_make_result(), )) s.add(BatchRecord( id="b2", kind="837p", input_filename="b.txt", parsed_at=_PARSED_AT, result=_make_result(), )) out = s.recent_activity(limit=10) assert out[0]["timestamp"] == "2026-06-19T12:00:00Z" assert out[1]["timestamp"] == "2026-06-19T11:00:00Z" def test_iter_remittances_fills_payer_name_from_batch(): """iter_remittances should populate payerName from ParseResult835.payer.name.""" s = InMemoryStore() cp = _make_remit(status_code="1") s.add(BatchRecord( id="835-1", kind="835", input_filename="a.txt", parsed_at=_PARSED_AT, result=_make_835_result(claims=[cp]), )) out = list(s.iter_remittances()) assert len(out) == 1 assert out[0]["payerName"] == "CO_TXIX"