"""Tests for the outbound 837P serializer (full rebuild approach).""" from datetime import date from decimal import Decimal import pytest from cyclone.parsers.parse_837 import parse from cyclone.parsers.payer import PayerConfig from cyclone.parsers.serialize_837 import ( SerializeError, _build_bht, _build_clm, _build_dmg, _build_dtp_472, _build_gs, _build_hl, _build_isa, _build_lx, _build_n3, _build_n4, _build_nm1, _build_per, _build_ref, _build_ref_g1, _build_se, _build_sbr, _build_st, _build_sv1, serialize_837, serialize_837_for_resubmit, ) _CFG = PayerConfig(name="CO_MEDICAID") def _load_claim(path: str = "tests/fixtures/co_medicaid_837p.txt"): text = open(path).read() result = parse(text, _CFG) assert result.claims, f"no claims parsed from {path}" return result.claims[0] # --------------------------------------------------------------------------- # Envelope helpers # --------------------------------------------------------------------------- def test_build_isa_is_106_chars_with_standard_delimiters(): isa = _build_isa("SENDER", "RECEIVER", "000000001") assert isa.startswith("ISA*") # 106 chars total: 105 content + 1 segment terminator. assert len(isa) == 106 # ISA16 (component separator, ':') sits right before the segment terminator. assert isa.endswith(":~") def test_build_gs_emits_gs_segment_with_hc_functional_id(): gs = _build_gs("SENDER", "RECEIVER", "1") parts = gs.rstrip("~").split("*") assert parts[0] == "GS" assert parts[1] == "HC" assert parts[2] == "SENDER" assert parts[3] == "RECEIVER" assert parts[6] == "1" def test_build_st_emits_837_segment(): st = _build_st("0001") assert st.startswith("ST*837*0001*005010X222A1~") def test_build_se_emits_se_segment(): se = _build_se(10, "0001") assert se == "SE*10*0001~" # --------------------------------------------------------------------------- # Hierarchy helpers # --------------------------------------------------------------------------- def test_build_nm1_non_person_entity_uses_nm103_for_name(): nm1 = _build_nm1("85", "85", "CLINIC A", "XX", "1234567890") parts = nm1.rstrip("~").split("*") assert parts[0] == "NM1" assert parts[1] == "85" assert parts[2] == "2" # non-person assert parts[3] == "CLINIC A" assert parts[8] == "XX" assert parts[9] == "1234567890" def test_build_nm1_person_entity_splits_first_last(): nm1 = _build_nm1("IL", "IL", "Doe Jane", "MI", "M12345") parts = nm1.rstrip("~").split("*") assert parts[1] == "IL" assert parts[2] == "1" assert parts[3] == "Doe" assert parts[4] == "Jane" def test_build_per_returns_empty_when_no_contact(): assert _build_per(None, None) == "" assert _build_per("", "") == "" def test_build_per_emits_segment_with_contact(): per = _build_per("Jane Doe", "5551234567") parts = per.rstrip("~").split("*") assert parts[0] == "PER" assert parts[1] == "IC" assert parts[2] == "Jane Doe" assert parts[3] == "TE" assert parts[4] == "5551234567" def test_build_n3_returns_empty_when_no_address(): assert _build_n3(None, None) == "" assert _build_n3("", "") == "" def test_build_n4_returns_empty_when_no_address(): assert _build_n4(None, None, None) == "" def test_build_dmg_returns_empty_when_no_demographics(): assert _build_dmg(None, None) == "" def test_build_hl_emits_segment(): hl = _build_hl("1", "", "20", "1") assert hl == "HL*1**20*1~" def test_build_sbr_emits_segment(): sbr = _build_sbr("18", "M123", "PAYER") parts = sbr.rstrip("~").split("*") assert parts[0] == "SBR" assert parts[1] == "18" assert parts[9] == "M123" def test_build_ref_returns_empty_when_no_value(): assert _build_ref("EI", None) == "" assert _build_ref("EI", "") == "" def test_build_ref_emits_segment_with_value(): assert _build_ref("EI", "123456789") == "REF*EI*123456789~" # --------------------------------------------------------------------------- # Claim-level segment builders # --------------------------------------------------------------------------- def _stub_claim_header(): from cyclone.parsers.models import ClaimHeader return ClaimHeader( claim_id="CLM-1", total_charge=Decimal("100.00"), place_of_service="11", frequency_code="1", ) def test_build_bht_uses_default_transaction_type_ch_when_none(): bht = _build_bht( transaction_type_code=None, reference_id="REF1", transaction_date=date(2026, 6, 20), transaction_time="1200", ) parts = bht.rstrip("~").split("*") assert parts[0] == "BHT" assert parts[6] == "CH" def test_build_bht_propagates_transaction_type_code(): bht = _build_bht( transaction_type_code="RP", reference_id="REF1", transaction_date=date(2026, 6, 20), transaction_time="1200", ) parts = bht.rstrip("~").split("*") assert parts[6] == "RP" def test_build_clm_emits_clm01_to_clm05(): claim = _stub_claim_header() clm = _build_clm(claim) parts = clm.rstrip("~").split("*") assert parts[0] == "CLM" assert parts[1] == "CLM-1" assert parts[2] == "100.00" # CLM05 is a composite "POS:facility_qualifier:frequency_code" — the stub # has POS="11" and frequency_code="1", no qualifier set. assert parts[5] == "11:1" def test_build_ref_g1_returns_empty_when_no_prior_auth(): assert _build_ref_g1(None) == "" assert _build_ref_g1("") == "" def test_build_ref_g1_emits_segment_when_prior_auth_set(): assert _build_ref_g1("AUTH123") == "REF*G1*AUTH123~" # --------------------------------------------------------------------------- # Service-line segment builders # --------------------------------------------------------------------------- def _stub_service_line(charge="100.00", units=Decimal("1"), date_value=date(2026, 6, 15)): from cyclone.parsers.models import Procedure, ServiceLine return ServiceLine( line_number=1, procedure=Procedure(qualifier="HC", code="99213", modifiers=["25"]), charge=Decimal(charge), units=units, unit_type="UN", service_date=date_value, ) def test_build_lx_emits_lx_segment(): assert _build_lx(1) == "LX*1~" def test_build_sv1_emits_procedure_modifiers_charge_units(): line = _stub_service_line() sv1 = _build_sv1(line) parts = sv1.rstrip("~").split("*") assert parts[0] == "SV1" assert parts[1].startswith("HC:99213") assert ":25" in parts[1] assert parts[2] == "100.00" assert parts[3] == "UN" assert parts[4] == "1" def test_build_dtp_472_emits_service_date(): assert _build_dtp_472(date(2026, 6, 15)) == "DTP*472*D8*20260615~" def test_build_dtp_472_returns_empty_when_no_date(): assert _build_dtp_472(None) == "" # --------------------------------------------------------------------------- # serialize_837 — envelope + body composition # --------------------------------------------------------------------------- def test_serialize_837_envelope_has_106_char_isa(): claim = _load_claim() text = serialize_837(claim) # The ISA segment is 106 chars including the segment terminator. isa_segment = next(part + "~" for part in text.split("~") if part.startswith("ISA")) assert len(isa_segment) == 106 def test_serialize_837_emits_envelope_in_order(): claim = _load_claim() text = serialize_837(claim) seg_ids = [seg.split("*")[0] for seg in text.split("~") if seg] # Envelope order assert seg_ids[0] == "ISA" assert seg_ids[1] == "GS" assert seg_ids[2] == "ST" assert seg_ids[3] == "BHT" # Termination assert seg_ids[-2] == "GE" assert seg_ids[-1] == "IEA" # Body order (after BHT, before SE) body = seg_ids[3:-2] assert "CLM" in body assert "HI" in body assert "NM1" in body assert "HL" in body assert "SV1" in body def test_serialize_837_round_trips_canonical_fields(): claim = _load_claim() text = serialize_837(claim) reparsed = parse(text, _CFG).claims[0] # Canonical header fields assert reparsed.claim.claim_id == claim.claim.claim_id assert Decimal(reparsed.claim.total_charge) == claim.claim.total_charge assert reparsed.claim.frequency_code == claim.claim.frequency_code assert reparsed.claim.place_of_service == claim.claim.place_of_service # Diagnoses assert [d.code for d in reparsed.diagnoses] == [d.code for d in claim.diagnoses] # Service lines (charge, units) assert len(reparsed.service_lines) == len(claim.service_lines) for src, out in zip(claim.service_lines, reparsed.service_lines): assert out.procedure.code == src.procedure.code assert Decimal(out.charge) == src.charge def test_serialize_837_edited_charge_propagates(): claim = _load_claim() claim.claim.total_charge = Decimal("999.99") text = serialize_837(claim) assert "CLM*" in text and "*999.99*" in text reparsed = parse(text, _CFG).claims[0] assert Decimal(reparsed.claim.total_charge) == Decimal("999.99") def test_serialize_837_edited_frequency_propagates(): claim = _load_claim() claim.claim.frequency_code = "7" text = serialize_837(claim) reparsed = parse(text, _CFG).claims[0] assert reparsed.claim.frequency_code == "7" def test_serialize_837_edited_prior_auth_propagates(): claim = _load_claim() claim.claim.prior_auth = "AUTH999" text = serialize_837(claim) assert "REF*G1*AUTH999" in text reparsed = parse(text, _CFG).claims[0] assert reparsed.claim.prior_auth == "AUTH999" def test_serialize_837_added_diagnosis_propagates(): claim = _load_claim() from cyclone.parsers.models import Diagnosis claim.diagnoses.append(Diagnosis(code="Z00.00", qualifier="BF")) text = serialize_837(claim) assert "BF:Z00.00" in text reparsed = parse(text, _CFG).claims[0] codes = [d.code for d in reparsed.diagnoses] assert "Z00.00" in codes def test_serialize_837_edited_service_line_charge_propagates(): claim = _load_claim() claim.service_lines[0].charge = Decimal("555.55") text = serialize_837(claim) assert "555.55" in text reparsed = parse(text, _CFG).claims[0] assert any(Decimal(sl.charge) == Decimal("555.55") for sl in reparsed.service_lines) def test_serialize_837_edited_service_date_propagates(): claim = _load_claim() claim.service_lines[0].service_date = date(2027, 1, 15) text = serialize_837(claim) assert "20270115" in text def test_serialize_837_uses_custom_sender_receiver_ids(): claim = _load_claim() text = serialize_837( claim, sender_id="AXISCARE", receiver_id="CO_MED", submitter_name="AXISCARE BILLING", ) assert "AXISCARE" in text assert "CO_MED" in text # Sanity: file still parses parse(text, _CFG) def test_serialize_error_is_an_exception(): assert issubclass(SerializeError, Exception) # --------------------------------------------------------------------------- # serialize_837_for_resubmit # --------------------------------------------------------------------------- def test_serialize_837_for_resubmit_assigns_unique_control_numbers(): claim = _load_claim() text_a = serialize_837_for_resubmit(claim, interchange_index=42) text_b = serialize_837_for_resubmit(claim, interchange_index=43) isa_a = next(line for line in text_a.split("~") if line.startswith("ISA")) isa_b = next(line for line in text_b.split("~") if line.startswith("ISA")) assert isa_a != isa_b assert "000000042" in isa_a assert "000000043" in isa_b