Files
cyclone/backend/tests/test_parse_837.py
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

71 lines
2.5 KiB
Python

from datetime import date
from pathlib import Path
from cyclone.parsers.parse_837 import parse
from cyclone.parsers.payer import PayerConfig
FIXTURE = Path(__file__).parent / "fixtures" / "minimal_837p.txt"
def test_parse_minimal_fixture_returns_one_claim():
text = FIXTURE.read_text()
result = parse(text, PayerConfig.co_medicaid())
assert len(result.claims) == 1
claim = result.claims[0]
assert claim.claim_id == "CLM001"
assert claim.billing_provider.npi == "1993999998"
assert claim.subscriber.last_name == "Doe"
assert claim.subscriber.first_name == "John"
assert claim.subscriber.member_id == "ABC123"
assert claim.payer.id == "SKCO0"
assert claim.claim.frequency_code == "1"
assert claim.claim.place_of_service == "12"
assert claim.claim.prior_auth == "PA123"
assert claim.diagnoses[0].code == "Z00"
assert claim.diagnoses[0].qualifier == "ABK"
assert len(claim.service_lines) == 1
sl = claim.service_lines[0]
assert sl.procedure.code == "99213"
assert sl.procedure.qualifier == "HC"
assert sl.charge == 100.00
def test_parse_envelope_captured():
result = parse(FIXTURE.read_text(), PayerConfig.co_medicaid())
assert result.envelope is not None
assert result.envelope.control_number == "991102977"
assert result.envelope.transaction_date == date(2026, 6, 11)
assert result.envelope.sender_id == "11525703"
def test_parse_summary_counts():
result = parse(FIXTURE.read_text(), PayerConfig.co_medicaid())
assert result.summary.total_claims == 1
assert result.summary.passed == 1
assert result.summary.failed == 0
def test_parse_continues_on_malformed_subscriber():
"""A bad subscriber record must not abort the whole batch."""
text = FIXTURE.read_text().replace("MI*ABC123", "MI*") # truncate member id segment
result = parse(text, PayerConfig.co_medicaid())
# Should still find the claim, but with errors
assert len(result.claims) == 1
assert result.claims[0].validation.passed is False
assert result.summary.failed == 1
def test_parse_preserves_raw_segments():
result = parse(FIXTURE.read_text(), PayerConfig.co_medicaid())
claim = result.claims[0]
assert any(s[0] == "CLM" for s in claim.raw_segments)
assert any(s[0] == "SV1" for s in claim.raw_segments)
def test_parse_uses_generic_config_when_requested():
result = parse(FIXTURE.read_text(), PayerConfig.generic_837p())
assert len(result.claims) == 1
# No patient-loop rule on generic config
assert result.claims[0].validation.passed is True