2893676c0b
X12 835 SVC segment: SVC01 = composite procedure SVC02 = charge SVC03 = payment SVC04 = Unit or Basis for Measurement Code (UN, MJ, DA, ...) SVC05 = Service Unit Count The parser previously read SVC04 as the units count and SVC05 as the unit type — backwards. On real 835s (and the canonical minimal fixture), SVC04 carries the code 'UN' which fails Decimal parsing, so the units always came out as None and the code string was assigned to unit_type. SP7's line-level matcher couldn't compare units on the SVC side against the claim side because of this. - _consume_service_payment: SVC04 → unit_type, SVC05 → units count - Default unit_type to 'UN' when only the count is present - minimal_835.txt + unbalanced_835.txt: swap positions to match spec - Add 2 regression tests (units-and-unit-type, default-unit-type-to-UN)
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
"""Tests for the 835 ERA parser orchestrator."""
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cyclone.parsers.exceptions import CycloneParseError
|
|
from cyclone.parsers.parse_835 import parse
|
|
from cyclone.parsers.payer import PayerConfig835
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "minimal_835.txt"
|
|
|
|
|
|
def test_parse_minimal_fixture_returns_one_claim():
|
|
text = FIXTURE.read_text()
|
|
result = parse(text, PayerConfig835.co_medicaid_835())
|
|
assert len(result.claims) == 1
|
|
c = result.claims[0]
|
|
assert c.payer_claim_control_number == "CLM001"
|
|
assert c.status_code == "1"
|
|
assert c.status_label == "Processed as Primary"
|
|
assert c.total_charge == 100.00
|
|
assert c.total_paid == 85.00
|
|
assert c.patient_responsibility == 15.00
|
|
assert c.claim_filing_indicator == "MC"
|
|
assert c.facility_type == "12"
|
|
assert c.frequency_code == "1"
|
|
|
|
|
|
def test_parse_envelope_captured():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
assert result.envelope is not None
|
|
assert result.envelope.control_number == "991102984"
|
|
assert result.envelope.transaction_date == date(2026, 6, 17)
|
|
assert result.envelope.sender_id == "COMEDASSISTPROG"
|
|
assert result.envelope.implementation_guide == "005010X221A1"
|
|
|
|
|
|
def test_parse_financial_info():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
f = result.financial_info
|
|
assert f.handling_code == "C"
|
|
assert f.paid_amount == 85.00
|
|
assert f.credit_debit_flag == "C"
|
|
assert f.payment_method == "ACH"
|
|
assert f.payer_tax_id == "81-1725341"
|
|
assert f.payment_date == date(2026, 6, 17)
|
|
|
|
|
|
def test_parse_trace():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
t = result.trace
|
|
assert t.trace_type_code == "1"
|
|
# X12 standard: TRN02 = Reference Identification (trace number),
|
|
# TRN03 = Originating Company Identifier. The CO guide labels
|
|
# TRN03 as the Payer Tax ID, so for CO Medicaid 835s the
|
|
# originating_company_id is "81-1725341" and the trace number is
|
|
# the provider's TPID.
|
|
assert t.trace_number == "TPID001"
|
|
assert t.originating_company_id == "81-1725341"
|
|
assert t.payer_id is None # TRN04 is not present in this fixture
|
|
|
|
|
|
def test_parse_payer_and_payee():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
p = result.payer
|
|
assert p.name == "CO_TXIX"
|
|
assert p.id == "7912900843"
|
|
assert p.contact_url == "hcpf.colorado.gov"
|
|
assert p.address is not None
|
|
assert p.address.city == "DENVER"
|
|
pe = result.payee
|
|
assert pe.name == "TOC, Inc."
|
|
assert pe.npi == "1881068062"
|
|
assert pe.address is not None
|
|
assert pe.address.zip == "814014063"
|
|
|
|
|
|
def test_parse_claim_has_two_service_lines():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
c = result.claims[0]
|
|
assert len(c.service_payments) == 2
|
|
s1, s2 = c.service_payments
|
|
assert s1.line_number == 1
|
|
assert s1.procedure_code == "S5130"
|
|
assert s1.procedure_qualifier == "HC"
|
|
assert s1.modifiers == ["U2"]
|
|
assert s1.payment == 50.00
|
|
assert s1.service_date == date(2026, 6, 2)
|
|
assert s2.line_number == 2
|
|
assert s2.payment == 35.00
|
|
assert s2.service_date == date(2026, 6, 3)
|
|
|
|
|
|
def test_parse_summary_counts():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
assert result.summary.total_claims == 1
|
|
assert result.summary.input_file == ""
|
|
# The orchestrator sets passed/failed from the validation report.
|
|
# Without running validation it remains 0 — the CLI / API does that.
|
|
assert result.summary.passed == 0
|
|
|
|
|
|
def test_parse_preserves_raw_segments():
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
c = result.claims[0]
|
|
assert any(s[0] == "CLP" for s in c.raw_segments)
|
|
assert any(s[0] == "SVC" for s in c.raw_segments)
|
|
|
|
|
|
def test_parse_uses_generic_config():
|
|
"""Generic config shouldn't suppress parser errors but accepts any payer."""
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.generic_835())
|
|
assert len(result.claims) == 1
|
|
|
|
|
|
def test_parse_no_bpr_raises():
|
|
text = (
|
|
"ISA*00* *00* *ZZ*COMEDASSISTPROG*ZZ*TPID001 *260617*1937*^*00501*991102990*1*P*:~"
|
|
"ST*835*991102990*005010X221A1~"
|
|
"SE*1*991102990~"
|
|
"GE*1*991102990~"
|
|
"IEA*1*991102990~"
|
|
)
|
|
with pytest.raises(CycloneParseError, match="No BPR"):
|
|
parse(text, PayerConfig835.co_medicaid_835())
|
|
|
|
|
|
def test_parse_service_payment_units_and_unit_type():
|
|
"""X12 005010X221A1: SVC04 = unit-of-measure, SVC05 = service unit count.
|
|
|
|
Bug history: the parser previously read SVC04 as ``units`` and SVC05
|
|
as ``unit_type`` (with the comment stating the opposite of the spec).
|
|
On real-world 835s — and the canonical minimal fixture — this left
|
|
``units=None`` because SVC04 carries the string "UN" (which fails
|
|
``Decimal`` parsing) and SVC05 (a count like "7.73") got assigned to
|
|
``unit_type``.
|
|
|
|
This test pins down the correct mapping so SP7's line-level matcher
|
|
can compare units on the SVC side against the claim side.
|
|
"""
|
|
from decimal import Decimal
|
|
result = parse(FIXTURE.read_text(), PayerConfig835.co_medicaid_835())
|
|
c = result.claims[0]
|
|
s1, s2 = c.service_payments
|
|
# minimal_835.txt fixture: SVC*HC:S5130:U2*60.00*50.00*UN*7.73*date*
|
|
assert s1.units == Decimal("7.73"), s1.units
|
|
assert s1.unit_type == "UN"
|
|
assert s2.units == Decimal("7.73"), s2.units
|
|
assert s2.unit_type == "UN"
|
|
|
|
|
|
def test_parse_service_payment_units_default_unit_type_to_un():
|
|
"""If SVC04 is empty but SVC05 carries a count, default unit_type to UN.
|
|
|
|
Some payers omit the unit-of-measure code when the units are obvious
|
|
(e.g. always "UN"). The parser should still parse the units and
|
|
surface a reasonable default for ``unit_type`` so downstream
|
|
consumers (validation, SP7 line matcher) have a non-None value.
|
|
"""
|
|
from decimal import Decimal
|
|
base = FIXTURE.read_text()
|
|
text = (
|
|
base
|
|
.replace(
|
|
"SVC*HC:S5130:U2*60.00*50.00*UN*7.73*20260602~",
|
|
"SVC*HC:S5130:U2*60.00*50.00**3*20260602~",
|
|
)
|
|
.replace(
|
|
"SVC*HC:S5130:U2*40.00*35.00*UN*7.73*20260603~",
|
|
"SVC*HC:S5130:U2*40.00*35.00**4*20260603~",
|
|
)
|
|
)
|
|
result = parse(text, PayerConfig835.co_medicaid_835())
|
|
s1, s2 = result.claims[0].service_payments
|
|
assert s1.units == Decimal("3"), s1.units
|
|
assert s2.units == Decimal("4"), s2.units
|
|
# Default to UN when SVC04 missing but units present.
|
|
assert s1.unit_type == "UN", s1.unit_type
|
|
assert s2.unit_type == "UN", s2.unit_type
|