Files
cyclone/backend/tests/test_validator.py
T
Tyler 1942a22629 feat(sp20): NPI Luhn checksum + Tax ID (EIN) format validation
Adds pure local validators for the 10-digit NPI Luhn checksum (CMS-
published algorithm with the '80840' NPPES prefix) and 9-digit EIN
format (rejects reserved prefixes 00/07/80-89). No NPPES round-trip,
no IRS e-file lookup — catches the 99% typo case at parse time.

Surface:
- cyclone.npi.is_valid_npi / is_valid_tax_id / normalize_tax_id
- CLI: 'cyclone validate-npi <npi>' and 'cyclone validate-tax-id <ein>'
- API: GET /api/admin/validate-provider?npi=&tax_id=
- Parser validator: new R021_npi_checksum rule (warning, not error,
  to keep test fixtures with placeholder NPIs ingestible)
- minimal_837p.txt fixture NPI updated from '1234567890' to the
  Luhn-valid '1993999998' so strict-mode CLI parses still pass

Tests:
- test_npi.py — 27 cases (Luhn math, valid/invalid NPIs, EIN cases,
  normalize_tax_id edge cases)
- test_api_validate_provider.py — 4 cases (both valid, both invalid,
  omitted NPI, omitted tax_id)
- test_cli_validate.py — 8 cases (valid/invalid for both subcommands,
  exit codes, malformed inputs)
- test_validator.py — 4 new R021 cases (valid Luhn silent, bad Luhn
  warning, skipped when format bad, skipped when NPI missing)

Total: 923 tests pass.
2026-06-21 10:46:10 -06:00

363 lines
12 KiB
Python

