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.
This commit is contained in:
@@ -100,6 +100,53 @@ def test_r020_npi_must_be_ten_digits():
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user