"""Tests for the 277CA Claim Acknowledgment parser. SP10 T1. The 277CA is the semantic claim-level ack from the payer — the parser must walk the HL hierarchy and capture one ClaimStatus per Patient HL, with REF*1K (payer_claim_control_number) and the STC category code. """ from __future__ import annotations import json from datetime import date from pathlib import Path import pytest from cyclone.parsers.exceptions import CycloneParseError from cyclone.parsers.models_277ca import ( ACCEPTED_CODES, PAID_CODES, PENDED_CODES, REJECTED_CODES, ParseResult277CA, classify_status_code, ) from cyclone.parsers.parse_277ca import parse_277ca_text FIXTURE_DIR = Path(__file__).parent / "fixtures" # --------------------------------------------------------------------------- # # Status-code classification # --------------------------------------------------------------------------- # class TestClassifyStatusCode: def test_accepted_codes(self): for code in ACCEPTED_CODES: assert classify_status_code(code) == "accepted" def test_rejected_codes(self): for code in REJECTED_CODES: assert classify_status_code(code) == "rejected" def test_pended_codes(self): for code in PENDED_CODES: assert classify_status_code(code) == "pended" def test_paid_codes(self): for code in PAID_CODES: assert classify_status_code(code) == "paid" def test_unknown_code_does_not_raise(self): """Unknown codes (e.g. payer-specific) surface as 'unknown', not error.""" assert classify_status_code("ZZ") == "unknown" assert classify_status_code("") == "unknown" assert classify_status_code(" ") == "unknown" def test_case_insensitive(self): assert classify_status_code("a3") == "accepted" assert classify_status_code("A6") == "rejected" # --------------------------------------------------------------------------- # # Parser: minimal happy path with mixed statuses # --------------------------------------------------------------------------- # class TestParse277CAMixed: @pytest.fixture def parsed(self) -> ParseResult277CA: text = (FIXTURE_DIR / "minimal_277ca.txt").read_text() return parse_277ca_text(text, input_file="minimal_277ca.txt") def test_envelope_control_number(self, parsed): assert parsed.envelope.control_number == "000000123" assert parsed.envelope.implementation_guide == "005010X214" def test_bht_captures_reference_and_date(self, parsed): assert parsed.bht.hierarchical_structure_code == "0085" assert parsed.bht.transaction_set_purpose_code == "08" assert parsed.bht.reference_identification == "REFNUM001" assert parsed.bht.transaction_set_creation_date == date(2024, 6, 20) assert parsed.bht.transaction_type_code == "TH" def test_three_patient_hls_three_statuses(self, parsed): assert len(parsed.claim_statuses) == 3 def test_first_status_is_accepted(self, parsed): s = parsed.claim_statuses[0] assert s.payer_claim_control_number == "CLAIM001" assert s.billing_provider_tax_id == "721587149" assert s.status_code == "A3" assert s.classification == "accepted" assert s.total_claim_charge_amount == 100.00 def test_second_status_is_rejected(self, parsed): s = parsed.claim_statuses[1] assert s.payer_claim_control_number == "CLAIM002" assert s.status_code == "A6" assert s.classification == "rejected" assert s.total_claim_charge_amount == 250.00 def test_third_status_is_pended(self, parsed): s = parsed.claim_statuses[2] assert s.payer_claim_control_number == "CLAIM003" assert s.status_code == "A8" assert s.classification == "pended" def test_service_date_parsed_from_dtp_472(self, parsed): """DTP*472 with RD8 fmt should return the start date.""" s = parsed.claim_statuses[0] assert s.service_date == date(2024, 6, 15) def test_summary_counts(self, parsed): # accepted=1 (A3), rejected=1 (A6), pended=1 (A8) assert parsed.summary.total_claims == 3 assert parsed.summary.passed == 1 # only "accepted" assert parsed.summary.failed == 1 # only "rejected" (pended excluded) def test_round_trips_via_json(self, parsed): blob = json.loads(parsed.model_dump_json()) assert blob["envelope"]["control_number"] == "000000123" assert len(blob["claim_statuses"]) == 3 rebuilt = ParseResult277CA.model_validate(blob) assert rebuilt.claim_statuses[1].status_code == "A6" # --------------------------------------------------------------------------- # # Parser: ST*277 (instead of ST*277CA) — X12 spec also allows it # --------------------------------------------------------------------------- # class TestParse277CAAltST: def test_st_277_accepted(self): text = (FIXTURE_DIR / "minimal_277ca_st277.txt").read_text() result = parse_277ca_text(text, input_file="minimal_277ca_st277.txt") assert len(result.claim_statuses) == 1 assert result.claim_statuses[0].classification == "accepted" def test_st_wrong_value_rejected(self): """A non-277 ST segment should raise CycloneParseError.""" text = ( "ISA*00* *00* *ZZ*AAAAAAAAAAAAAAA*ZZ*BBBBBBBBBBBBBBB*240620*1200*^*00501*000000001*0*P*:~" "GS*HN*A*B*20240620*1200*1*X*005010X214~" "ST*835*0001*005010X221A1~" "BHT*0085*08*X*20240620*1200*TH~" "SE*3*0001~" "GE*1*1~" "IEA*1*000000001~" ) with pytest.raises(CycloneParseError, match="ST\\*277"): parse_277ca_text(text) # --------------------------------------------------------------------------- # # Parser: rejected-only fixture (single A7) # --------------------------------------------------------------------------- # class TestParse277CARejectedOnly: def test_single_a7_rejected(self): text = (FIXTURE_DIR / "minimal_277ca_rejected_only.txt").read_text() result = parse_277ca_text(text, input_file="minimal_277ca_rejected_only.txt") assert len(result.claim_statuses) == 1 s = result.claim_statuses[0] assert s.status_code == "A7" assert s.classification == "rejected" assert s.payer_claim_control_number == "CLAIM099" # --------------------------------------------------------------------------- # # Parser: error handling # --------------------------------------------------------------------------- # class TestParse277CAErrors: def test_missing_isa_raises(self): """No ISA envelope → CycloneParseError, never silent fail.""" with pytest.raises(CycloneParseError, match="ISA"): parse_277ca_text("not a valid 277ca\n") def test_empty_input_raises(self): with pytest.raises(CycloneParseError): parse_277ca_text("") # --------------------------------------------------------------------------- # # Parser: multiple STC per patient (last wins) # --------------------------------------------------------------------------- # class TestParse277CAMultipleStcPerPatient: def test_last_stc_wins_per_patient(self): """A patient HL with multiple STC segments: the last one wins. This is the canonical pattern for "first pended, then paid" acknowledgments — the most recent action is authoritative. """ text = ( "ISA*00* *00* *ZZ*AAAAAAAAAAAAAAA*ZZ*BBBBBBBBBBBBBBB*240620*1200*^*00501*000000001*0*P*:~" "GS*HN*A*B*20240620*1200*1*X*005010X214~" "ST*277CA*0001*005010X214~" "BHT*0085*08*X*20240620*1200*TH~" "HL*1**20*1~" "HL*2*1*21*1~" "HL*3*2*19*1~" "HL*4*3*PT~" "REF*1K*CLAIM555~" "STC*A1:19:PR*20240620*WQ*100.00~" "STC*A8:19:PR*20240620*WQ*100.00~" "STC*A3:19:PR*20240620*WQ*100.00~" "SE*9*0001~" "GE*1*1~" "IEA*1*000000001~" ) result = parse_277ca_text(text) assert len(result.claim_statuses) == 1 assert result.claim_statuses[0].status_code == "A3" assert result.claim_statuses[0].classification == "accepted" # --------------------------------------------------------------------------- # # Parser: subscriber-level STCs surface in unscoped_statuses # --------------------------------------------------------------------------- # class TestParse277CASubscriberLevelStc: def test_subscriber_stc_goes_to_unscoped(self): """Subscriber-level (HL*19) STCs without a Patient child go to unscoped_statuses.""" text = ( "ISA*00* *00* *ZZ*AAAAAAAAAAAAAAA*ZZ*BBBBBBBBBBBBBBB*240620*1200*^*00501*000000001*0*P*:~" "GS*HN*A*B*20240620*1200*1*X*005010X214~" "ST*277CA*0001*005010X214~" "BHT*0085*08*X*20240620*1200*TH~" "HL*1**20*1~" "HL*2*1*21*1~" "HL*3*2*19*1~" "STC*A6:19:PR*20240620*U~" "SE*7*0001~" "GE*1*1~" "IEA*1*000000001~" ) result = parse_277ca_text(text) assert len(result.claim_statuses) == 0 assert len(result.unscoped_statuses) == 1 assert result.unscoped_statuses[0].status_code == "A6" assert result.unscoped_statuses[0].classification == "rejected"