"""Tests for the 270 (Eligibility Benefit Inquiry) serializer. SP3 Phase 4 (T22 + T25): build a ``ParseResult270``, serialize to X12, then re-parse and assert the round-trip is stable. The cross- parser round-trip test (T25) lives at the bottom of this file. """ from __future__ import annotations from datetime import date import pytest from cyclone.parsers.models import BatchSummary, Envelope from cyclone.parsers.models_270 import ( EligibilityBenefitInquiry, InformationReceiver270, InformationSource270, ParseResult270, Subscriber270, ) from cyclone.parsers.parse_270 import parse from cyclone.parsers.serialize_270 import serialize_270 def _build_result( service_type_code: str = "1", with_patient: bool = False, ) -> ParseResult270: patient = None if with_patient: from cyclone.parsers.models_270 import Patient270 patient = Patient270( first_name="JANE", last_name="DOE", dob=date(2010, 5, 5), relationship="19", ) return ParseResult270( envelope=Envelope( sender_id="SUBMITTERID", receiver_id="RECEIVERID", control_number="000000001", transaction_date=date(2024, 1, 1), implementation_guide="005010X279A1", ), information_source=InformationSource270(name="PAYER NAME", id="SKCO0"), information_receiver=InformationReceiver270(name="PROVIDER NAME", npi="1234567890"), subscriber=Subscriber270( member_id="MEMBER123", first_name="JOHN", last_name="DOE", dob=date(1980, 1, 1), ), patient=patient, inquiries=[EligibilityBenefitInquiry(service_type_code=service_type_code)], summary=BatchSummary( input_file="round_trip.txt", total_claims=1, passed=1, failed=0, ), ) def test_serialize_270_envelope_segments(): """Output starts with ``ISA*`` and ends with ``IEA*``; canonical envelope layers are all present in order. """ text = serialize_270(_build_result()) assert text.startswith("ISA*") # Strip trailing newline if any, then check the IEA tail. body = text.rstrip("\n") assert body.endswith("~IEA*1*000000001~") # Sender/receiver ids in the GS are padded to 15 chars (X12 fixed width). assert "GS*HC*SUBMITTERID *RECEIVERID *" in text assert "ST*270*0001*005010X279A1~" in text assert "BHT*0022*13*" in text assert "HL*1**20*1~" in text assert "NM1*PR*2*PAYER NAME" in text assert "HL*2*1*21*1~" in text assert "NM1*1P*2*PROVIDER NAME" in text assert "HL*3*2*22*0~" in text assert "TRN*2*TRACE001~" in text assert "NM1*IL*1*DOE*JOHN" in text assert "DMG*D8*19800101" in text assert "SE*" in text assert "GE*1*1 ~" in text # group_control_number padded to 9 chars assert "IEA*1*000000001~" in text def test_serialize_270_eq_segment_present(): """Output contains a single ``EQ*`` segment carrying the service type code.""" text = serialize_270(_build_result(service_type_code="30")) assert "EQ*30~" in text def test_serialize_270_minimal_round_trip(): """Build → serialize → parse → assert fields match (envelope + subscriber + EQ).""" original = _build_result(service_type_code="1") text = serialize_270(original) re_parsed = parse(text, input_file="round_trip.txt") # Envelope assert re_parsed.envelope.sender_id.strip() == original.envelope.sender_id assert re_parsed.envelope.receiver_id.strip() == original.envelope.receiver_id assert re_parsed.envelope.control_number == "000000001" assert re_parsed.envelope.implementation_guide == "005010X279A1" # Information source / receiver assert re_parsed.information_source.name == "PAYER NAME" assert re_parsed.information_source.id == "SKCO0" assert re_parsed.information_receiver.name == "PROVIDER NAME" assert re_parsed.information_receiver.npi == "1234567890" # Subscriber assert re_parsed.subscriber.first_name == "JOHN" assert re_parsed.subscriber.last_name == "DOE" assert re_parsed.subscriber.member_id == "MEMBER123" assert re_parsed.subscriber.dob == date(1980, 1, 1) # Inquiry assert len(re_parsed.inquiries) == 1 assert re_parsed.inquiries[0].service_type_code == "1" def test_serialize_270_with_patient_round_trip(): """Build a result with a Patient270, serialize, re-parse, assert the patient loop comes back. """ original = _build_result(with_patient=True) text = serialize_270(original) re_parsed = parse(text, input_file="round_trip_with_patient.txt") assert re_parsed.patient is not None assert re_parsed.patient.first_name == "JANE" assert re_parsed.patient.last_name == "DOE" assert re_parsed.patient.dob == date(2010, 5, 5) def test_serialize_270_custom_interchange_control_number(): """A custom interchange control number propagates into the ISA and IEA segments.""" text = serialize_270(_build_result(), interchange_control_number="000000042") assert "ISA*00* *00* *ZZ*SUBMITTERID *ZZ*RECEIVERID" in text # IEA*1*~ at the end. assert text.rstrip("\n").endswith("~IEA*1*000000042~") # ISA also carries it. assert "*000000042*" in text.split("~", 1)[0] # --------------------------------------------------------------------------- # # T25 — Cross-parser round-trip (full) # --------------------------------------------------------------------------- # def test_cross_parser_270_round_trip(): """Build a fully populated ParseResult270, serialize, parse, assert each field matches the original (except the raw envelope control_number which the serializer regenerates from defaults). """ original = _build_result( service_type_code="88", with_patient=True, ) text = serialize_270(original) re_parsed = parse(text, input_file="cross.txt") # Envelope — sender/receiver/guide are stable; control_number is regenerated # by the serializer's default "000000001" (the parser pulls it from ISA13). assert re_parsed.envelope.control_number == "000000001" assert re_parsed.envelope.sender_id.strip() == "SUBMITTERID" assert re_parsed.envelope.receiver_id.strip() == "RECEIVERID" assert re_parsed.envelope.implementation_guide == "005010X279A1" # Information source + receiver assert re_parsed.information_source.name == original.information_source.name assert re_parsed.information_source.id == original.information_source.id assert re_parsed.information_receiver.name == original.information_receiver.name assert re_parsed.information_receiver.npi == original.information_receiver.npi # Subscriber assert re_parsed.subscriber.member_id == original.subscriber.member_id assert re_parsed.subscriber.first_name == original.subscriber.first_name assert re_parsed.subscriber.last_name == original.subscriber.last_name assert re_parsed.subscriber.dob == original.subscriber.dob # Patient (set on the original). assert re_parsed.patient is not None assert re_parsed.patient.first_name == original.patient.first_name assert re_parsed.patient.last_name == original.patient.last_name assert re_parsed.patient.dob == original.patient.dob # Inquiries assert len(re_parsed.inquiries) == len(original.inquiries) for got, exp in zip(re_parsed.inquiries, original.inquiries): assert got.service_type_code == exp.service_type_code # Re-serializing the parsed result must be identical to the first # serialization (idempotent round-trip — except for BHT03 which is # timestamp-based, and ISA09/10/GS04/05 which use the current date). text2 = serialize_270(re_parsed) # Normalize both texts to drop the date/time-driven segments before # comparing for "shape" stability: we keep envelope, HL/NM1, EQ. def _shape(t: str) -> list[str]: return [ s for s in t.split("~") if s and not s.startswith("BHT") and not s.startswith("ISA") and not s.startswith("GS") and not s.startswith("GE") and not s.startswith("IEA") ] assert _shape(text) == _shape(text2)