from datetime import date
from decimal import Decimal
import pytest
from cyclone.parsers.models import (
Address,
BillingProvider,
ClaimHeader,
ClaimOutput,
Diagnosis,
Envelope,
Payer,
Procedure,
ServiceLine,
Subscriber,
ValidationReport,
)
from cyclone.parsers.payer import PayerConfig
from cyclone.parsers.validator import validate
def _build_claim(**overrides) -> ClaimOutput:
"""Build a passing claim for tests; override fields to break specific rules."""
base = dict(
claim_id="C1",
control_number="991102977",
transaction_date=date(2026, 6, 11),
billing_provider=BillingProvider(
name="Test Provider",
npi="1234567890",
tax_id="123456789",
address=Address(line1="1 Main", city="X", state="CO", zip="80000"),
),
subscriber=Subscriber(
first_name="John",
last_name="Doe",
member_id="M1",
dob=date(1980, 1, 1),
gender="M",
address=Address(line1="1 Main", city="X", state="CO", zip="80000"),
),
payer=Payer(name="COHCPF", id="SKCO0"),
claim=ClaimHeader(
claim_id="C1",
total_charge=Decimal("100.00"),
place_of_service="12",
frequency_code="1",
provider_signature="Y",
assignment="Y",
release_of_info="Y",
),
diagnoses=[Diagnosis(code="Z00", qualifier="ABK")],
service_lines=[
ServiceLine(
line_number=1,
procedure=Procedure(qualifier="HC", code="99213", modifiers=[]),
charge=Decimal("100.00"),
unit_type="UN",
units=Decimal("1.0"),
service_date=date(2026, 6, 11),
)
],
validation=ValidationReport(passed=True, errors=[], warnings=[]),
raw_segments=[],
)
base.update(overrides)
return ClaimOutput(**base)
def test_validate_passing_claim():
cfg = PayerConfig.co_medicaid()
report = validate(_build_claim(), cfg)
assert report.passed is True
assert report.errors == []
def test_r010_clm01_required():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.claim_id = ""
report = validate(claim, cfg)
assert not report.passed
assert any(i.rule == "R010_clm01_present" for i in report.errors)
def test_r011_total_charge_positive():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.total_charge = Decimal("0.00")
report = validate(claim, cfg)
assert any(i.rule == "R011_total_charge_positive" for i in report.errors)
def test_r020_npi_must_be_ten_digits():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.billing_provider.npi = "12345"
report = validate(claim, cfg)
assert any(i.rule == "R020_npi_format" for i in report.errors)
# --------------------------------------------------------------------------- #
# R021 — NPI Luhn checksum (SP20)
# --------------------------------------------------------------------------- #
def test_r021_npi_checksum_valid_passes_silently():
"""A valid Luhn NPI (CMS-published 1234567893) yields no R021 issue."""
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.billing_provider.npi = "1234567893"
report = validate(claim, cfg)
assert not any(i.rule == "R021_npi_checksum" for i in report.errors + report.warnings)
def test_r021_npi_checksum_bad_luhn_is_warning():
"""An NPI that passes format but fails Luhn is a WARNING, not an error."""
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.billing_provider.npi = "1234567890" # right length, wrong check digit
report = validate(claim, cfg)
assert report.passed is True # WARNINGs don't fail the report
assert any(i.rule == "R021_npi_checksum" and i.severity == "warning" for i in report.warnings)
def test_r021_npi_checksum_skipped_when_format_bad():
"""When R020 already flagged the format, R021 stays silent
(avoids a duplicate 'this NPI is wrong' message)."""
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.billing_provider.npi = "12345" # wrong length
report = validate(claim, cfg)
# R020 errors as expected; R021 stays quiet.
assert any(i.rule == "R020_npi_format" for i in report.errors)
assert not any(i.rule == "R021_npi_checksum" for i in report.errors + report.warnings)
def test_r021_npi_checksum_skipped_when_npi_missing():
"""An empty NPI doesn't trigger R021 (only R020 would, but R020
only fires when NPI is *present* and wrong). R021 must stay silent
when NPI is empty so we don't double-fire."""
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.billing_provider.npi = ""
report = validate(claim, cfg)
assert not any(i.rule == "R021_npi_checksum" for i in report.errors + report.warnings)
def test_r030_frequency_allowed():
cfg = PayerConfig.co_medicaid() # only 1, 7, 8
claim = _build_claim()
claim.claim.frequency_code = "5"
report = validate(claim, cfg)
assert any(i.rule == "R030_frequency_allowed" for i in report.errors)
def test_r031_ref_g1_optional_no_error():
"""R031 is informational in v1 — no REF*G1 should not error."""
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
report = validate(claim, cfg)
assert not any(i.rule == "R031_ref_g1_optional" and i.severity == "error" for i in report.errors)
def test_r050_diagnosis_required():
cfg = PayerConfig.co_medicaid()
claim = _build_claim(diagnoses=[])
report = validate(claim, cfg)
assert any(i.rule == "R050_diagnosis_present" for i in report.errors)
def test_r060_service_dates_required():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.service_lines[0].service_date = None
report = validate(claim, cfg)
assert any(i.rule == "R060_service_dates_present" for i in report.errors)
# Now add the date:
claim.service_lines[0].service_date = date(2026, 6, 11)
report = validate(claim, cfg)
assert not any(i.rule == "R060_service_dates_present" for i in report.errors + report.warnings)
def test_r070_charges_sum_warning():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.total_charge = Decimal("999.00") # mismatch
report = validate(claim, cfg)
assert any(i.rule == "R070_charges_sum" and i.severity == "warning" for i in report.warnings)
def test_r100_payer_id_warning_only():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.payer.id = "WRONG"
report = validate(claim, cfg)
assert any(i.rule == "R100_payer_id_matches" and i.severity == "warning" for i in report.warnings)
assert report.passed is True
def test_r032_passes_when_qualifier_b():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.facility_code_qualifier = "B"
report = validate(claim, cfg)
assert not any(i.rule == "R032_clm05_2_facility_qualifier" for i in report.errors + report.warnings)
def test_r032_errors_when_qualifier_unknown():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.facility_code_qualifier = "X"
report = validate(claim, cfg)
assert any(i.rule == "R032_clm05_2_facility_qualifier" and i.severity == "error" for i in report.errors)
assert report.passed is False
def test_r032_skipped_when_qualifier_none():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
# Default _build_claim leaves facility_code_qualifier unset (None).
assert claim.claim.facility_code_qualifier is None
report = validate(claim, cfg)
assert not any(i.rule == "R032_clm05_2_facility_qualifier" for i in report.errors + report.warnings)
def test_r033_passes_with_valid_pos_code():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.place_of_service = "12" # Office — valid CMS POS
report = validate(claim, cfg)
assert not any(i.rule == "R033_clm05_1_place_of_service_code" for i in report.errors + report.warnings)
def test_r033_errors_with_invalid_pos_code():
cfg = PayerConfig.co_medicaid()
claim = _build_claim()
claim.claim.place_of_service = "999" # not a valid CMS POS code
report = validate(claim, cfg)
assert any(i.rule == "R033_clm05_1_place_of_service_code" and i.severity == "error" for i in report.errors)
assert report.passed is False
# --------------------------------------------------------------------------- #
# R034 — REF*G1 enforcement (SP3 Phase 1)
# --------------------------------------------------------------------------- #
def _strict_cfg() -> PayerConfig:
"""Strict cfg that mirrors the CO Medicaid defaults but turns on R034 enforcement.
We don't modify :meth:`PayerConfig.co_medicaid` — the lenient default stays
in v1. This local fixture lets us exercise the strict path without leaking
config changes across the suite.
"""
return PayerConfig(
name="StrictTest",
require_ref_g1_for_adjustments=True,
allowed_bht06={"CH"},
payer_id="X",
)
def test_r034_ref_g1_required_freq_7_no_ref_g1_errors():
cfg = _strict_cfg()
claim = _build_claim()
claim.claim.frequency_code = "7"
claim.raw_segments = []
report = validate(claim, cfg)
assert any(
i.rule == "R034_ref_g1_required" and i.severity == "error" for i in report.errors
)
assert report.passed is False
def test_r034_ref_g1_required_freq_8_no_ref_g1_errors():
cfg = _strict_cfg()
claim = _build_claim()
claim.claim.frequency_code = "8"
claim.raw_segments = []
report = validate(claim, cfg)
assert any(
i.rule == "R034_ref_g1_required" and i.severity == "error" for i in report.errors
)
assert report.passed is False
def test_r034_ref_g1_required_freq_7_with_ref_g1_passes():
cfg = _strict_cfg()
claim = _build_claim()
claim.claim.frequency_code = "7"
claim.raw_segments = [["REF", "G1", "12345"]]
report = validate(claim, cfg)
assert not any(
i.rule == "R034_ref_g1_required" for i in report.errors + report.warnings
)
def test_r034_ref_g1_required_freq_1_no_ref_g1_passes():
cfg = _strict_cfg()
claim = _build_claim()
claim.claim.frequency_code = "1"
claim.raw_segments = []
report = validate(claim, cfg)
assert not any(
i.rule == "R034_ref_g1_required" for i in report.errors + report.warnings
)
def test_r034_ref_g1_lenient_cfg_never_errors():
cfg = PayerConfig.co_medicaid() # require_ref_g1_for_adjustments=False (lenient v1)
claim = _build_claim()
claim.claim.frequency_code = "7"
claim.raw_segments = []
report = validate(claim, cfg)
assert not any(
i.rule == "R034_ref_g1_required" for i in report.errors + report.warnings
)
# --------------------------------------------------------------------------- #
# R035 — BHT06 transaction type code (SP3 Phase 1)
# --------------------------------------------------------------------------- #
def test_r035_bht06_allowed_ch_passes():
cfg = PayerConfig.co_medicaid()
claim = _build_claim(transaction_type_code="CH")
report = validate(claim, cfg)
assert not any(
i.rule == "R035_bht06_allowed" for i in report.errors + report.warnings
)
def test_r035_bht06_allowed_rp_errors_for_co_medicaid():
cfg = PayerConfig.co_medicaid()
claim = _build_claim(transaction_type_code="RP")
report = validate(claim, cfg)
assert any(
i.rule == "R035_bht06_allowed" and i.severity == "error" for i in report.errors
)
assert report.passed is False
def test_r035_bht06_missing_skips():
cfg = PayerConfig.co_medicaid()
claim = _build_claim(transaction_type_code=None)
assert claim.transaction_type_code is None
report = validate(claim, cfg)
assert not any(
i.rule == "R035_bht06_allowed" for i in report.errors + report.warnings
)
def test_r035_bht06_rp_allowed_for_generic_837p():
cfg = PayerConfig.generic_837p() # allows {"CH", "RP"}
claim = _build_claim(transaction_type_code="RP")
report = validate(claim, cfg)
assert not any(
i.rule == "R035_bht06_allowed" for i in report.errors + report.warnings
)