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:
@@ -0,0 +1,80 @@
|
||||
"""Tests for ``cyclone validate-npi`` + ``cyclone validate-tax-id`` (SP20).
|
||||
|
||||
CLI smoke tests — verify exit codes (0 = valid, 1 = invalid) and that
|
||||
the help text references the new subcommands. We don't pipe the value
|
||||
into shared logs (NPI / EIN are PHI / PII); the CliRunner captures it.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cyclone.cli import main
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate-npi
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_cli_validate_npi_valid_exits_zero():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-npi", "1234567893"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "OK" in result.output
|
||||
|
||||
|
||||
def test_cli_validate_npi_bad_checksum_exits_one():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-npi", "1234567890"])
|
||||
assert result.exit_code == 1
|
||||
assert "INVALID" in result.output
|
||||
|
||||
|
||||
def test_cli_validate_npi_wrong_length_exits_one():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-npi", "12345"])
|
||||
assert result.exit_code == 1
|
||||
assert "INVALID" in result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate-tax-id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_cli_validate_tax_id_formatted_exits_zero():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-tax-id", "72-1587149"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "721587149" in result.output # normalized form echoed
|
||||
|
||||
|
||||
def test_cli_validate_tax_id_unformatted_exits_zero():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-tax-id", "721587149"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_cli_validate_tax_id_reserved_prefix_exits_one():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-tax-id", "00-1234567"])
|
||||
assert result.exit_code == 1
|
||||
assert "reserved" in result.output.lower()
|
||||
|
||||
|
||||
def test_cli_validate_tax_id_malformed_exits_one():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["validate-tax-id", "not-an-ein"])
|
||||
assert result.exit_code == 1
|
||||
assert "9-digit" in result.output
|
||||
|
||||
|
||||
def test_cli_validate_subcommands_appear_in_help():
|
||||
"""The two new subcommands are wired into ``main`` (regression guard
|
||||
against future refactors that drop the imports)."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "validate-npi" in result.output
|
||||
assert "validate-tax-id" in result.output
|
||||
Reference in New Issue
Block a user