261 lines
9.9 KiB
Python
261 lines
9.9 KiB
Python
"""Tests for the 835 ERA validator rules."""
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cyclone.parsers.models import Address, BatchSummary, Envelope
|
|
from cyclone.parsers.models_835 import (
|
|
ClaimAdjustment,
|
|
ClaimPayment,
|
|
FinancialInfo,
|
|
ParseResult835,
|
|
Payer835,
|
|
Payee835,
|
|
ReassociationTrace,
|
|
ServicePayment,
|
|
)
|
|
from cyclone.parsers.payer import PayerConfig835
|
|
from cyclone.parsers.validator_835 import validate
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Builders
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def _build_result(**overrides) -> ParseResult835:
|
|
"""Build a passing CO Medicaid 835 result; override fields to break rules."""
|
|
financial = FinancialInfo(
|
|
handling_code="C",
|
|
paid_amount=Decimal("85.00"),
|
|
credit_debit_flag="C",
|
|
payment_method="ACH",
|
|
payer_tax_id="81-1725341",
|
|
payment_date=date(2026, 6, 17),
|
|
)
|
|
trace = ReassociationTrace(
|
|
trace_type_code="1",
|
|
trace_number="TPID001",
|
|
originating_company_id="TPID001",
|
|
payer_id="81-1725341",
|
|
)
|
|
payer = Payer835(
|
|
name="CO_TXIX",
|
|
id="7912900843",
|
|
address=Address(line1="PO BOX 11000", city="DENVER", state="CO", zip="802110000"),
|
|
)
|
|
payee = Payee835(
|
|
name="TOC, Inc.",
|
|
npi="1881068062",
|
|
address=Address(line1="1100 East Main St", city="Montrose", state="CO", zip="814014063"),
|
|
)
|
|
claim = ClaimPayment(
|
|
payer_claim_control_number="CLM001",
|
|
status_code="1",
|
|
status_label="Processed as Primary",
|
|
total_charge=Decimal("100.00"),
|
|
total_paid=Decimal("85.00"),
|
|
patient_responsibility=Decimal("15.00"),
|
|
claim_filing_indicator="MC",
|
|
original_claim_id="CLM001",
|
|
facility_type="12",
|
|
frequency_code="1",
|
|
per_diem_covered_days=None,
|
|
ref_benefit_plan=None,
|
|
service_payments=[
|
|
ServicePayment(
|
|
line_number=1,
|
|
procedure_qualifier="HC",
|
|
procedure_code="S5130",
|
|
modifiers=["U2"],
|
|
charge=Decimal("60.00"),
|
|
payment=Decimal("50.00"),
|
|
units=Decimal("7.73"),
|
|
unit_type="UN",
|
|
service_date=date(2026, 6, 2),
|
|
adjustments=[],
|
|
ref_benefit_plan=None,
|
|
),
|
|
ServicePayment(
|
|
line_number=2,
|
|
procedure_qualifier="HC",
|
|
procedure_code="S5130",
|
|
modifiers=["U2"],
|
|
charge=Decimal("40.00"),
|
|
payment=Decimal("35.00"),
|
|
units=Decimal("7.73"),
|
|
unit_type="UN",
|
|
service_date=date(2026, 6, 3),
|
|
adjustments=[],
|
|
ref_benefit_plan=None,
|
|
),
|
|
],
|
|
raw_segments=[],
|
|
)
|
|
summary = BatchSummary(input_file="f", total_claims=1, passed=1, failed=0)
|
|
result = ParseResult835(
|
|
envelope=Envelope(
|
|
sender_id="COMEDASSISTPROG",
|
|
receiver_id="TPID001",
|
|
control_number="991102984",
|
|
transaction_date=date(2026, 6, 17),
|
|
),
|
|
financial_info=financial,
|
|
trace=trace,
|
|
payer=payer,
|
|
payee=payee,
|
|
claims=[claim],
|
|
summary=summary,
|
|
)
|
|
if overrides:
|
|
result = result.model_copy(update=overrides)
|
|
return result
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Sanity: balanced claim passes
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_balanced_claim_passes_all_rules():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
report = validate(_build_result(), cfg)
|
|
assert report.passed is True
|
|
assert report.errors == []
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# BPR rules
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_r835_bpr01_handling_code_disallowed():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"handling_code": "Z"}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_BPR01_handling_code_allowed" for i in report.errors)
|
|
assert report.passed is False
|
|
|
|
|
|
def test_r835_bpr02_positive():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"paid_amount": Decimal("0.00")}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_BPR02_positive" for i in report.errors)
|
|
|
|
|
|
def test_r835_bal_bpr_vs_clp04_unbalanced():
|
|
"""BPR02 (85.00) must equal sum of CLP04 (85.00). Off by 0.50 → error."""
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"paid_amount": Decimal("85.50")}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_BAL_BPR_vs_CLP04" for i in report.errors)
|
|
assert report.passed is False
|
|
|
|
|
|
def test_r835_bal_bpr_vs_clp04_within_tolerance_passes():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"paid_amount": Decimal("85.005")}))
|
|
report = validate(result, cfg)
|
|
# 0.005 difference is below tolerance (0.01) — no error.
|
|
assert not any(i.rule == "R835_BAL_BPR_vs_CLP04" for i in report.errors)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# CLP rules
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_r835_bal_clp04_vs_svc03_unbalanced():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
# The default fixture has CLP04=85.00 and SVC03 sum = 50+35 = 85.00.
|
|
# Change one SVC payment to 40 → sum = 40+35 = 75, off by 10.
|
|
base = _build_result()
|
|
svc = base.claims[0].service_payments[0].model_copy(update={"payment": Decimal("40.00")})
|
|
result = base.model_copy(update={"claims": [base.claims[0].model_copy(update={"service_payments": [svc, base.claims[0].service_payments[1]]})]})
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_BAL_CLP04_vs_SVC03" for i in report.errors)
|
|
assert "CLM001" in [i.message for i in report.errors if i.rule == "R835_BAL_CLP04_vs_SVC03"][0]
|
|
|
|
|
|
def test_r835_clp01_present():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(claims=[_build_result().claims[0].model_copy(update={"payer_claim_control_number": ""})])
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_CLP01_present" for i in report.errors)
|
|
|
|
|
|
def test_r835_clp02_status_disallowed():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(claims=[_build_result().claims[0].model_copy(update={"status_code": "9", "status_label": None})])
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_CLP02_status_allowed" for i in report.errors)
|
|
|
|
|
|
def test_r835_clp03_present():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(claims=[_build_result().claims[0].model_copy(update={"total_charge": Decimal("0.00")})])
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_CLP03_present" for i in report.errors)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Payer / payee rules
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_r835_payer_tax_id_mismatch_warning():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"payer_tax_id": "99-9999999"}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_PAYER_TAX_ID_matches" and i.severity == "warning" for i in report.warnings)
|
|
# Warning only — report still passes.
|
|
assert report.passed is True
|
|
|
|
|
|
def test_r835_payer_tax_id_skipped_when_set_empty():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"payer_tax_id": None}))
|
|
report = validate(result, cfg)
|
|
# When BPR10 is empty, the rule is skipped silently (per spec wording).
|
|
assert not any(i.rule == "R835_PAYER_TAX_ID_matches" for i in report.errors + report.warnings)
|
|
|
|
|
|
def test_r835_payer_health_plan_id_mismatch_warning():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(payer=_build_result().payer.model_copy(update={"id": "12345"}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_PAYER_HEALTH_PLAN_ID_matches" and i.severity == "warning" for i in report.warnings)
|
|
assert report.passed is True
|
|
|
|
|
|
def test_r835_payee_npi_format():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(payee=_build_result().payee.model_copy(update={"npi": "12345"}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_PAYEE_NPI_format" for i in report.errors)
|
|
assert report.passed is False
|
|
|
|
|
|
def test_r835_trn02_originating_company_id_present():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
result = _build_result(trace=_build_result().trace.model_copy(update={"originating_company_id": ""}))
|
|
report = validate(result, cfg)
|
|
assert any(i.rule == "R835_REGN01_trn02_present" for i in report.errors)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Optional: warnings don't fail the report
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_warnings_do_not_set_passed_false():
|
|
cfg = PayerConfig835.co_medicaid_835()
|
|
# A wrong payer tax id is a warning; report should still pass.
|
|
result = _build_result(financial_info=_build_result().financial_info.model_copy(update={"payer_tax_id": "BAD"}))
|
|
report = validate(result, cfg)
|
|
assert report.passed is True
|
|
assert any(i.severity == "warning" for i in report.warnings)
|